Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
# `resolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 a
`TenantResolver` conforming to `@suaveplan/api-types/tenant-resolver`.
- Dependency-injected `getPrincipal` extractor: defaults to reading
`(request as { principal }).principal`, override for host wiring backed
by a `WeakMap` or `AsyncLocalStorage`.
- Configurable `claimKey`: defaults to `"tenantId"` (reads
`Principal.tenantId`); any other key reads `Principal.claims[key]` and
brand-casts the string to `TenantId`.
- Strict-mode `requirePrincipal`: when `true`, a missing principal or a
missing claim throws `TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID`
rather than returning `null`.
- Pure read-path: no module state, no async beyond the `Promise<…>` shape
required by the contract.
## Usage: Basic
```ts
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
```ts
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) }`. `resolve` returns
`Promise<TenantContext | null>`; throws
`JwtTenantResolverError(PRINCIPAL_MISSING_TENANT_ID)` only when
`requirePrincipal: true` and 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.