npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

medusa-plugin-ordinant

v0.1.5

Published

Ordinant compliance for Medusa v2: catalog classification with merchant attestation and a deterministic, citation-bearing checkout gate. Compliance information and automation - not legal advice.

Downloads

49

Readme

medusa-plugin-ordinant

Ordinant compliance for Medusa v2: AI-proposed product classification with merchant attestation, and a deterministic, citation-bearing checkout gate.

Published on npm — install with npm install medusa-plugin-ordinant (npmjs.com/package/medusa-plugin-ordinant).

Ordinant is a compliance information and automation service — not legal advice. No attorney–client relationship is created by its use. Merchants attest to the final classification of their products.

Contents

  1. What it does
  2. Requirements
  3. Installation
  4. Configuration
  5. How it works
  6. The admin dashboard
  7. The per-product widget
  8. The checkout gate
  9. Buyer verification assertions
  10. Recommended: early compliance preview
  11. Onboarding an existing catalog
  12. Admin API reference
  13. Store API reference
  14. Troubleshooting
  15. Local development

What it does

| Moment | What happens | |---|---| | Product created or updated | Variants are sent to Ordinant; an LLM proposes dictionary attributes with a confidence score | | Merchant opens the product page | The Ordinant compliance widget shows each SKU's status; the merchant confirms or edits the proposal — recording their attestation | | Merchant opens the Ordinant dashboard | A catalog-wide triage queue: an "illegal orders stopped" impact metric, and bulk SKU-generation + classification + confirmation | | Buyer completes checkout | Every line item is decided against the destination's rules; blocks and unmet requirements stop the order with statute citations in the error message |

Three properties worth knowing up front:

  • No storefront changes required for the gate. It runs inside the backend's cart completion workflow; failures surface through the standard checkout error path every storefront already renders. (An optional early-preview UX can be wired in, and recommended — see Recommended: early compliance preview.)
  • Fail closed, always. Unclassified SKUs cannot sell. An unreachable decision engine blocks checkout (configurable, see failMode). A wrong "allow" never happens silently.
  • The plugin holds no compliance logic. Decisions are made, logged, and replayable on the Ordinant server; the plugin carries data and citations.

Requirements

  • Medusa v2.4+ (built and tested against 2.17)
  • Node.js 20+
  • A running Ordinant deployment and a merchant API key
  • Ordinant's catalog-classification module enabled server-side (ORDINANT_CLASSIFY_ENABLED=true on the Ordinant deployment) if you want automatic proposals — the widget and gate work without it, but proposals must then be created via Ordinant's API directly

Installation

1. Install the package

npm install medusa-plugin-ordinant
# or: yarn add medusa-plugin-ordinant / pnpm add medusa-plugin-ordinant

