3.9 KiB
resolver
(no submodules)
Purpose
Implements The tenant resolver factory createJwtTenantResolver. The
resolver reads the tenant identifier from an already-resolved Principal
attached by an earlier api-auth-* chain step and projects it into a
TenantContext. When the configured claim is present the resolver returns
that context (the chain short-circuits); when absent it returns null so a
subsequent header / subdomain resolver can take over.
The resolver is intentionally trivial — it does no JWT verification of its
own (that is api-auth-jwt's job) and no network I/O. Its value is making
the data-flow explicit: the host wires auth → tenant-resolver-jwt → next
and the conformance suite gets to test the empty/missing-tenant edge cases
uniformly without coupling the auth and tenant contracts.
Features
createJwtTenantResolver(opts?)factory returning aTenantResolverconforming to@suaveplan/api-types/tenant-resolver.- Dependency-injected
getPrincipalextractor: defaults to reading(request as { principal }).principal, override for host wiring backed by aWeakMaporAsyncLocalStorage. - Configurable
claimKey: defaults to"tenantId"(readsPrincipal.tenantId); any other key readsPrincipal.claims[key]and brand-casts the string toTenantId. - Strict-mode
requirePrincipal: whentrue, a missing principal or a missing claim throwsTENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_IDrather than returningnull. - Pure read-path: no module state, no async beyond the
Promise<…>shape required by the contract.
Usage: Basic
import { createJwtTenantResolver } from "@suaveplan/api-tenant-resolver-jwt/resolver";
const resolver = createJwtTenantResolver();
const ctx = await resolver.resolve(request);
if (ctx) {
// → { id: "acme" as TenantId, slug: "acme" }
}
Usage: Advanced
import type { Principal } from "@suaveplan/api-types/principal";
import { createJwtTenantResolver } from "@suaveplan/api-tenant-resolver-jwt/resolver";
const principals = new WeakMap<Request, Principal | null>();
const resolver = createJwtTenantResolver({
id: "jwt-primary",
claimKey: "tid",
requirePrincipal: true,
getPrincipal: (req) => principals.get(req) ?? null,
});
// host wiring (api-auth-jwt would do this in production):
principals.set(request, { id: "u" as never, scopes: [], claims: { tid: "acme" } });
const ctx = await resolver.resolve(request);
// → { id: "acme" as TenantId, slug: "acme" } — short-circuits the chain
API Reference
type PrincipalExtractor = (request: Request) => Principal | null | undefined.interface CreateJwtTenantResolverOptions extends JwtTenantResolverOptions { getPrincipal?: PrincipalExtractor }.function createJwtTenantResolver(options?): TenantResolver— returns{ id, resolve(request) }.resolvereturnsPromise<TenantContext | null>; throwsJwtTenantResolverError(PRINCIPAL_MISSING_TENANT_ID)only whenrequirePrincipal: trueand the principal / claim is absent.
Implementation Notes
The resolver brand-casts the claim value to TenantId without further
validation: the principal has already passed an api-auth-* adapter that
owns the verification of the JWT signature and the trust boundary. Re-validating
here would duplicate work the host already performed. Empty-string claim values
are treated as absent so a misconfigured signer can never accidentally route
all traffic into a tenant whose slug is the empty string.
The default PrincipalExtractor reads request.principal, which works for
the simple host wiring (mutate the request once after auth resolves) but
falls down for environments where the Request is frozen (Workers, certain
edge runtimes). In those cases pass an explicit getPrincipal backed by a
WeakMap<Request, Principal> populated by the auth middleware.