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

@mi9-identity/jwt-verifier

v1.0.0

Published

Mi9 JWT verifier — pure RS256/JWKS core for verifying tokens issued by the Mi9 Identity Service. Framework-agnostic.

Readme

@mi9-identity/jwt-verifier

Pure RS256/JWKS verifier for Mi9-issued JWTs. Framework-agnostic — used as the verification core by @mi9-identity/auth-middleware-express, raw HTTP handlers, and operational scripts.

Apache-2.0-licensed and free to use; access to the Mi9 Identity Platform whose tokens it verifies is governed by your commercial agreement with Mi9 Retail.

Which package do I need?

| Package | Use it when | | --- | --- | | @mi9-identity/token-client | Your service calls a Mi9 API and needs a token. | | @mi9-identity/jwt-verifier (this one) | Your service receives Mi9-issued tokens and must verify them. | | @mi9-identity/auth-middleware-express | The same, and your service is an Express 5 app — it wraps this verifier as middleware. |

A service that both calls Mi9 APIs and serves Mi9-authenticated requests installs two of them.

Install

npm install @mi9-identity/[email protected]

Versions are semver, and the three @mi9-identity/* packages (jwt-verifier, token-client, auth-middleware-express) are released in lockstep at the same version — pin an exact version rather than a ^ or ~ range. A breaking change to the HTTP wire shapes ships as a new API path version (/api/v2), not as a package major bump.

The package targets ESM and Node 24+. Use await import() if you consume it from CommonJS.

Configuration

Verifying needs no credential: the signing keys are public and served from the issuer's JWKS URL. You need two values, both given to you by your Mi9 contact when your service is registered — the issuer URL, and the audience identifier assigned to your service.

Configure one issuer URL and one audience:

MI9_IDENTITY_ISSUER     # https://identity.mi9retail.com/api/v1  — matches the JWT `iss` claim
MI9_IDENTITY_AUDIENCE   # this service's own audience identifier (a full URL)

The issuer URL is the versioned one: /api/v1 (or a future /api/v2) is part of the configured value, and every endpoint URL is derived from it.

Audience values are always full URLs of the form https://<host>/<path>, never a bare service name — for example https://merchant.mi9retail.com/api, not merchant. A token is valid for your service when your configured audience appears in its aud.

Usage

import { createVerifier, mi9IdentityJwksEndpoint } from '@mi9-identity/jwt-verifier';

const jwksUri = mi9IdentityJwksEndpoint('https://identity.mi9retail.com/api/v1');

const verifier = createVerifier({
    jwksUri,
    issuers: ['https://identity.mi9retail.com/api/v1'],
    audience: 'https://merchant.mi9retail.com/api',
});

const ctx = await verifier.verify(token);
console.log(ctx.retailerId, ctx.product);

mi9IdentityJwksEndpoint — single-source-of-truth helper

Consumers configure one issuer URL (MI9_IDENTITY_ISSUER) that matches the JWT iss claim — e.g. https://identity.mi9retail.com/api/v1. The helper appends the bare JWKS path to that single value. It contains no /api/v1 literal; the version lives in the caller's issuer URL, so a future v2 cutover is a one-env-var change at each consumer.

Inbound verification is this package's only role, so the mint and credential-claim URLs are not derived here — mi9IdentityEndpoints(issuer) in @mi9-identity/token-client returns { tokenEndpoint, credentialEndpoint }. Neither package depends on the other, so an outbound-only service never installs a JOSE stack for a string concat.

The Mi9 Identity Service does not publish /.well-known/openid-configuration. Generic OAuth client libraries that auto-discover will fail; configure the issuer URL, the JWKS URI, and the token endpoint explicitly, as above.

VerifierOptions reference

| Option | Required | Default | Purpose | | --- | --- | --- | --- | | jwksUri | yes | — | Full JWKS URL; use mi9IdentityJwksEndpoint(...). | | issuers | yes | — | Allowlist of acceptable iss values (no wildcards). | | audience | yes | — | string \| string[] — the token's aud must intersect this. | | clockToleranceSec | no | 30 | Leeway for exp/iat/nbf, in seconds. | | jwksCacheTtlMs | no | 3_600_000 (1 h) | How long a fetched JWKS is served before a re-fetch. | | jwksInitialJitterMs | no | 30_000 | Random delay before the first JWKS fetch — thundering-herd guard. | | jwksCooldownMs | no | 30_000 | Min interval between unknown-kid re-fetches. | | jwksFetchTimeoutMs | no | 5_000 | Per-fetch timeout for a JWKS download (jose's timeoutDuration). | | logger | no | none | Optional pino-shaped Logger; see Logging below. |

Logging

Pass a pino-compatible Logger to receive observability events from the verifier core. The verifier emits exactly one log event:

  • warn — event: 'jwks_unavailable', message 'jwt-verifier.jwks_unavailable': fired when JwksUnavailableError is thrown (JWKS fetch failed and no fresh cache is available). This is the only event the framework-agnostic core emits; richer observability (per-request audit, scope/retailer guard events) is the adapter's job.

See the JWKS availability section below for the failure semantics this event signals.

Validation pipeline

  1. Extract Bearer token (caller's responsibility — verifier accepts the raw string).
  2. Decode header, read kid and iss.
  3. iss must be in the configured allowlist.
  4. Fetch JWKS from the explicitly-configured jwksUri (the verifier never derives the URL from iss). The underlying jose.createRemoteJWKSet caches the document for jwksCacheTtlMs (default 1h) and applies a cooldownDuration (default 30s) between unknown-kid refreshes — so an unknown kid triggers a single re-fetch and is then memoised for the cooldown window before retrying.
  5. Verify RS256 signature against the key for kid.
  6. Validate aud (OR-membership against configured audiences) and exp / nbf / iat with a 30-second default clock skew leeway.
  7. Extract custom claims and return AuthContext.

AuthContext

Returned on success:

| Field | Type | Notes | | --- | --- | --- | | iss | string | Issuer (versioned, e.g. …/api/v1). | | sub | string | Is the clientId (UUIDv7) — no separate clientId claim. | | aud | string \| string[] | Scalar for one audience, array for several. | | exp / iat / nbf | number | Unix seconds. | | jti | string | Token id. | | scope | string | Raw space-separated scope string. Empty today — Mi9 credentials are provisioned with no authorized scopes, so do not authorize on it. | | scopes | readonly string[] | Pre-split, deduped view of scope. Empty today. | | retailerId | string | From the https://mi9retail.com/retailerId claim. | | product | string | Product the credential belongs to. | | storeCode | string \| null | Always present; null unless the credential is store-bound. |

The authClass claim (issuer-internal, used only by the rotation-pickup path) is stripped before AuthContext is returned. The verifier exposes only an explicit allowlist of fields — anything else is dropped, so future internal claims added by the issuer cannot leak to gateway code.

Error taxonomy

Typed errors thrown on failure:

| Error | When | | --------------------------- | ---------------------------------------------- | | MissingTokenError | (Adapter-level — the verifier itself receives a string.) | | InvalidTokenError | Signature mismatch, malformed token, decode failure. | | TokenExpiredError | exp in the past (after leeway). | | IssuerNotAllowedError | iss not in allowlist. | | AudienceMismatchError | aud does not intersect configured audiences. | | AlgorithmNotAllowedError | Header alg ≠ RS256. | | MissingRequiredClaimError | Required claim absent (e.g. retailerId). | | JwksUnavailableError | JWKS fetch failed and no fresh cached key set (within jwksCacheTtlMs) is available. |

JWKS availability

jose.createRemoteJWKSet serves keys from its in-memory cache only while the cache is fresh — younger than jwksCacheTtlMs (default 1h). Once the cache is stale, the next verify() triggers a refetch; if that refetch fails, the verifier throws JwksUnavailableError (a 503-shaped error). A fresh cache is still served through a transient endpoint outage, so a blip shorter than the TTL is invisible — but a stale cache is not used as a fallback. The verifier never falls back to an unauthenticated path; it never fails open.

Unknown kid after signing-key rotation

A token signed by a newly-rotated signing key whose kid is not yet in the cached JWKS maps to InvalidTokenError (401), not JwksUnavailableError (503): the JWKS is reachable, the key is simply absent from it for now. jose refetches once on an unknown kid, then memoises the miss for jwksCooldownMs (default 30s) before retrying — so such tokens 401 for at most that window, then recover on the next mint. This is deliberately conservative; the issuer publishes a rotated signing key to the JWKS document ahead of minting tokens under it, so the window should not arise in a normal rotation.

Signing-key rotation is otherwise transparent to consumers — there is nothing to configure or rotate on your side.

Troubleshooting

| Symptom | Likely cause | Fix | | --- | --- | --- | | Requests fail with JwksUnavailableError (503) | The JWKS document is unreachable and no fresh cache is left | Transient; confirm your service can reach ${issuer}/.well-known/jwks.json. | | 401s right after a deploy or a signing-key rotation | The token was signed by a key not yet in the cached JWKS | Self-heals within jwksCooldownMs; callers recover on their next mint. | | AudienceMismatchError | The configured audience is a bare name, or does not match the token's aud | Set it to the full-URL audience the issuer mints for your service. | | IssuerNotAllowedError | The configured issuer URL omits the /api/v1 path segment | Configure the versioned issuer URL, exactly as it appears in the token's iss. |


Outbound token acquisition lives in @mi9-identity/token-client; the Express 5 adapter — scope/retailer guards and the RFC 6750 error mapping — lives in @mi9-identity/auth-middleware-express.

License

Apache-2.0. The full text ships in the package as LICENSE.