pnpm users: ensure @tanstack/react-query matches the version used by @medusajs/dashboard (see Medusa's admin-customization docs) or the admin widget will fail to resolve its peer dependency.

2. Register the plugin

In medusa-config.ts:

module.exports = defineConfig({
  projectConfig: {
    // ... your existing config
  },
  plugins: [
    {
      resolve: "medusa-plugin-ordinant",
      options: {
        baseUrl: process.env.ORDINANT_BASE_URL,
        apiKey: process.env.ORDINANT_API_KEY,
      },
    },
  ],
})

3. Set environment variables

In the backend's .env:

ORDINANT_BASE_URL=https://your-ordinant-deployment.example
ORDINANT_API_KEY=ord_live_...

The API key is a merchant key issued by your Ordinant deployment (ORDINANT_API_KEYS on the server). It never reaches the browser: the admin widget talks to plugin-provided backend routes, which call Ordinant server-side.

4. Restart and verify

npm run dev

If baseUrl or apiKey is missing the plugin does not crash — it boots into a degraded, un-activated state: the Ordinant dashboard shows an activation gate instead of data, and the checkout gate treats the engine as unreachable (honoring failMode). A half-configured install never takes down the backend.

To verify a good connection, open the Ordinant page in the admin sidebar: the header's engine-health pill should read "Engine connected · <ruleset version>" and your catalog should load.

Configuration

All plugin options:

| Option | Required | Default | Purpose | |---|---|---|---| | baseUrl | ✅ | — | Ordinant API base URL | | apiKey | ✅ | — | Merchant bearer key | | timeoutMs | — | 5000 | Per-request timeout; checkout must not hang | | failMode | — | "closed" | Gate behavior when Ordinant is unreachable: "closed" blocks completion; "open" completes the order and logs an error. Choose "open" only as a deliberate, documented risk decision — it means orders ship unchecked during an outage. | | skuMode | — | "block" | What the gate does with a cart line that has no SKU. Ordinant identifies (and gates) products by SKU, so a SKU-less item can't be checked. "block" fails closed — checkout stops with a message to assign a SKU. "allow" skips SKU-less items; choose it only if you knowingly sell unregulated products without SKUs (gift cards, services) and accept they are never gated. |

How it works

product.created / product.updated
        │  (subscriber)
        ▼
classifyProductWorkflow ──► POST {ordinant}/v1/catalog/classify
        │                    LLM proposes attributes + confidence
        ▼
proposal stored on Ordinant (status: proposed — cannot sell yet)
        │
        ▼  merchant reviews in the product-page widget
POST /admin/ordinant/classifications/{sku}/confirm
        │  attestation recorded (merchant is the classifier of record)
        ▼
SKU is confirmed — checkout decisions now resolve its attributes
        │
        ▼  buyer completes checkout
completeCartWorkflow.hooks.validate
        └─► POST {ordinant}/v1/decisions  (bare SKUs + destination + buyer)
             ALLOW → order places
             CONDITIONAL/BLOCK → completion stops, citations in the error

Classification failures (Ordinant down, module disabled, odd product data) are logged and swallowed — catalog work is never disrupted. The SKU simply stays unclassified, which the gate fails closed on.

The admin dashboard

A dedicated Ordinant page in the admin sidebar (its icon swaps to your logo — see Branding) is the catalog-wide cockpit the per-product widget can't give you. It's built as a triage queue: it surfaces what needs action first, not a wall of everything.

  • Activation gate. Until a valid API key is configured, the whole page is blurred behind a lock card linking to your Ordinant sign-up — an un-activated install looks intentional, not broken.
  • Welcome header + engine-health pill. A greeting (by the signed-in admin's first name) and a live status pill — Engine connected · <ruleset version>, Engine unreachable, or Not activated — from GET /admin/ordinant/status.
  • Impact hero — "Illegal orders stopped." The headline metric, from GET /admin/ordinant/stats: an animated count of blocked orders with a Day / Month / Year / All toggle, a trend chart, and KPI cards (all-time stopped, top blocked destination, stopped this year). Before the first block it reads "You're protected — no illegal orders yet" rather than a bare zero.
  • Filter tiles. Click to filter: Needs attention (the default — everything actionable: needs-SKU + unclassified + to-review), Needs SKU, Unclassified, To review, Confirmed, All. A caught-up queue shows a calm "You're all caught up — N SKUs protected" state.
  • Search + pagination. Filter by product title or SKU. The queue views paginate product cards; All / Confirmed switch to a dense, paginated table (editing a row there opens a side drawer) so hundreds of SKUs stay scannable. Filter, search, and page are kept in the URL — shareable and back-button friendly.
  • Bulk actions (top-right, shown only when relevant):
    • Generate N SKUs — assign readable, collision-free SKUs to every SKU-less variant, then auto-classify them: one hands-off action with a live progress bar.
    • Classify N — classify the unclassified backlog in chunks.
    • Confirm N ready — attest high-confidence proposals (≥ 85%) in one go. Because every confirmation is a legal attestation, the dialog shows a category-grouped breakdown of exactly what you're attesting ("212 accessories, 40 magazines, 12 handguns"), not a bare count.

Attributes are shown as plain-language chips (Magazine · 30 rounds · non-NFA), never raw JSON. Typical onboarding: open the page → Generate N SKUsClassify NConfirm N ready for the confident batch → review the handful that need judgment with the dropdown editor.

Branding

Drop your company media in one place — src/admin/components/brand-assets.tsx:

export const BRAND = {
  name: "Ordinant",
  logo: "",      // square logo (data: URI or imported asset) → sidebar icon, gate, empty-state marks
  wordmark: "",  // optional horizontal wordmark → welcome header
  website: "https://ordinant.com", // gate CTA target
}

Set logo and the sidebar icon, activation gate, and empty-state marks all pick it up; leave it empty to use the built-in shield. These are build-time constants, so set them before building the plugin.

The per-product widget

On every product page (product.details.after zone):

  • Per-SKU status: unclassified, proposed · NN% (hover for the model's notes), or confirmed (hover for the attestation timestamp).
  • Generate SKUs for SKU-less variants — the same helper as the dashboard.
  • Confirm accepts the proposal as-is. Edit opens a dropdown/checkbox attribute editor generated from the compliance dictionary — never raw JSON — so the confirmed set is your assertion, corrected in a guided form.
  • Classify (re-)submits the product's variants — use it for products created before the plugin was installed, or after editing product data.
  • Invalid attribute edits are rejected by Ordinant's dictionary validation and shown inline — garbage can't be attested.

Category-adaptive fields. The editor shows only the attributes relevant to the chosen category — pick optic and you won't see magazine capacity, action type, or NFA class; pick knife and you get blade type and length. This is built into the plugin (a category → fields map) and also honors the engine dictionary's applies_to when present, so it's correct without waiting on a dictionary change.

Re-classifying a SKU (e.g. after a product edit) resets it to proposed: a stale attestation never survives changed product data.

Live shipping-impact preview. While editing a classification, the widget shows "With this configuration, shipping/checkout will be denied to: CA, NY, …" (and a separate "requires FFL/permit/age in …" list), swept across every modeled jurisdiction — so you see exactly where a classification will and won't sell before you attest to it. Fields with subtle legal meaning carry inline guidance (e.g. for a suppressor part, nfa_class = Suppressor for the sound- reducing core, None for an external accessory like an end cap).

The checkout gate

Consumes completeCartWorkflow.hooks.validate, which runs before any order is created. Semantics:

  • Items: every cart line is checked by its variant_sku. A line with no SKU cannot be identified or gated, so it fails closed (blocks checkout) by default — silently skipping it would be a fail-open hole on a compliance gate. Give every sellable variant a SKU; or set skuMode: "allow" if you knowingly sell unregulated SKU-less items and accept they go ungated. (In Medusa every product has at least one variant, but a variant's SKU is optional — this is about SKU presence, not variant count.)
  • Bare-SKU decisions: the gate sends empty attributes; Ordinant resolves the merchant's confirmed classifications server-side. Proposed-but- unconfirmed classifications are never used.
  • Destination: from the cart's shipping address (province = US state). A missing state blocks completion with a clear message. Non-US destinations skip the gate entirely (Ordinant is US-only in v1) with a warning log.
  • Outcomes: ALLOW → order proceeds (the decision ID and ruleset version are logged for audit). BLOCK / unmet CONDITIONAL → a MedusaError stops completion; the message lists each problem SKU with its requirement or prohibition and the statute citations. Storefronts render this through their normal checkout error handling — the Next.js starter shows it on the payment step with no modification.
  • Idempotency: decisions are keyed medusa-cart:{cart_id}, so checkout retries never double-log on the Ordinant side.
  • Unclassified SKUs produce a distinct message ("product is not yet classified for compliance") so merchants immediately know it's an onboarding gap, not a legal block.

Buyer verification assertions

Conditional effects (age gates, FFL routing, permits) are satisfied by verification assertions — Ordinant never receives identity documents. Upstream integrations (BlueCheck/AgeChecker-style age verification, FFL selection UIs) should write their results to cart.metadata.ordinant_buyer before completion:

{
  "age_verified": true,
  "verified_age": 27,
  "ffl_verified": true,
  "sot_verified": false,
  "permits": ["il_foid"],
  "fulfillment_method": "ship"
}

Until such an integration exists, conditional requirements simply block completion with a message stating exactly what's missing (e.g. "route shipment to a verified FFL [18 U.S.C. § 922(b)(3)]") — which is the correct fail-safe default.

Recommended: early compliance preview (progressive checkout UX)

Strongly recommended for every storefront. It's a few lines to wire, it can't break checkout (fail-open, backstopped by the mandatory gate), and it transforms the experience: the buyer learns their order can't ship to their state — with the statute citation — the instant they enter the address, right where they can fix it, instead of after they've entered a card at "Place Order". This is the difference between compliance that feels like a dead end and compliance that feels helpful.

The checkout gate above is the authoritative, non-bypassable enforcement point: it runs server-side at cart completion and stops non-compliant orders no matter what the storefront does. That is mandatory and needs zero storefront work.

Surfacing a problem earlier — the moment a buyer picks a shipping state instead of at "Place Order" — is the recommended UX layer on top. The plugin exposes an advisory, non-logging preview for exactly this:

  • POST /store/ordinant/preview (storefront, publishable key) — body { "cart_id": "...", "destination"?: { "state", ... }, "buyer"?: { ... } }. It resolves the cart's items server-side; the destination comes from the optional destination (a proposed, not-yet-saved address) or the cart's saved shipping address. Returns what the gate would decide. It never blocks and fails open — it is purely advisory.
  • It proxies to Ordinant's POST /v1/decisions/preview, which is non-logging by default so re-previews don't inflate anything — with one deliberate exception: a BLOCK is recorded once per cart (deduped by an idempotency key the store route sends). A buyer turned away at the address step is a real "stopped illegal order," so it counts toward the merchant's blocked-order metric — but repeat previews of the same cart never double-count, and ALLOW/CONDITIONAL previews are never logged.

Two rules keep this safe:

  1. The preview is advisory; the completeCart gate is the guarantee. Client checks are bypassable, so they can never be the enforcement point. Always keep the gate — the preview only complements it.
  2. Surface each requirement where its input appears. The destination verdict (PII-free) resolves as soon as the shipping state is known — the highest-value early signal, and it fires before payment authorization, so you never authorize a card you're about to block. Age/FFL requirements surface at their own steps once those assertions exist.

Wiring it into your checkout

Two small steps — the same everywhere; only where you hook in differs.

Step 1 — find where your checkout saves the shipping address. That's the action behind your "Continue to delivery" (or equivalent) button.

| Storefront | Hook point | |---|---| | Medusa Next.js starter | the setAddresses server action in src/lib/data/cart.ts | | Any custom storefront | wherever you call sdk.store.cart.update({ shipping_address }) (i.e. POST /store/carts/{id}) to save the address |

Step 2 — check the proposed address before you save it, and stop if it's blocked. Import the helper the plugin ships:

import { checkAddressCompliance } from "medusa-plugin-ordinant/storefront"

// in your address-submit handler, BEFORE you persist the address:
const block = await checkAddressCompliance(sdk, {
  cartId,
  destination: { state, country_code }, // the values the buyer just entered
})
if (block) {
  return block          // show `block`; do NOT save the address
}
await saveTheAddress()  // your existing cart update
  • sdk is your Medusa JS SDK (anything with a client.fetch).
  • block is a ready-to-display message with statute citations, or null.
  • Fail-open: any error, or an unconfigured plugin, returns null — it can never break checkout. Only a hard BLOCK returns a message; a CONDITIONAL (age/FFL not gathered yet) returns null and proceeds to the next step.
  • Checking the proposed destination before saving means a blocked address is never written to the cart — a buyer who backs out and re-enters checkout won't find a restricted address waiting.

Display: render the returned string wherever your form shows errors. In the Next.js starter, setAddresses already returns its errors to an <ErrorMessage>, so you just return block — no component change:

// src/lib/data/cart.ts
import { checkAddressCompliance } from "medusa-plugin-ordinant/storefront"

export async function setAddresses(currentState: unknown, formData: FormData) {
  try {
    // ...parse the form into `data`...
    const block = await checkAddressCompliance(sdk, {
      cartId,
      destination: {
        state: String(formData.get("shipping_address.province") ?? ""),
        country_code: String(formData.get("shipping_address.country_code") ?? ""),
      },
    })
    if (block) return block   // stops the redirect; shown by <ErrorMessage>
    await updateCart(data)    // save only when clear
  } catch (e: any) {
    return e.message
  }
  redirect(`/${countryCode}/checkout?step=delivery`)
}

Prefer not to add the dependency? checkAddressCompliance is a single, self-contained function — copy it from src/storefront.ts into your codebase instead. Either way, anything you don't wire is still caught by the completeCart gate; the preview is pure upside.

Also re-check when the shopper enters checkout (defense in depth)

The address-step check catches a bad address as it's entered. But a shopper can set a compliant address, then add an item that's illegal to their state, and walk up to the review step — where the block would otherwise only appear at "Place Order". Close that gap by re-checking the whole cart when they land on (or return to) the checkout page, and bouncing them back to the shipping step if it's blocked.

The same preview does it — pass the cartId with no destination and it evaluates the cart's saved address against its current items server-side:

import { checkAddressCompliance } from "medusa-plugin-ordinant/storefront"

// on entering checkout (any storefront):
const block = await checkAddressCompliance(sdk, { cartId }) // no destination = full current cart
if (block) {
  // send them back to the shipping step and show `block`
}

In the Medusa Next.js starter, do it in the checkout page (a server component) so the redirect happens before payment/review render:

// src/app/[countryCode]/(checkout)/checkout/page.tsx
import { getCartComplianceBlock } from "@lib/data/cart" // thin wrapper over the preview
import { redirect } from "next/navigation"

const complianceBlock = await getCartComplianceBlock(cart.id)
if (complianceBlock && step !== "address" && step !== "delivery") {
  redirect(`/${countryCode}/checkout?step=delivery`) // bounce back to shipping
}
// ...and render `complianceBlock` as a banner above <CheckoutForm />.

It only redirects when the shopper is past the shipping step, and it targets the shipping step — so there's no redirect loop, and they can fix the address or remove the item. Fail-open, same as the address check.

The three layers together: (1) the address step vets the address as it's entered, (2) checkout entry re-checks the whole cart, (3) the completeCart gate is the non-bypassable backstop at "Place Order". Wire 1 and 2, or neither — 3 always holds.

Onboarding an existing catalog

Products created after installation classify automatically. For the existing catalog:

  • Small catalogs: open each product and click Classify.
  • Bulk: call Ordinant's POST /v1/catalog/classify directly with your merchant key, in chunks of ~100 SKUs per request (see the Ordinant API reference for the payload). Then confirm in the widget, or via POST /v1/catalog/classifications/{sku}/confirm for programmatic confirmation of high-confidence proposals — remembering that confirmation is your attestation.

Classify everything, including non-regulated products (t-shirts and cleaning kits classify as accessory): the gate fails closed on unclassified SKUs by design. An unclassified hoodie blocking checkout is an onboarding gap; a misclassified suppressor shipping is a felony. The design prefers the first failure.

Admin API reference

Routes the plugin adds to your Medusa backend (admin-authenticated; they proxy to Ordinant server-side):

| Route | Purpose | |---|---| | GET /admin/ordinant/overview | Every store variant × classification state (incl. needs_sku) + summary counts | | GET /admin/ordinant/products/{id}/overview | Per-product version of the above (the widget's display query) | | GET /admin/ordinant/dictionary | The attribute dictionary the editor renders its dropdowns from | | POST /admin/ordinant/products/{id}/generate-skus | Assign readable, collision-free SKUs to a product's SKU-less variants | | POST /admin/ordinant/classify-all | Classify the next chunk of unclassified SKUs; returns remaining so the caller loops | | POST /admin/ordinant/classifications/bulk-confirm | Attest a batch of proposals as-is; body {"skus": [...]} | | GET /admin/ordinant/classifications?skus=a,b,c | Classification records, optionally filtered to specific SKUs (the widget's display query) | | POST /admin/ordinant/classifications/{sku}/confirm | Record the merchant's attestation; body {"attributes": {...}} | | POST /admin/ordinant/products/{id}/classify | (Re-)classify a product's variants on demand | | GET /admin/ordinant/status | Readiness for the dashboard: configured, engine_reachable, ruleset_version | | GET /admin/ordinant/stats | Blocked-order metrics ("stopped illegal orders") for the impact hero |

Store API reference

Routes the plugin adds for the storefront (publishable-key auth, guest-safe):

| Route | Purpose | |---|---| | POST /store/ordinant/preview | Advisory, non-logging compliance preview for a cart; body {"cart_id": "...", "destination"?: {...}, "buyer"?: {...}} — optional destination vets a proposed address before it's saved. Never blocks, fails open. See Recommended: early compliance preview. |

Troubleshooting

| Symptom | Cause / fix | |---|---| | Dashboard shows the activation gate / health pill reads "Not activated" | ORDINANT_BASE_URL / ORDINANT_API_KEY are unset or empty — set them and pass them in the plugin options, then restart. The plugin degrades gracefully rather than crashing when unconfigured | | Health pill reads "Engine unreachable" or widget shows "Could not reach Ordinant" | Ordinant deployment down or wrong baseUrl; check backend logs for the underlying error | | Widget loads but classify returns an error | Ordinant's classification module is disabled server-side — set ORDINANT_CLASSIFY_ENABLED=true + ANTHROPIC_API_KEY on the Ordinant deployment (not in Medusa) | | Every checkout is blocked with "not yet classified" | Working as designed — confirm classifications for every sellable SKU, including non-regulated ones | | Checkout blocked with "Compliance check is temporarily unavailable" | Ordinant unreachable and failMode is closed (default). Restore connectivity; only set failMode: "open" if you accept unchecked orders during outages | | Checkout blocked with a permit/FFL/age requirement | Correct legal outcome for that destination — the citation is in the message. Recovering these sales requires the corresponding verification integration writing cart.metadata.ordinant_buyer | | 401s from Ordinant in logs | Wrong or rotated merchant key | | Gate never fires | Cart items have no variant_sku, or the destination isn't US |

Local development

This plugin lives in a workspace inside the ordinant-test monorepo:

# build the plugin (server + admin extensions)
cd plugins/medusa-plugin-ordinant && npm run build

# run the backend against it (workspace-linked automatically)
cd apps/backend && npm run dev

# live-reload plugin development (publishes to the local yalc registry)
cd plugins/medusa-plugin-ordinant && npm run dev

Point ORDINANT_BASE_URL at a local engine (go run ./cmd/ordinantd in the Ordinant repo, default http://localhost:8080) for a fully local loop.

To publish: npm publish (the prepublishOnly hook builds .medusa/server).