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

@xemahq/oidc-guard

v0.6.0

Published

Framework-agnostic OIDC / JWT verification and multi-tenancy primitives — JWKS fetching, issuer-metadata resolution, signature validation, tenant-claim projection and tenant fencing.

Readme

@xemahq/oidc-guard

Layer 0 — pure, framework-agnostic OpenID-Connect JWT verification and multi-tenancy primitives. Zero runtime dependencies (Node crypto + fetch only); zero framework dependency; zero domain concepts.

Verification

The single, shared implementation of the error-prone bits every consumer previously copied by hand:

  • JwksVerifier — per-(cacheKey, kid) signing-key cache, exp/nbf checks, optional issuer pinning, RS/PS/ES signature verification. Fail-closed.
  • IssuerMetadataResolver — resolves a provider's jwks_uri from its OIDC discovery document (<base>/.well-known/openid-configuration), so no caller hard-codes a provider-specific keys path. Cached, fail-closed.
  • IssuerTenantResolver — the one IdP-shaped seam: how an iss maps to a security domain and to a reachable discovery URL. Ships a Keycloak (…/realms/<domain>), a generic path-segment and an Entra implementation.
  • OidcTokenVerifier — the high-level primitive: discover the JWKS endpoint for a token's security domain and verify the signature + temporal + issuer claims, returning the decoded payload.

Choosing the resolver

A deployment declares its IdP family; the selection lives in one call, so no consumer parses iss or builds a discovery URL itself.

const resolver = createIssuerTenantResolver(
  process.env.IDP_KIND, // or omit — IDP_KIND is read when nothing is passed
);
const verifier = new OidcTokenVerifier({ tenantResolver: resolver });

IdpKind is a closed set — keycloak (the default) | oidc | entra | cognito. An explicit argument outranks IDP_KIND; a blank declaration counts as unset and yields the default; anything else unrecognised throws, naming the set. There is no silent Keycloak fallback: an IdP nobody implemented must fail at boot, not authenticate against the wrong issuer shape. A consumer whose own IdP enum is wider than this set passes its value through as a string and gets that same refusal.

For the path-segment families (oidc, cognito, entra) the tenant is the issuer path segment at tenantPathIndex — the option outranks IDP_TENANT_PATH_INDEX, which must be a decimal non-negative integer or the call throws. Nothing is coerced: 0x10, 1e1 and +1 are refused rather than quietly meaning 16, 10 and 1, and a blank value means "not configured". Keycloak ignores the index entirely; its tenant is the /realms/<realm> suffix.

EntraIssuerTenantResolver adds the one piece of safety the generic path-segment resolver lacks. Entra's multi-tenant meta-authorities — ENTRA_RESERVED_TENANTS: common, organizations, consumers — appear only in an application's configured authority, never in the iss of a concrete token (Microsoft always mints a per-tenant issuer carrying the real directory GUID). One in the issuer position is ambiguous, so it resolves to null and the verifier fails closed. issuerBelongsToTenant re-derives through that refusal, so ('…/common/v2.0', 'common') is false.

Multi-tenancy

A deployment that serves many tenants from one process derives the tenant correctly at the edge and then loses it on the way to the decision. These primitives make the two halves that get lost exist exactly once.

Vocabulary

Two dimensions, both modelled, never interchangeable (TenancyDimension):

| Dimension | What it is | Where it comes from | | ---------------- | --------------------------------------------------------------------------------------------- | ------------------------------- | | SecurityDomain | The IdP-level isolation boundary — a Keycloak realm, an Entra tenant, a Cognito user pool | The verified iss, and nothing else | | Tenant | An application-level tenant/organization identifier, finer-grained than the domain | A configured token claim |

A resource can be inside the caller's security domain and still belong to a different tenant within it, so a fence always names which dimension it compares.

TenantClaimProjector — verified token ⇒ typed principal

The claim name is configuration. This package never spells any platform's claim vocabulary; a deployment declares its own once, and no call site writes a claim literal.

const projector = new TenantClaimProjector({
  tenantClaim: { primaryClaim: 'tenant_id', fallbackClaims: ['legacy_tenant'] },
  issuerTenantResolver: resolver, // the same one the verifier uses
});

const payload = await verifier.verify(token, verifyConfig);

// Required ⇒ TenantedPrincipal { securityDomain, subject, tenant: string }
const principal = projector.project(payload, TenantRequirement.Required);

