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,4 @@
export {
type JwtTenantResolverOptions,
parseJwtTenantResolverOptions,
} from "./options.js";

View File

@@ -0,0 +1,81 @@
# `options`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines and validates the `JwtTenantResolverOptions` accepted by
`createJwtTenantResolver`. The options shape is deliberately narrow — Stage 4
adapters do almost no work — but every field is Zod-validated at construction
time so a typo in `claimKey` or a boolean coerced from a string never reaches
the per-request hot path. Construction-time failures throw
`JwtTenantResolverError(INVALID_OPTIONS)` so the host surfaces a stable error
code rather than a raw `ZodError`.
The parser also fills in defaults (`id = "jwt"`, `claimKey = "tenantId"`,
`requirePrincipal = false`), so the resolver implementation can treat its
options as fully-resolved `Required<…>` and avoid scattering `??` defaults
through the request path.
## Features
- `JwtTenantResolverOptions` — three optional fields covering the entire
configuration surface (`id`, `claimKey`, `requirePrincipal`).
- `parseJwtTenantResolverOptions(input)` — strict Zod parser; rejects
unknown keys, empty strings, and non-boolean values.
- Pure: no I/O, no side effects, no module-level state — safe to call from
module-init code.
## Usage: Basic
```ts
import { parseJwtTenantResolverOptions } from "@suaveplan/api-tenant-resolver-jwt/options";
const resolved = parseJwtTenantResolverOptions(undefined);
// → { id: "jwt", claimKey: "tenantId", requirePrincipal: false }
```
## Usage: Advanced
```ts
import {
parseJwtTenantResolverOptions,
type JwtTenantResolverOptions,
} from "@suaveplan/api-tenant-resolver-jwt/options";
import {
JwtTenantResolverError,
TENANT_RESOLVER_JWT_CODES,
} from "@suaveplan/api-tenant-resolver-jwt/error-codes";
function safeParse(input: JwtTenantResolverOptions): "ok" | "bad" {
try {
parseJwtTenantResolverOptions(input);
return "ok";
} catch (e) {
if (
e instanceof JwtTenantResolverError &&
e.code === TENANT_RESOLVER_JWT_CODES.INVALID_OPTIONS
) {
return "bad";
}
throw e;
}
}
```
## API Reference
- `interface JwtTenantResolverOptions` — `{ id?: string; claimKey?: string; requirePrincipal?: boolean }`.
- `function parseJwtTenantResolverOptions(input)` — `Required<Pick<JwtTenantResolverOptions, "id" | "claimKey" | "requirePrincipal">>`. Throws `JwtTenantResolverError(INVALID_OPTIONS)`.
## Implementation Notes
The Zod schema is `.strict()` — unknown keys reject at parse time. This is
intentional: a downstream caller passing a misspelt field (e.g.
`claim_key` instead of `claimKey`) would otherwise see the field silently
ignored and the resolver fall back to defaults, masking the configuration
bug behind a hard-to-trace "no tenant resolved" failure mode. The parser
runs once at construction so the per-request hot path operates on
fully-resolved `Required<…>` values without re-validation.

View File

@@ -0,0 +1,93 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
JwtTenantResolverError,
TENANT_RESOLVER_JWT_CODES,
} from "../error-codes/error-codes.js";
import { parseJwtTenantResolverOptions } from "./options.js";
describe("parseJwtTenantResolverOptions", () => {
it("fills defaults when input is undefined", { timeout: 5000 }, () => {
expect(parseJwtTenantResolverOptions(undefined)).toEqual({
id: "jwt",
claimKey: "tenantId",
requirePrincipal: false,
});
});
it("fills defaults when input is empty object", { timeout: 5000 }, () => {
expect(parseJwtTenantResolverOptions({})).toEqual({
id: "jwt",
claimKey: "tenantId",
requirePrincipal: false,
});
});
it("honours explicit overrides", { timeout: 5000 }, () => {
expect(
parseJwtTenantResolverOptions({
id: "custom",
claimKey: "tid",
requirePrincipal: true,
})
).toEqual({ id: "custom", claimKey: "tid", requirePrincipal: true });
});
it("rejects empty string id", { timeout: 5000 }, () => {
let thrown: unknown;
try {
parseJwtTenantResolverOptions({ id: "" });
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(JwtTenantResolverError);
expect((thrown as JwtTenantResolverError).code).toBe(
TENANT_RESOLVER_JWT_CODES.INVALID_OPTIONS
);
});
it("rejects empty string claimKey", { timeout: 5000 }, () => {
let thrown: unknown;
try {
parseJwtTenantResolverOptions({ claimKey: "" });
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(JwtTenantResolverError);
});
it("rejects non-boolean requirePrincipal", { timeout: 5000 }, () => {
let thrown: unknown;
try {
parseJwtTenantResolverOptions({
requirePrincipal: "yes" as unknown as boolean,
});
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(JwtTenantResolverError);
});
it("rejects unknown keys under strict mode", { timeout: 5000 }, () => {
let thrown: unknown;
try {
parseJwtTenantResolverOptions({
claim_key: "tid",
} as unknown as Parameters<
typeof parseJwtTenantResolverOptions
>[0]);
} catch (e) {
thrown = e;
}
expect(thrown).toBeInstanceOf(JwtTenantResolverError);
});
it("carries the ZodError as cause", { timeout: 5000 }, () => {
let thrown: unknown;
try {
parseJwtTenantResolverOptions({ id: "" });
} catch (e) {
thrown = e;
}
expect((thrown as JwtTenantResolverError).cause).toBeDefined();
});
});

View File

@@ -0,0 +1,55 @@
import z from "zod";
import {
JwtTenantResolverError,
TENANT_RESOLVER_JWT_CODES,
} from "../error-codes/error-codes.js";
/**
* Public options accepted by `createJwtTenantResolver`.
*
* `claimKey` selects which field on `Principal.claims` (or the projected
* `Principal.tenantId`) the resolver reads. Defaults to `"tenantId"` so the
* common path is zero-configuration. `requirePrincipal` toggles strict-mode:
* when `true`, a missing or principal-less request throws
* `TENANT_RESOLVER_JWT.PRINCIPAL_MISSING_TENANT_ID` rather than returning
* `null` (the chain-advance sentinel). `id` overrides the resolver's
* registered id used in logs / metrics tags.
*/
export interface JwtTenantResolverOptions {
readonly id?: string;
readonly claimKey?: string;
readonly requirePrincipal?: boolean;
}
const optionsSchema = z
.object({
id: z.string().min(1).optional(),
claimKey: z.string().min(1).optional(),
requirePrincipal: z.boolean().optional(),
})
.strict();
/**
* Parse and validate user-supplied options at construction time. Throws
* `JwtTenantResolverError(INVALID_OPTIONS)` on any Zod failure so the host
* surfaces a stable code rather than a raw `ZodError`.
*/
export function parseJwtTenantResolverOptions(
input: JwtTenantResolverOptions | undefined
): Required<
Pick<JwtTenantResolverOptions, "id" | "claimKey" | "requirePrincipal">
> {
const parsed = optionsSchema.safeParse(input ?? {});
if (!parsed.success) {
throw new JwtTenantResolverError(
TENANT_RESOLVER_JWT_CODES.INVALID_OPTIONS,
`Invalid JwtTenantResolverOptions: ${parsed.error.message}`,
{ cause: parsed.error }
);
}
return {
id: parsed.data.id ?? "jwt",
claimKey: parsed.data.claimKey ?? "tenantId",
requirePrincipal: parsed.data.requirePrincipal ?? false,
};
}