first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
# @suaveplan/api-auth-jwt — concrete JWT session resolver for @suaveplan/api-auth. Verifies a JWT presented on the Authorization header (or a configurable cookie) using @suaveplan/jws against either a JWKS endpoint (TTL + negative-cache + in-flight dedup) or a single static JsonWebKey; projects validated claims onto a Principal via an overridable mapper. Returns null on parse/verify failure (chain semantics) and contributes a Bearer JWT OpenAPI security scheme.
Stage-4 concrete `SessionResolver` for the
[`@suaveplan/api`](../api/README.md) plugin system. Verifies a JWT
presented on the `Authorization: Bearer <jwt>` header (or a configured
cookie) against either a remote JWKS endpoint or a single static JWK,
then projects the validated claims onto the foundation's `Principal`
contract via an overridable mapper.
This is the adapter you pair with hosted identity providers (Clerk,
Auth0, Cognito, Stytch, custom Keycloak deployments) or with any
self-hosted JWT-issuing provider. It is **session-resolver only** —
login / register / refresh routes are the responsibility of an `auth`
provider, not this package.
<!-- begin:auto-docs -->
## Documentation tree
- [claims-mapper](./src/claims-mapper/claims-mapper.md) — Default and runtime helpers for converting a verified JWT payload (`Claims`) into a `Principal` from `@suaveplan/api-ty…
- [claims-mapper](./src/claims-mapper/claims-mapper.md) — Default and runtime helpers for converting a verified JWT payload (`Claims`) into a `Principal` from `@suaveplan/api-ty…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the `AUTH_JWT.*` namespace via `defineCodes` from `@…
- [error-codes](./src/error-codes/error-codes.md) — Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the `AUTH_JWT.*` namespace via `defineCodes` from `@…
- [jwks-cache](./src/jwks-cache/jwks-cache.md) — In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS document from a remote endpoint, holds it under a p…
- [jwks-cache](./src/jwks-cache/jwks-cache.md) — In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS document from a remote endpoint, holds it under a p…
- [options](./src/options/options.md) — Defines the Zod-validated boundary for `createJwtSessionResolver` and the companion `validateOptions` helper. The bound…
- [options](./src/options/options.md) — Defines the Zod-validated boundary for `createJwtSessionResolver` and the companion `validateOptions` helper. The bound…
- [resolver](./src/resolver/resolver.md) — Stage-4 concrete `SessionResolver` factory for the API plugin system. Reads a JWT from the request (Authorization h…
- [resolver](./src/resolver/resolver.md) — Stage-4 concrete `SessionResolver` factory for the API plugin system. Reads a JWT from the request (Authorization h…
- [security-scheme](./src/security-scheme/security-scheme.md) — Builds the `SecuritySchemeContribution` this adapter contributes to the generated OpenAPI document. The shape is the ca…
- [security-scheme](./src/security-scheme/security-scheme.md) — Builds the `SecuritySchemeContribution` this adapter contributes to the generated OpenAPI document. The shape is the ca…
<!-- end:auto-docs -->
## Surface
- [`./resolver`](src/resolver/resolver.md) — `createJwtSessionResolver(opts)`.
- [`./jwks-cache`](src/jwks-cache/jwks-cache.md) — `JwksCache` with TTL,
negative cache, in-flight dedup, `kid`-miss refresh.
- [`./claims-mapper`](src/claims-mapper/claims-mapper.md) —
`defaultClaimsToPrincipal` + `runClaimsMapper` helpers.
- [`./security-scheme`](src/security-scheme/security-scheme.md) —
`JWT_BEARER_SCHEME` and `createJwtBearerSecurityScheme(name)` for
OpenAPI emission.
- [`./error-codes`](src/error-codes/error-codes.md) — `AUTH_JWT_*` codes
+ `AuthJwtError` class.
- [`./options`](src/options/options.md) — Zod-validated options
boundary.
## Quick start
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
import { JWT_BEARER_SCHEME } from "@suaveplan/api-auth-jwt/security-scheme";
const resolver = createJwtSessionResolver({
jwksUrl: "https://idp.example/.well-known/jwks.json",
issuer: "https://idp.example",
audience: "https://api.example",
algorithms: ["RS256"],
});
// Use in an api-host AuthAdapter:
const adapter = {
resolver,
securityScheme: JWT_BEARER_SCHEME,
};
```
## Pairing with hosted IdPs
The package is designed to be paired with an external identity provider
that already issues JWTs. The recipe is the same for every provider —
plug in the provider's JWKS URL, issuer, and audience, then add a custom
`claimsToPrincipal` when the provider uses non-standard claim names.
### Clerk
```ts
createJwtSessionResolver({
jwksUrl: "https://your-instance.clerk.accounts.dev/.well-known/jwks.json",
issuer: "https://your-instance.clerk.accounts.dev",
audience: "your-frontend-api",
algorithms: ["RS256"],
claimsToPrincipal: (claims) => {
const sub = claims["sub"];
if (typeof sub !== "string") return null;
return {
id: sub as never,
scopes: [] as never[],
tenantId: typeof claims["org_id"] === "string"
? (claims["org_id"] as never)
: undefined,
claims,
};
},
});
```
### Auth0
```ts
createJwtSessionResolver({
jwksUrl: "https://your-tenant.auth0.com/.well-known/jwks.json",
issuer: "https://your-tenant.auth0.com/",
audience: "https://api.example.com",
algorithms: ["RS256"],
});
```
### Cognito
```ts
createJwtSessionResolver({
jwksUrl: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXX/.well-known/jwks.json",
issuer: "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXX",
audience: "your-cognito-app-client-id",
algorithms: ["RS256"],
});
```
## Semantics
`resolve()` returns `null` on any per-request soft failure (missing
token, malformed token, bad signature, wrong issuer, wrong audience,
expired, not-yet-valid, no matching JWKS key). That collapse lets the
upstream `SessionResolverChain` fall through to the next adapter
(anonymous, static-bearer, etc.). It throws only on infrastructure
failures — `AUTH_JWT.JWKS_FETCH_FAILED` when the JWKS endpoint is
unreachable, `AUTH_JWT.INVALID_KEY_FORMAT` for a malformed static key,
`AUTH_JWT.INVALID_CLAIMS_MAPPER` when the caller's mapper throws or
returns a non-Principal value.
## License
MIT — see the repository.

View File

@@ -0,0 +1,84 @@
# `claims-mapper` — `Claims → Principal` projection
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Default and runtime helpers for converting a verified JWT payload
(`Claims`) into a `Principal` from `@suaveplan/api-types`. The default
projection follows OAuth 2 / OIDC conventions: `sub` for subject,
space-delimited `scope` (or array-form `scopes`) for authorisation
scopes, and `tenant_id` / `tid` for the tenant. Callers can override
the projection entirely to read non-standard claims (e.g. Clerk's
`org_id`) or to enforce additional invariants — return `null` to defer
to the next resolver in the chain.
## Features
- `defaultClaimsToPrincipal` — drop-in OAuth 2 / OIDC projection.
- `runClaimsMapper` — wraps a caller-supplied mapper; catches throws,
rejects non-Principal returns, surfaces both as
`AUTH_JWT.INVALID_CLAIMS_MAPPER`.
- Returns `null` when the token has no `sub` — that is the documented
"not a user identity" path and lets the chain move on.
- Brands `PrincipalId`, `Scope`, and `TenantId` via type assertion at
the trust boundary; downstream consumers see opaque branded strings.
- Passes the full claims record through as `principal.claims` so
downstream tenant resolvers / authorisation rules can read raw values
without a second token parse.
## Usage: Basic
```ts
import { defaultClaimsToPrincipal } from "@suaveplan/api-auth-jwt/claims-mapper";
const principal = defaultClaimsToPrincipal({
sub: "user_123",
scope: "billing:read profile:read",
tenant_id: "acme",
});
// → { id: "user_123", scopes: ["billing:read", "profile:read"], tenantId: "acme", claims: {...} }
```
## Usage: Advanced
Override the projection to read Clerk-style `org_id` and enforce a
non-empty scopes list:
```ts
import type { ClaimsToPrincipal } from "@suaveplan/api-auth-jwt/claims-mapper";
const clerkMapper: ClaimsToPrincipal = (claims) => {
const sub = claims["sub"];
const scope = claims["scope"];
const org = claims["org_id"];
if (typeof sub !== "string" || typeof scope !== "string") return null;
return {
id: sub as never,
scopes: scope.split(" ") as never[],
tenantId: typeof org === "string" ? (org as never) : undefined,
claims,
};
};
```
## API Reference
- `ClaimsToPrincipal` — `(claims) => Principal | null`.
- `defaultClaimsToPrincipal: ClaimsToPrincipal` — the built-in projection.
- `runClaimsMapper(mapper, claims): Principal | null` — invoke a mapper
with structured error reporting.
## Implementation Notes
The default mapper deliberately tolerates either string-form (`"a b c"`)
or array-form (`["a", "b", "c"]`) scopes — RFC 6749 §3.3 uses the
former, while several IdPs (Auth0, Cognito) emit the latter. Empty
strings are filtered out so a stray double-space does not produce a
zero-length scope token. Tenant extraction prefers `tenant_id` (the
Suaveplan-canonical name) over `tid` (the Microsoft / OIDC name) but
accepts both. A mapper that throws is treated as an operator bug, not a
caller error — surfacing it as a `null` would silently mask broken
configuration.

View File

@@ -0,0 +1,76 @@
# `error-codes` — `AUTH_JWT_*` stable codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable error-code registry for `@suaveplan/api-auth-jwt`, exposing the
`AUTH_JWT.*` namespace via `defineCodes` from `@suaveplan/error` and a
matching `AuthJwtError` subclass of `SuaveplanError`. The registry lists
only the codes the package throws — namely the infrastructure failures an
operator must act on. Per-request soft failures (bad token, expired
signature, wrong audience, etc.) deliberately do not get codes; they
collapse to `null` so that the upstream `SessionResolverChain` can move on
to the next resolver, which is the documented chain semantic.
## Features
- `AUTH_JWT_CODES.JWKS_FETCH_FAILED` — JWKS endpoint unreachable or
returned a non-2xx after the configured retries, and there is no cached
set old enough to use. Operators see this in logs / metrics; callers
see a 401.
- `AUTH_JWT_CODES.INVALID_KEY_FORMAT` — `options.staticKey` failed JWK
schema validation at construction time.
- `AUTH_JWT_CODES.INVALID_CLAIMS_MAPPER` — `options.claimsToPrincipal`
threw or returned a value that is not a `Principal` or `null`.
- `AuthJwtError` carries the matching `AuthJwtErrorCode`, integrates with
the `SuaveplanError` cause-chain, redaction, and OTel span-event
adapters from `@suaveplan/error`.
## Usage: Basic
```ts
import { AUTH_JWT_CODES, AuthJwtError } from "@suaveplan/api-auth-jwt/error-codes";
try {
// ... resolver internals ...
} catch (cause) {
throw new AuthJwtError(
AUTH_JWT_CODES.JWKS_FETCH_FAILED,
"JWKS endpoint unreachable",
{ cause }
);
}
```
## Usage: Advanced
Branch on the code at the boundary to map infrastructure failures to
operator alerts rather than user-facing 401s:
```ts
import { AUTH_JWT_CODES, AuthJwtError } from "@suaveplan/api-auth-jwt/error-codes";
function isOperatorActionable(err: unknown): err is AuthJwtError {
if (!(err instanceof AuthJwtError)) return false;
return err.code === AUTH_JWT_CODES.JWKS_FETCH_FAILED;
}
```
## API Reference
- `AUTH_JWT_CODES` — frozen record produced by `defineCodes("AUTH_JWT", [...])`.
- `AuthJwtErrorCode` — union of the three string literals.
- `AuthJwtError extends SuaveplanError<AuthJwtErrorCode>` — `jwtCode`
mirrors `code` for ergonomic `instanceof` narrowing.
## Implementation Notes
The three-code registry is intentionally small. Adding "soft" failure
codes here would tempt consumers to branch on them, defeating the chain
semantic that `null` means "not my caller". If a richer breakdown is
needed downstream, emit it on the failure counter (`reason` attribute)
rather than as a separate `AUTH_JWT.*` code. Registry membership is the
public contract; only add codes through an OpenSpec change.

View File

@@ -0,0 +1,91 @@
# `jwks-cache` — TTL-bounded JWKS resolution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
In-process JWKS cache for `@suaveplan/api-auth-jwt`. Fetches a JWKS
document from a remote endpoint, holds it under a positive TTL,
negative-caches transient failures, dedupes concurrent fetches into a
single network request, and supports forced refresh on a `kid` miss
(the standard "the IdP rotated keys" recovery path). Throws
`AUTH_JWT.JWKS_FETCH_FAILED` only when neither a fresh fetch nor a stale
cached document is available — under partial outages we prefer
stale-but-valid keys to a 401 storm.
## Features
- Positive TTL (default 10 minutes) with monotonic clock injection.
- Negative cache window (default 30 seconds) on fetch failure — within
the window callers see the stale document if any, otherwise the
error.
- In-flight dedup: concurrent `getKeyForKid` callers share a single
network round-trip.
- `forceRefresh()` for operator-triggered rotation.
- `kid` miss against a cached document triggers exactly one refresh.
- JSON shape validation (`{ keys: object[] }`) at the boundary.
## Usage: Basic
```ts
import { JwksCache } from "@suaveplan/api-auth-jwt/jwks-cache";
const cache = new JwksCache({
url: "https://idp.example/.well-known/jwks.json",
ttlMs: 10 * 60 * 1000,
negativeMs: 30 * 1000,
clock: { nowMs: () => Date.now() },
fetchImpl: fetch,
});
const jwk = await cache.getKeyForKid("kid-2024-01");
```
## Usage: Advanced
Inject a fake clock and a controllable fetch impl for tests of the
expiry and dedup behaviours:
```ts
let now = 0;
const calls: number[] = [];
const cache = new JwksCache({
url: "https://idp.example/.well-known/jwks.json",
ttlMs: 1000,
negativeMs: 100,
clock: { nowMs: () => now },
fetchImpl: async () => {
calls.push(now);
return new Response(JSON.stringify({ keys: [{ kid: "a", kty: "RSA", n: "...", e: "AQAB" }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
});
await cache.getKeyForKid("a");
now = 500;
await cache.getKeyForKid("a"); // cache hit, no second fetch
```
## API Reference
- `class JwksCache` — constructor takes `{ url, ttlMs, negativeMs, clock,
fetchImpl }`.
- `getKeyForKid(kid?: string): Promise<Record<string, unknown> | undefined>`
— fetch-as-needed, returns `undefined` when no key matches.
- `forceRefresh(): Promise<JwksDocument>` — clear cache and refetch.
- `JwksDocument` — `{ keys: ReadonlyArray<Record<string, unknown>> }`.
## Implementation Notes
When the token carries no `kid`, the cache only returns a key when the
JWKS contains exactly one entry — ambiguous lookups must be a 401, not
a guess. The negative cache window is short by design (30s default);
longer windows accumulate authentication outages, shorter windows
hammer the IdP. The in-flight dedup uses a single `Promise` field
rather than a queue — the second concurrent caller awaits the same
promise as the first. The "kid miss after a just-fetched document"
guard prevents infinite refresh loops when the IdP legitimately does
not have a matching key.

View File

@@ -0,0 +1,82 @@
# `options` — `JwtSessionResolverOptions` boundary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the Zod-validated boundary for `createJwtSessionResolver` and the
companion `validateOptions` helper. The boundary enforces every static
contract the runtime depends on: exactly one of `jwksUrl` or `staticKey`,
non-empty `algorithms` allowlist, mandatory `issuer` and `audience`, a
positive cache TTL, and a non-negative negative-cache window. Anything
the schema cannot encode (injectable clock, fetch impl, claims mapper
function) is part of the wider `JwtSessionResolverOptions` TypeScript
type but is not Zod-validated — those are dev-time contracts.
## Features
- Strict-mode object schema rejects unknown keys at construction.
- Mutual exclusion check for `jwksUrl` / `staticKey` — both supplied or
neither supplied throws `AUTH_JWT.INVALID_KEY_FORMAT`.
- Sensible defaults: header name `authorization`, JWKS TTL 10 min,
negative cache 30s, clock tolerance 0.
- Accepts `issuer` and `audience` as either a single string or a
non-empty list of strings — JWS verify enforces membership.
## Usage: Basic
```ts
import { validateOptions } from "@suaveplan/api-auth-jwt/options";
const opts = validateOptions({
jwksUrl: "https://idp.example/.well-known/jwks.json",
issuer: "https://idp.example",
audience: "https://api.example",
algorithms: ["RS256"],
});
```
## Usage: Advanced
Custom header, cookie fallback, tighter cache TTL, and a custom claims
mapper:
```ts
import { validateOptions } from "@suaveplan/api-auth-jwt/options";
const opts = validateOptions({
staticKey: { kty: "oct", k: "..." },
issuer: ["https://idp1.example", "https://idp2.example"],
audience: "https://api.example",
algorithms: ["HS256"],
headerName: "x-api-token",
cookieName: "sp_session",
clockToleranceSec: 5,
jwksCacheTtlMs: 60_000,
jwksNegativeCacheMs: 5_000,
});
```
## API Reference
- `JwtSessionResolverOptionsSchema` — Zod object schema.
- `JwtSessionResolverOptions` — public surface (includes non-serialisable
fields like `clock`, `fetchImpl`, `claimsToPrincipal`).
- `ValidatedJwtSessionResolverOptions` — inferred shape after defaults
are applied.
- `validateOptions(raw)` — synchronous validator; throws `AuthJwtError`
with code `AUTH_JWT.INVALID_KEY_FORMAT` on any boundary failure.
## Implementation Notes
The schema is intentionally `.strict()` so a typo in a key name fails
loudly at construction rather than silently activating defaults. `iss`
and `aud` are both required because token-level `iss` / `aud` confusion
is a documented attack vector — every Stage-4 deployment is expected to
pin both. `algorithms` is also mandatory and non-empty — `@suaveplan/jws`
already refuses to verify without an allowlist, but we re-enforce it at
the API boundary so the error surface is consistent. The mutual
exclusion check after parsing matches what the spec calls out: a single
key source is the only way to reason about JWKS cache invalidation.

View File

@@ -0,0 +1,108 @@
# `resolver` — `createJwtSessionResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stage-4 concrete `SessionResolver` factory for the API plugin
foundation. Reads a JWT from the request (Authorization header or
configurable cookie), verifies it via `@suaveplan/jws` against either a
JWKS endpoint (cached and refreshed on `kid` miss) or a single static
JWK, validates `iss`, `aud`, `exp`, `nbf` against the configured policy,
and projects the validated claims onto a `Principal` via an overridable
mapper. The factory is the only public entry point — internals
(`JwksCache`, claims mapper, error registry) are surface-exported for
testability but most consumers use `createJwtSessionResolver(opts)` alone.
## Features
- `Authorization: Bearer <jwt>` header extraction with case-insensitive
scheme match; configurable header name accepts a raw token form for
non-standard headers (e.g. `x-api-token: <jwt>`).
- Cookie fallback when `cookieName` is supplied; header still wins when
both are present.
- JWKS resolution via `JwksCache` with positive TTL, negative cache,
in-flight dedup, and `kid`-miss refresh.
- Static-key path for HS-family symmetric tokens or single-key
deployments.
- Mandatory `iss` and `aud` enforcement (single string or list); `exp`
and `nbf` enforced when present with configurable `clockToleranceSec`.
- Per-request soft failures collapse to `null` so the upstream
`SessionResolverChain` can fall through to the next adapter; operator
failures (JWKS unreachable, malformed mapper, malformed static key)
surface as `AuthJwtError` with the documented codes.
- Idempotent `dispose()` flips an internal flag so subsequent
`resolve()` calls return `null` rather than emitting late traffic on
a torn-down resolver.
## Usage: Basic
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
const resolver = createJwtSessionResolver({
jwksUrl: "https://idp.example/.well-known/jwks.json",
issuer: "https://idp.example",
audience: "https://api.example",
algorithms: ["RS256"],
});
const req = new Request("https://api.example/users", {
headers: { authorization: "Bearer eyJ..." },
});
const principal = await resolver.resolve(req);
```
## Usage: Advanced
Static HS256 key, cookie fallback, custom claims mapper, and an
injectable clock for deterministic temporal-claim tests:
```ts
import { createJwtSessionResolver } from "@suaveplan/api-auth-jwt/resolver";
const resolver = createJwtSessionResolver({
staticKey: { kty: "oct", k: "<base64url-secret>" },
issuer: ["https://idp1.example", "https://idp2.example"],
audience: "https://api.example",
algorithms: ["HS256"],
cookieName: "sp_session",
clockToleranceSec: 5,
clock: { nowMs: () => 1_700_000_000_000 },
claimsToPrincipal: (claims) => {
const sub = claims["sub"];
if (typeof sub !== "string") return null;
return {
id: sub as never,
scopes: [] as never[],
tenantId: typeof claims["org_id"] === "string"
? (claims["org_id"] as never)
: undefined,
claims,
};
},
});
```
## API Reference
- `createJwtSessionResolver(opts: JwtSessionResolverOptions): SessionResolver`
— returns the `@suaveplan/api-auth` `SessionResolver` shape (`resolve` +
idempotent `dispose`).
## Implementation Notes
The resolver deliberately separates "this is not my caller" (chain
falls through, returns `null`) from "the host is mis-configured"
(throws `AuthJwtError`). Verification failures inside `@suaveplan/jws`
return `null` so a stray Bearer header from an unrelated IdP cannot
401-storm the API — the chain's next adapter (anonymous, static key,
etc.) still gets its turn. The temporal-claim check uses a single
clock reading per request to avoid TOCTOU between the `exp` and `nbf`
arms. We re-implement `iss`/`aud`/`exp`/`nbf` enforcement here rather
than push it into `@suaveplan/jws` because those are JWT-claim
concerns, not JWS-protected-header concerns — the JWS package
deliberately handles only RFC 7515 (signature) and leaves RFC 7519
(payload claims) to its consumers.

View File

@@ -0,0 +1,66 @@
# `security-scheme` — OpenAPI Bearer JWT contribution
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Builds the `SecuritySchemeContribution` this adapter contributes to the
generated OpenAPI document. The shape is the canonical OAS 3.1
`http` / `bearer` scheme with `bearerFormat: "JWT"`. Stage-8
`@suaveplan/api-openapi` consumes the contribution and inlines it under
`components.securitySchemes[name]`, then references it from operation
`security` blocks for non-public routes.
## Features
- Canonical Bearer-JWT scheme — no surprise extensions, no proprietary
fields.
- Configurable contribution name (`bearerAuth` default) so multiple JWT
resolvers can coexist (e.g. user-side `bearerAuth` + service-side
`serviceBearerAuth`).
- `JWT_BEARER_SCHEME` constant for callers that only ever expose one
JWT resolver.
- Human-readable `description` field that links the scheme back to the
adapter so docs readers understand where verification happens.
## Usage: Basic
```ts
import { JWT_BEARER_SCHEME } from "@suaveplan/api-auth-jwt/security-scheme";
const adapter = {
resolver: createJwtSessionResolver(...).resolver,
securityScheme: JWT_BEARER_SCHEME,
};
```
## Usage: Advanced
Rename the contribution for a dual-resolver host:
```ts
import { createJwtBearerSecurityScheme } from "@suaveplan/api-auth-jwt/security-scheme";
const userScheme = createJwtBearerSecurityScheme("userAuth");
const serviceScheme = createJwtBearerSecurityScheme("serviceAuth");
```
## API Reference
- `createJwtBearerSecurityScheme(name = "bearerAuth")` →
`SecuritySchemeContribution` from `@suaveplan/api-auth`.
- `JWT_BEARER_SCHEME` — pre-built `bearerAuth`-named contribution.
## Implementation Notes
The `http` / `bearer` form is the only OAS 3.1 scheme that documents
"this endpoint accepts a Bearer JWT" without forcing the OpenAPI
emitter to inline OAuth-flow metadata that this adapter does not own.
The `bearerFormat` field is documentation-only per the spec, but
populating it as `"JWT"` is the convention every OAS reader (Swagger
UI, Redoc, Stoplight) recognises. Tenant or scope expectations are
expressed by individual route handlers, not the scheme itself —
keeping the scheme orthogonal to the route catalogue keeps it stable
across plugin upgrades.