// Optional ⇒ MaybeTenantedPrincipal { …, tenant: string | null }
const viewer = projector.project(payload, TenantRequirement.Optional);

TenantRequirement is the distinction the type system enforces: only a Required projection produces a TenantedPrincipal, and only a TenantedPrincipal can build a tenant fence or be cross-checked. A route that never proved a tenant cannot fence on one — it will not compile.

Fail-closed throughout, each raising a typed TenantClaimError (map to 401):

  • no resolvable security domain from iss — never a default "home" domain;
  • no sub;
  • Required and the configured claim is absent — never inferred from a header, a path parameter, or a similarly-named claim the config did not declare;
  • Required and the deployment declared no tenant claim at all;
  • a claim that is present but is not a non-empty string — and a malformed primary claim refuses rather than falling through to a fallback, so a garbage claim is never indistinguishable from no claim.

reconcileSuppliedTenant — the claim is the authority

Where a caller also supplies the tenant out of band, the verified claim wins and a disagreement is a refusal. The function is shaped so the only value flowing out of it is the claim's, so wiring it in backwards still yields the safe value:

const tenant = reconcileSuppliedTenant(
  principal,
  request.header('X-Tenant-Id'),   // untrusted
  SuppliedTenantSource.Header,
);                                  // === principal.tenant, or throws

An absent supplied value is not a conflict; the claim simply stands.

TenantFence — one primitive for "or refuse"

Bind the caller's scope once, then apply it wherever that scope must hold — a guard, a service, a repository, a cache read.

const fence = TenantFence.forSecurityDomain(principal); // or .forTenant(principal)

// 1. Scope the QUERY — strictly stronger than checking the row afterwards,
//    because it cannot be skipped on a path that locks, aggregates or deletes.
await prisma.secret.findFirst({ where: { ...fence.predicate('realmId'), id } });

// 2. Fence a lookup result — the row is unusable until it has passed through.
const subscription = fence.require(
  await repo.findById(id),
  (row) => row.realmId,
  { resourceKind: 'subscription' },
);

// 3. Fence a value you already hold.
fence.assert(price.realmId, { resourceKind: 'price' });

Refusals raise a typed TenantAccessError (map to 403, or 404 if the deployment prefers not to confirm existence).

A missing resource tenant is a refusal. A NULL tenant column is how cross-tenant reads stay invisible: every hand-rolled resource.tenant !== caller.tenant comparison passes for it. A deployment where NULL genuinely means "shared by every tenant" opts in per call site, by name:

fence.assert(producer.realmId, {
  resourceKind: 'producer',
  onAbsentResourceTenant: AbsentResourceTenantPolicy.AdmitAsShared,
});

The opt-in covers absence only — a row belonging to another tenant is still refused.

Refusal messages name what was refused and nothing else. They carry the resource kind and the dimension, never an identifier — not the resource's tenant, not the caller's, not the resource id. "Does not exist" and "exists, not yours" produce the same text, so the fence is not an existence oracle; they differ only in the structured code, which is for the server's log.

assertTenantMatch is the same comparison as a standalone function, for code holding a bare scope string rather than a principal.

compositeKey / tenantScopedKey — unambiguous scoped keys

Anything partitioned per caller — a cache entry, a rate-limit bucket, a concurrency slot, an advisory-lock name, an AEAD's associated data — must carry the tenancy, and must compose it unambiguously:

`${'a'}:${'b:c'}`  ===  `${'a:b'}:${'c'}`  ===  'a:b:c'

Two scopes, one key; a read for one returns the other's entry. Every separator that gets used by hand (:, |, /, ., -) occurs inside real identifiers.

tenantScopedKey(principal, 'provider-a');     // domain ␟ tenant ␟ provider-a
compositeKey(realmId ?? null, eventType);     // when there is no principal

The separator is U+001F (KEY_SEGMENT_SEPARATOR), a C0 control character no identifier contains. A segment that is empty, is not a string, or contains the separator is refused — escaping or truncating it would reintroduce the collision. null is the encoding for a deliberately absent segment, and is distinguishable from every real value. The encoding is therefore injective: distinct segment lists always produce distinct keys.

Notes

Wrapping any of this in a NestJS guard, an interceptor, an attestation check, or any other transport is the caller's job — this package holds no framework dependency, so the same primitives are usable from a guard, a middleware, a repository, or a plain function.

The tenancy primitives operate on an already-verified payload. They perform no cryptography; verify first (OidcTokenVerifier.verify), then project.