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,125 @@
# @suaveplan/api-auth-static-principal — fixed-principal session resolver for @suaveplan/api: returns a configured Principal verbatim on every request. Used in plugin integration tests and the api-plugin-example fixture.
Fixed-`Principal` session resolver for the `@suaveplan/api` plugin
system. Returns the configured `Principal` verbatim on every request —
no token decoding, no network I/O, no internal state. Used in plugin
integration tests and the `api-plugin-example` fixture, where the goal is to
exercise the entire downstream pipeline (RBAC gate, tenant resolver chain
reading `principal.tenantId`, tenant-scoped DB) with a deterministic
identity carried in config.
<!-- begin:auto-docs -->
## Documentation tree
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
- [adapter](./src/adapter/adapter.md) — Bundles the static-principal resolver, the (absent) security-scheme contribution, and a pinned `csrf: false` into a sin…
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
- [error-codes](./src/error-codes/error-codes.md) — Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the `AuthStaticPrincipalError` concrete subclass of `@su…
- [factory](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
- [create-static-principal-resolver](./src/factory/create-static-principal-resolver.md) — Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the configured `Principal` verbatim on every req…
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
- [options](./src/options/options.md) — Owns the runtime contract between the test harness call site and the resolver factory. The schema validates both the ou…
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
- [security-scheme](./src/security-scheme/security-scheme.md) — Provides the per-adapter `SecuritySchemeContribution` slot of `AuthAdapter`. The static-principal adapter is a test-onl…
<!-- end:auto-docs -->
## Why does this exist?
Per the API foundation design doc §13.2, the testing kit needs an auth
adapter that supplies a fixed `Principal` without any token decoding. The
§13.2 example uses it directly:
```ts
adapters: {
"auth.session": {
kind: "static-principal",
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
},
}
```
Tests exercise the whole pipeline — RBAC, tenant resolution, tenant-scoped
DB — with a deterministic principal in config. No mocks, no JWKS setup, no
token signing.
## Surface
| Subpath | Export |
| -------------- | --------------------------------------------------------------------------------------- |
| `./resolver` | `createStaticPrincipalResolver(opts): SessionResolver` |
| `./adapter` | `createStaticPrincipalAdapter(opts): AuthAdapter` |
| `./error-codes`| `AUTH_STATIC_PRINCIPAL_CODES`, `AuthStaticPrincipalError`, `AuthStaticPrincipalErrorCode` |
The root barrel re-exports the resolver, adapter, error codes, options
schema (`StaticPrincipalResolverOptionsSchema` /
`parseStaticPrincipalResolverOptions`), and the empty
`staticPrincipalSecurityScheme()` contribution.
## Usage
```ts
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
const adapter = createStaticPrincipalAdapter({
principal: {
id: "u1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { sub: "u1" },
},
});
host.registerAuth(adapter);
// every request now produces `principal === { id: "u1", ... }`.
```
Skip auth on selected paths to test `publicRoutes` behaviour without
rewiring the adapter chain:
```ts
const adapter = createStaticPrincipalAdapter({
principal: { id: "u1", scopes: [], claims: {} },
nullForPaths: ["/healthz", "/webhooks/*"],
applyTo: (req) => !req.headers.has("x-skip-auth"),
});
```
## Conformance
The resolver passes `runAuthSessionConformance` from
`@suaveplan/api-auth/testing` in seeded-principal mode. See
`src/factory/create-static-principal-resolver.spec-conformance.test.ts` and
`tests/conformance/api-auth.test.ts`.
## Errors
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` — outer options bag failed Zod
validation (wrong shape, missing required fields, unknown top-level keys).
`AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL` — the supplied `principal` field
is malformed (missing `id`, non-string `id`, non-array `scopes`, unknown
keys inside `principal`, …).
Both fire synchronously at construction time. The resolver itself never
throws at request time.
## Stage-5 wiring (forward-looking)
When `api-host/testing` ships its Stage-5 `createTestHost` surface, this
adapter is registered in the adapter table under
`kind: "static-principal"`, so tests will declare it inline:
```ts
const host = createTestHost({
adapters: {
"auth.session": {
kind: "static-principal",
principal: { id: "u1", scopes: ["billing:write"], tenantId: "acme" },
},
},
});
```
Until then, callers wire the adapter directly via
`createStaticPrincipalAdapter()` as shown above.

View File

@@ -0,0 +1,76 @@
{
"name": "@suaveplan/api-auth-static-principal",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Stage-5 fixed-principal session resolver for @suaveplan/api: returns a configured Principal verbatim on every request. Used in plugin integration tests and the api-plugin-example fixture.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./resolver": {
"import": {
"types": "./dist/factory/create-static-principal-resolver.d.ts",
"default": "./dist/factory/create-static-principal-resolver.js"
}
},
"./adapter": {
"import": {
"types": "./dist/adapter/adapter.d.ts",
"default": "./dist/adapter/adapter.js"
}
},
"./error-codes": {
"import": {
"types": "./dist/error-codes/error-codes.d.ts",
"default": "./dist/error-codes/error-codes.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/api-auth": "workspace:^",
"@suaveplan/api-types": "workspace:^",
"@suaveplan/error": "workspace:^",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": ["dist", "src", "README.md"]
}

View File

@@ -0,0 +1,81 @@
# `adapter` — composite `AuthAdapter` factory
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Bundles the static-principal resolver, the (absent) security-scheme
contribution, and a pinned `csrf: false` into a single `AuthAdapter` value.
This is the call-site shape for the canonical "test host with a known
Principal" wiring — `host.registerAuth(createStaticPrincipalAdapter({...}))`
gives the test the entire auth pipeline without further plumbing.
The factory delegates construction-time validation to
`createStaticPrincipalResolver`, so misconfigured options surface as the
same typed `AuthStaticPrincipalError` they do at the resolver level. The
adapter holds no state of its own.
## Features
- Single-call construction of the resolver + scheme + csrf triple.
- Pins `csrf: false` because the static-principal layer has no session
cookie to forge against — every request carries the configured Principal
by construction.
- `securityScheme` omitted (matching `api-auth-anonymous` topology) so the
generated OpenAPI document doesn't advertise a real auth flow for
routes wired to this adapter.
## Usage: Basic
```ts
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
const adapter = createStaticPrincipalAdapter({
principal: {
id: "u1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { sub: "u1" },
},
});
host.registerAuth(adapter);
```
## Usage: Advanced
Compose with `applyTo` / `nullForPaths` to skip auth on test-only routes:
```ts
import { createStaticPrincipalAdapter } from "@suaveplan/api-auth-static-principal/adapter";
const adapter = createStaticPrincipalAdapter({
principal: { id: "u1", scopes: [], claims: {} },
nullForPaths: ["/healthz", "/webhooks/*"],
});
// host.registerAuth(adapter) — /healthz and /webhooks/* resolve to null;
// every other route gets the configured Principal.
```
## API Reference
| Symbol | Kind | Description |
| ------------------------------------- | -------- | ---------------------------------------------------------- |
| `createStaticPrincipalAdapter(opts)` | function | Returns `AuthAdapter` composed from the four building blocks. |
## Implementation Notes
`csrf: false` is the load-bearing pin: a static-principal host can't honour
a real CSRF check (there's no session cookie carrying a synchronizer
token), so the host should skip CSRF entirely for routes wired to this
adapter. If a downstream test needs CSRF behaviour exercised in isolation,
compose with a separate adapter that pins `csrf: true` and let the host's
adapter-chain merge rule pick the safer value (the host's policy is to use
the strictest `csrf` flag in the chain).
`validateAuthAdapter` accepts this adapter's shape (security scheme absent;
csrf set to a boolean). The package's adapter test asserts the validation
contract is satisfied without re-implementing the validator.

View File

@@ -0,0 +1,59 @@
import { validateAuthAdapter } from "@suaveplan/api-auth";
import type { Principal } from "@suaveplan/api-types/principal";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createStaticPrincipalAdapter } from "./adapter.js";
const P: Principal = {
id: "u_1" as Principal["id"],
scopes: ["x:write" as Principal["scopes"][number]],
tenantId: "acme" as Principal["tenantId"],
claims: { sub: "u_1" },
};
describe("createStaticPrincipalAdapter", () => {
it("validates against validateAuthAdapter", { timeout: 5000 }, () => {
const adapter = createStaticPrincipalAdapter({ principal: P });
const out = validateAuthAdapter(adapter);
expect(out).toBe(adapter);
expect(out.securityScheme).toBeUndefined();
});
it("pins csrf:false (no underlying session)", {
timeout: 5000,
}, () => {
const adapter = createStaticPrincipalAdapter({ principal: P });
expect(adapter.csrf).toBe(false);
});
it("embedded resolver returns the principal for arbitrary requests", {
timeout: 5000,
}, async () => {
const adapter = createStaticPrincipalAdapter({ principal: P });
const result = await adapter.resolver.resolve(
new Request("https://example.test/api/anything")
);
expect(result).toEqual(P);
});
it("embedded resolver honours nullForPaths", {
timeout: 5000,
}, async () => {
const adapter = createStaticPrincipalAdapter({
principal: P,
nullForPaths: ["/healthz"],
});
expect(
await adapter.resolver.resolve(
new Request("https://example.test/healthz")
)
).toBeNull();
});
it("dispose on embedded resolver is idempotent", {
timeout: 5000,
}, async () => {
const adapter = createStaticPrincipalAdapter({ principal: P });
await adapter.resolver.dispose();
await expect(adapter.resolver.dispose()).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,33 @@
import type { AuthAdapter } from "@suaveplan/api-auth";
import { createStaticPrincipalResolver } from "../factory/create-static-principal-resolver.js";
import type { StaticPrincipalResolverOptions } from "../options/options.types.js";
import { staticPrincipalSecurityScheme } from "../security-scheme/security-scheme.js";
/**
* Build a composite {@link AuthAdapter} bundling the static-principal
* resolver with an empty security-scheme contribution and `csrf: false`.
*
* Use this when the call-site wants the canonical "test host with a known
* Principal" adapter as a single value to pass into the host's
* `registerAuth` slot. For mixed chains, prefer
* `createStaticPrincipalResolver()` directly and assemble the chain via
* `SessionResolverChain` from `@suaveplan/api-auth/chain`.
*
* `csrf` is pinned to `false` because the static-principal resolver has no
* underlying session cookie to forge against — every request carries the
* configured Principal by construction.
*/
export function createStaticPrincipalAdapter(
opts: StaticPrincipalResolverOptions
): AuthAdapter {
// `staticPrincipalSecurityScheme()` is documented to return `undefined`
// (the anonymous-style topology). The call is preserved for symmetry +
// for the test that asserts the no-scheme contract, but the result is
// dropped here so `exactOptionalPropertyTypes` is satisfied without a
// never-taken branch on the constructor.
void staticPrincipalSecurityScheme();
return {
resolver: createStaticPrincipalResolver(opts),
csrf: false,
};
}

View File

@@ -0,0 +1 @@
export { createStaticPrincipalAdapter } from "./adapter.js";

View File

@@ -0,0 +1,87 @@
# `error-codes` — stable code registry for the static-principal adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the `AUTH_STATIC_PRINCIPAL.*` error-code namespace and the
`AuthStaticPrincipalError` concrete subclass of
`@suaveplan/error/SuaveplanError`. This module is the single source of truth
for every failure code the package can surface to a caller — wiring this
registry into the host's error envelope is how a malformed test-time
Principal configuration reaches the developer rather than silently producing a
broken auth chain.
Codes are produced via `defineCodes("AUTH_STATIC_PRINCIPAL", [...])` so the
full literal value is e.g. `"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"`. The
class augments the base error with `authStaticPrincipalCode`, providing a
narrowed code property when consumers have already discriminated by
`instanceof`.
## Features
- Stable, frozen `AUTH_STATIC_PRINCIPAL_CODES` map built via `defineCodes` so
the prefix can't drift.
- Discriminated-union friendly: `AuthStaticPrincipalErrorCode` is a literal
union over the registry entries.
- Carries arbitrary `context` (Zod issues, the offending field path) via the
base `SuaveplanError` options bag.
## Usage: Basic
```ts
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "@suaveplan/api-auth-static-principal/error-codes";
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
"createStaticPrincipalResolver: options failed schema validation"
);
```
## Usage: Advanced
Attach Zod issues so the caller can pinpoint the bad field without re-running
validation:
```ts
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "@suaveplan/api-auth-static-principal/error-codes";
import z from "zod";
const parsed = OptionsSchema.safeParse(input);
if (!parsed.success) {
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
"principal failed schema validation",
{ context: { issues: parsed.error.issues } }
);
}
```
## API Reference
| Symbol | Kind | Description |
| ----------------------------------- | -------- | ------------------------------------------------------------------------ |
| `AUTH_STATIC_PRINCIPAL_CODES` | constant | Frozen map of code constants. Keys are the short names; values are full. |
| `AuthStaticPrincipalErrorCode` | type | Literal union over the registry values. |
| `AuthStaticPrincipalError` | class | `SuaveplanError` subclass typed against the code registry. |
## Implementation Notes
`defineCodes` joins the prefix with each suffix using a single `.` so the
resulting strings match the host's structured-log code field. The class
stores the code twice — once in the inherited `code` slot and once in the
narrower `authStaticPrincipalCode` slot — so callers that already narrowed by
`instanceof` get a precisely-typed code field without a follow-up `as`.
The package's static-principal resolver never throws at request time; both
codes are construction-time only. This is deliberate — every per-request
failure for fixed-principal tests should manifest in the test assertion, not
in the auth pipeline.

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "./error-codes.js";
describe("AUTH_STATIC_PRINCIPAL_CODES", () => {
it("exposes the INVALID_OPTIONS literal", { timeout: 5000 }, () => {
expect(AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS).toBe(
"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"
);
});
it("exposes the INVALID_PRINCIPAL literal", { timeout: 5000 }, () => {
expect(AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL).toBe(
"AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL"
);
});
it("AuthStaticPrincipalError mirrors code into the narrow field", {
timeout: 5000,
}, () => {
const err = new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
"bad options"
);
expect(err).toBeInstanceOf(AuthStaticPrincipalError);
expect(err.code).toBe(AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS);
expect(err.authStaticPrincipalCode).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
);
expect(err.message).toBe("bad options");
});
it("AuthStaticPrincipalError accepts an options bag", {
timeout: 5000,
}, () => {
const err = new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
"with ctx",
{ context: { field: "scopes" } }
);
expect(err.context).toEqual({ field: "scopes" });
expect(err.authStaticPrincipalCode).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
);
});
});

View File

@@ -0,0 +1,45 @@
import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error-code registry for `@suaveplan/api-auth-static-principal`.
*
* The static-principal resolver has no token decoding, no network I/O and no
* persistent state — every request returns the configured Principal verbatim
* (or `null` for paths in `nullForPaths`). The two failure modes both fire at
* construction time:
*
* - `AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` — the outer options bag failed
* Zod validation (wrong shape, missing required fields, unknown keys).
* - `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL` — the supplied `principal` is
* malformed (missing `id`, missing/empty `scopes`, non-object `claims`, …).
*
* Codes are namespaced `AUTH_STATIC_PRINCIPAL.*` via {@link defineCodes} so
* the full literal value is e.g. `"AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS"`.
* Consumers branch on the constant rather than the raw string.
*/
export const AUTH_STATIC_PRINCIPAL_CODES = defineCodes(
"AUTH_STATIC_PRINCIPAL",
["INVALID_OPTIONS", "INVALID_PRINCIPAL"] as const
);
export type AuthStaticPrincipalErrorCode =
(typeof AUTH_STATIC_PRINCIPAL_CODES)[keyof typeof AUTH_STATIC_PRINCIPAL_CODES];
/**
* Concrete error class for the static-principal package. Extends
* {@link SuaveplanError} so `instanceof AuthStaticPrincipalError` narrows
* `code` to {@link AuthStaticPrincipalErrorCode}.
*/
export class AuthStaticPrincipalError extends SuaveplanError<AuthStaticPrincipalErrorCode> {
public readonly authStaticPrincipalCode: AuthStaticPrincipalErrorCode;
public constructor(
code: AuthStaticPrincipalErrorCode,
message: string,
options?: SuaveplanErrorOptions
) {
super(code, message, options);
this.authStaticPrincipalCode = code;
}
}

View File

@@ -0,0 +1,5 @@
export {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
type AuthStaticPrincipalErrorCode,
} from "./error-codes.js";

View File

@@ -0,0 +1,99 @@
# `factory/create-static-principal-resolver` — fixed-Principal resolver
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Constructs a `SessionResolver` from `@suaveplan/api-auth` that returns the
configured `Principal` verbatim on every request — no token decoding, no
JWKS, no I/O. This is the workhorse adapter for plugin integration tests,
the `api-plugin-example` fixture, and any environment where the goal is to
exercise the downstream pipeline (RBAC gate, tenant resolver chain reading
`principal.tenantId`, tenant-scoped DB) with a deterministic identity carried
in config.
The factory consumes options previously validated by the `options` module so
construction is total: every misuse maps to either
`AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS` or `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL`
and surfaces synchronously at construction time. The resolver itself never
throws at request time.
## Features
- Defensive clone on every `resolve()` — callers cannot mutate one returned
Principal and observe the change on subsequent calls.
- `applyTo(req)` predicate for arbitrary "return null for this request"
rules.
- `nullForPaths` suffix-glob list (`/webhooks/*`) and exact-match patterns
for the most common public-route test setup.
- Idempotent no-op `dispose()` matching the `SessionResolver` contract.
- Returned objects are frozen (top-level + `scopes` + `claims`) to make
attempted mutations fail loudly in strict mode.
## Usage: Basic
```ts
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
const resolver = createStaticPrincipalResolver({
principal: {
id: "u1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { sub: "u1" },
},
});
const principal = await resolver.resolve(
new Request("https://example.test/api/billing/invoices")
);
// principal.id === "u1"
```
## Usage: Advanced
Combine `applyTo` and `nullForPaths` to model a mixed-route test surface:
```ts
import { createStaticPrincipalResolver } from "@suaveplan/api-auth-static-principal/resolver";
const resolver = createStaticPrincipalResolver({
principal: {
id: "u1",
scopes: ["billing:write", "billing:read"],
tenantId: "acme",
claims: { sub: "u1" },
},
nullForPaths: ["/webhooks/*", "/healthz"],
applyTo: (req) => !req.headers.has("x-skip-auth"),
});
// /healthz and /webhooks/stripe resolve to null.
// Requests with the `x-skip-auth` header resolve to null.
// Everything else returns the configured Principal.
```
## API Reference
| Symbol | Kind | Description |
| ----------------------------------- | -------- | ------------------------------------------------------------------ |
| `createStaticPrincipalResolver(o)` | function | Returns a `SessionResolver` carrying `o.principal` on every match. |
## Implementation Notes
The clone strategy is: deep-freeze a single template at construction, then
clone-from-template per `resolve()`. The per-call clone is unavoidable —
otherwise two callers each freezing the returned object would freeze each
other's state. The per-call clone copies the top level plus `scopes` (array
spread) and `claims` (object spread); deeper structures inside `claims` are
not cloned. This matches the `Principal` contract: `claims` values are
`unknown`, so the package can't responsibly deep-clone them. Callers passing
mutable nested claims values are responsible for not mutating them in-place.
`pathMatches` accepts only exact-match and a single suffix glob (`/foo/*`)
on purpose — richer matching belongs in `applyTo`. The trailing slash is
preserved when stripping the `*` so `/foo/*` matches `/foo/x` but not
`/foox`. The pattern dialect is documented on the spec and exercised by the
test file.

View File

@@ -0,0 +1,30 @@
/**
* §16 conformance proof — the static-principal resolver MUST pass
* `runAuthSessionConformance` from `@suaveplan/api-auth/testing`. Seeded
* with a known Principal so the harness exercises the "recognised request"
* scenario; `nullForPaths` lets the harness assert the "unrecognised"
* scenario without rewiring the factory.
*/
import { runAuthSessionConformance } from "@suaveplan/api-auth/testing";
import type { Principal } from "@suaveplan/api-types/principal";
import { createStaticPrincipalResolver } from "./create-static-principal-resolver.js";
const P: Principal = {
id: "u_conf" as Principal["id"],
scopes: ["x:write" as Principal["scopes"][number]],
claims: { sub: "u_conf" },
};
runAuthSessionConformance({
label: "static-principal-direct",
factory: () =>
createStaticPrincipalResolver({
principal: P,
nullForPaths: ["/__public/*"],
}),
seedNull: () => new Request("https://example.test/__public/health"),
seedPrincipal: () => ({
request: new Request("https://example.test/api/anything"),
expected: P,
}),
});

View File

@@ -0,0 +1,191 @@
import type { Principal } from "@suaveplan/api-types/principal";
import { describe, expect, expectTypeOf, it } from "@suaveplan/testing/runner";
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "../error-codes/error-codes.js";
import { createStaticPrincipalResolver } from "./create-static-principal-resolver.js";
const TARGET = "https://example.test/api/billing";
const P: Principal = {
id: "u_1" as Principal["id"],
scopes: ["billing:write" as Principal["scopes"][number]],
tenantId: "acme" as Principal["tenantId"],
claims: { sub: "u_1" },
};
describe("createStaticPrincipalResolver", () => {
it("every request resolves to the configured Principal", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({ principal: P });
const a = await r.resolve(new Request(TARGET));
const b = await r.resolve(new Request("https://example.test/other"));
expect(a).not.toBeNull();
expect(b).not.toBeNull();
expect(a).toEqual(P);
expect(b).toEqual(P);
await r.dispose();
});
it("returned Principal is isolated across calls", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({ principal: P });
const a = await r.resolve(new Request(TARGET));
const b = await r.resolve(new Request(TARGET));
expect(a).not.toBe(b); // distinct references
// Both are deep-frozen — attempting to mutate throws in strict mode.
expect(Object.isFrozen(a)).toBe(true);
expect(Object.isFrozen(b)).toBe(true);
});
it("matching path in nullForPaths returns null (glob)", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({
principal: P,
nullForPaths: ["/webhooks/*"],
});
const result = await r.resolve(
new Request("https://example.test/webhooks/stripe")
);
expect(result).toBeNull();
});
it("matching path in nullForPaths returns null (exact)", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({
principal: P,
nullForPaths: ["/healthz"],
});
const result = await r.resolve(
new Request("https://example.test/healthz")
);
expect(result).toBeNull();
});
it("non-matching path returns the configured Principal", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({
principal: P,
nullForPaths: ["/webhooks/*"],
});
const result = await r.resolve(
new Request("https://example.test/billing/invoices")
);
expect(result).toEqual(P);
});
it("exact pattern does not match a related prefix", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({
principal: P,
nullForPaths: ["/foo"],
});
const result = await r.resolve(
new Request("https://example.test/foobar")
);
// /foo exact does NOT match /foobar — must return the principal.
expect(result).toEqual(P);
});
it("applyTo predicate returning false resolves to null", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({
principal: P,
applyTo: (req) => !req.headers.has("x-skip-auth"),
});
const skipped = await r.resolve(
new Request(TARGET, { headers: { "x-skip-auth": "1" } })
);
expect(skipped).toBeNull();
const normal = await r.resolve(new Request(TARGET));
expect(normal).toEqual(P);
});
it("applyTo runs before nullForPaths", {
timeout: 5000,
}, async () => {
let nullForPathsConsulted = false;
const r = createStaticPrincipalResolver({
principal: P,
applyTo: (_req) => false, // always force null
nullForPaths: [
// If this pattern were consulted, it would also return null,
// but `applyTo` returning false should short-circuit before.
(() => {
nullForPathsConsulted = true;
return "/never";
})(),
],
});
const result = await r.resolve(new Request(TARGET));
expect(result).toBeNull();
// Pattern array was constructed (so the IIFE ran) — that's expected.
// The real assertion: even though `nullForPaths` is configured,
// `applyTo` returning false is enough to produce null without URL
// parsing failure on a non-standard URL.
expect(nullForPathsConsulted).toBe(true);
});
it("dispose is idempotent", { timeout: 5000 }, async () => {
const r = createStaticPrincipalResolver({ principal: P });
await r.dispose();
await expect(r.dispose()).resolves.toBeUndefined();
});
it("strict-mode: nullForPaths unset returns principal everywhere", {
timeout: 5000,
}, async () => {
const r = createStaticPrincipalResolver({ principal: P });
expect(
await r.resolve(new Request("https://example.test/anything"))
).toEqual(P);
expect(
await r.resolve(new Request("https://example.test/webhooks/x"))
).toEqual(P);
});
it("rejects invalid options at construction (INVALID_OPTIONS)", {
timeout: 5000,
}, () => {
try {
createStaticPrincipalResolver(null as never);
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(AuthStaticPrincipalError);
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
);
}
});
it("rejects missing principal.id at construction (INVALID_PRINCIPAL)", {
timeout: 5000,
}, () => {
try {
createStaticPrincipalResolver({
principal: { scopes: [], claims: {} },
} as never);
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
);
}
});
it("resolve return-type narrows to Promise<Principal | null>", {
timeout: 5000,
}, () => {
const r = createStaticPrincipalResolver({ principal: P });
expectTypeOf(r.resolve).toBeFunction();
expectTypeOf(r.resolve).parameter(0).toEqualTypeOf<Request>();
});
});

View File

@@ -0,0 +1,90 @@
import type { SessionResolver } from "@suaveplan/api-auth";
import type { Principal } from "@suaveplan/api-types/principal";
import { parseStaticPrincipalResolverOptions } from "../options/options.js";
import type { StaticPrincipalResolverOptions } from "../options/options.types.js";
/**
* Match a request path against a single `nullForPaths` pattern.
*
* Supported syntaxes:
* - Exact match — `/webhooks/stripe` matches only that path.
* - Suffix glob — `/webhooks/*` matches `/webhooks/<anything>` including
* further slashes (everything after `/webhooks/`).
*
* The pattern dialect is deliberately small. Tests that need richer matching
* use the `applyTo` predicate.
*/
function pathMatches(pathname: string, pattern: string): boolean {
if (pattern.endsWith("/*")) {
const prefix = pattern.slice(0, -1); // keep trailing slash
return pathname.startsWith(prefix);
}
return pathname === pattern;
}
/**
* Defensive-clone the configured Principal so callers can't mutate one
* returned object and affect subsequent `resolve()` results. The clone is
* shallow on the top-level fields plus a one-level copy of `scopes` and
* `claims` — every field of `Principal` is documented `readonly`, so callers
* shouldn't be mutating these anyway; this guards against the slip-up.
*/
function cloneFrozen(principal: Principal): Principal {
return Object.freeze({
id: principal.id,
scopes: Object.freeze([...principal.scopes]),
tenantId: principal.tenantId,
claims: Object.freeze({ ...principal.claims }),
}) as Principal;
}
/**
* Factory for the fixed-Principal {@link SessionResolver}.
*
* The returned resolver carries the configured Principal on every
* `resolve()` call — no token decoding, no network I/O, no internal state.
* It is the canonical test-harness adapter for plugin integration tests that
* exercise the full pipeline (RBAC, tenant resolution, tenant-scoped DB)
* without standing up a JWT signer.
*
* Behaviour overrides:
* - `applyTo(req)` — if supplied and returns `false`, `resolve()` returns
* `null` for that request. Runs first.
* - `nullForPaths` — exact-match or suffix-glob path patterns. Matching
* requests resolve to `null` (lets tests exercise `publicRoutes`
* behaviour without rewiring the adapter chain).
*
* `dispose()` is a no-op (still idempotent) because the resolver holds no
* resources.
*
* @throws {AuthStaticPrincipalError} `AUTH_STATIC_PRINCIPAL.INVALID_OPTIONS`
* for malformed outer options, `AUTH_STATIC_PRINCIPAL.INVALID_PRINCIPAL`
* for a malformed `principal` field.
*/
export function createStaticPrincipalResolver(
opts: StaticPrincipalResolverOptions
): SessionResolver {
const parsed = parseStaticPrincipalResolverOptions(opts);
const frozen = cloneFrozen(parsed.principal);
const nullForPaths = parsed.nullForPaths ?? [];
const applyTo = parsed.applyTo;
return {
resolve: async (req: Request): Promise<Principal | null> => {
if (applyTo !== undefined && !applyTo(req)) {
return null;
}
if (nullForPaths.length > 0) {
const url = new URL(req.url);
for (const pattern of nullForPaths) {
if (pathMatches(url.pathname, pattern)) {
return null;
}
}
}
// Return a fresh defensive clone so callers can't mutate one
// returned object and observe the change on the next call.
return cloneFrozen(frozen);
},
dispose: async (): Promise<void> => undefined,
};
}

View File

@@ -0,0 +1 @@
export { createStaticPrincipalResolver } from "./create-static-principal-resolver.js";

View File

@@ -0,0 +1,18 @@
// Adapter
export { createStaticPrincipalAdapter } from "./adapter/adapter.js";
// Error codes
export {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
type AuthStaticPrincipalErrorCode,
} from "./error-codes/error-codes.js";
// Resolver
export { createStaticPrincipalResolver } from "./factory/create-static-principal-resolver.js";
// Options
export {
parseStaticPrincipalResolverOptions,
StaticPrincipalResolverOptionsSchema,
} from "./options/options.js";
export type { StaticPrincipalResolverOptions } from "./options/options.types.js";
// Security scheme
export { staticPrincipalSecurityScheme } from "./security-scheme/security-scheme.js";

View File

@@ -0,0 +1,5 @@
export {
parseStaticPrincipalResolverOptions,
StaticPrincipalResolverOptionsSchema,
} from "./options.js";
export type { StaticPrincipalResolverOptions } from "./options.types.js";

View File

@@ -0,0 +1,82 @@
# `options` — Zod-validated options for `createStaticPrincipalResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Owns the runtime contract between the test harness call site and the
resolver factory. The schema validates both the outer options bag
(`{ principal, nullForPaths?, applyTo? }`) and the nested `Principal` shape
(non-empty `id`, readonly `scopes`, optional `tenantId`, default-`{}`
`claims`). On failure it raises a typed `AuthStaticPrincipalError` carrying
the originating Zod issues — splitting between `INVALID_OPTIONS` (anything
outside `principal`) and `INVALID_PRINCIPAL` (anything inside it).
This module is the only place the package touches Zod at runtime. Keeping it
isolated lets the resolver factory stay a thin three-line function and lets
the conformance test target the resolver behaviour without invoking schema
side-effects.
## Features
- Strict outer schema — unknown keys at the top level reject with
`INVALID_OPTIONS`.
- Discriminated error codes: `principal` issues map to `INVALID_PRINCIPAL`;
everything else maps to `INVALID_OPTIONS`.
- Pre-flight type check rejects non-object inputs with `INVALID_OPTIONS`
before Zod sees them — a cleaner message than the Zod default for `null` /
arrays.
- `claims` defaults to `{}` so callers don't have to write
`claims: {}` for the common case.
## Usage: Basic
```ts
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
const opts = parseStaticPrincipalResolverOptions({
principal: {
id: "u_1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { email: "user@acme.test" },
},
});
```
## Usage: Advanced
Validate options that include both `applyTo` and `nullForPaths`:
```ts
import { parseStaticPrincipalResolverOptions } from "@suaveplan/api-auth-static-principal/resolver";
const opts = parseStaticPrincipalResolverOptions({
principal: { id: "u_1", scopes: [], claims: {} },
nullForPaths: ["/webhooks/*", "/healthz"],
applyTo: (req) => !req.headers.has("x-skip-auth"),
});
```
## API Reference
| Symbol | Kind | Description |
| -------------------------------------------- | -------- | ----------------------------------------------------------------- |
| `StaticPrincipalResolverOptionsSchema` | Zod | Strict object schema for the outer bag. |
| `parseStaticPrincipalResolverOptions(raw)` | function | Returns the validated options or throws `AuthStaticPrincipalError`. |
## Implementation Notes
The pre-flight `typeof`/`Array.isArray` check lets the function return a
single canonical error for "you handed me something that isn't an object" —
Zod's own behaviour on `null` is to emit
`{ code: "invalid_type", expected: "object" }`, which is correct but verbose.
Splitting into a pre-flight + Zod pass is one extra branch but keeps the
caller-facing failure message stable.
`claims` is recorded as `z.record(z.unknown())` so any free-form JWT/claims
payload validates. Coercing to readonly happens at the TypeScript boundary
(`StaticPrincipalResolverOptions`), not in Zod — Zod's `.readonly()` is
type-level only and would re-wrap arrays at runtime unnecessarily.

View File

@@ -0,0 +1,173 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "../error-codes/error-codes.js";
import {
parseStaticPrincipalResolverOptions,
StaticPrincipalResolverOptionsSchema,
} from "./options.js";
const VALID = {
principal: {
id: "u_1",
scopes: ["billing:write"],
tenantId: "acme",
claims: { sub: "u_1" },
},
};
describe("parseStaticPrincipalResolverOptions", () => {
it("returns the parsed bag on a valid input", { timeout: 5000 }, () => {
const out = parseStaticPrincipalResolverOptions(VALID);
expect(out.principal.id).toBe("u_1");
expect(out.principal.scopes).toEqual(["billing:write"]);
expect(out.principal.tenantId).toBe("acme");
expect(out.principal.claims).toEqual({ sub: "u_1" });
});
it("defaults claims to empty object", { timeout: 5000 }, () => {
const out = parseStaticPrincipalResolverOptions({
principal: { id: "u_2", scopes: [] },
});
expect(out.principal.claims).toEqual({});
});
it("accepts nullForPaths + applyTo together", { timeout: 5000 }, () => {
const out = parseStaticPrincipalResolverOptions({
principal: { id: "u_1", scopes: [] },
nullForPaths: ["/webhooks/*"],
applyTo: (_req: Request) => true,
});
expect(out.nullForPaths).toEqual(["/webhooks/*"]);
expect(typeof out.applyTo).toBe("function");
});
it("throws INVALID_OPTIONS for null", { timeout: 5000 }, () => {
try {
parseStaticPrincipalResolverOptions(null);
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(AuthStaticPrincipalError);
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
);
}
});
it("throws INVALID_OPTIONS for an array", { timeout: 5000 }, () => {
try {
parseStaticPrincipalResolverOptions([]);
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
);
}
});
it("throws INVALID_OPTIONS for a primitive", { timeout: 5000 }, () => {
try {
parseStaticPrincipalResolverOptions("oops");
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
);
}
});
it("throws INVALID_PRINCIPAL when principal is missing", {
timeout: 5000,
}, () => {
try {
parseStaticPrincipalResolverOptions({});
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
);
}
});
it("throws INVALID_PRINCIPAL when principal is not an object", {
timeout: 5000,
}, () => {
try {
parseStaticPrincipalResolverOptions({ principal: "u_1" });
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
);
}
});
it("throws INVALID_PRINCIPAL when principal.id is missing", {
timeout: 5000,
}, () => {
try {
parseStaticPrincipalResolverOptions({
principal: { scopes: [], claims: {} },
});
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
);
}
});
it("throws INVALID_PRINCIPAL when principal.id is empty", {
timeout: 5000,
}, () => {
try {
parseStaticPrincipalResolverOptions({
principal: { id: "", scopes: [], claims: {} },
});
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
);
}
});
it("throws INVALID_OPTIONS for unknown top-level keys", {
timeout: 5000,
}, () => {
try {
parseStaticPrincipalResolverOptions({
...VALID,
unknown: true,
});
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS
);
}
});
it("throws INVALID_PRINCIPAL for unknown principal keys", {
timeout: 5000,
}, () => {
try {
parseStaticPrincipalResolverOptions({
principal: { id: "u_1", scopes: [], claims: {}, extra: 1 },
});
throw new Error("expected throw");
} catch (err) {
expect((err as AuthStaticPrincipalError).code).toBe(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL
);
}
});
});
describe("StaticPrincipalResolverOptionsSchema", () => {
it("is a Zod schema with safeParse", { timeout: 5000 }, () => {
const parsed = StaticPrincipalResolverOptionsSchema.safeParse(VALID);
expect(parsed.success).toBe(true);
});
});

View File

@@ -0,0 +1,84 @@
import z from "zod";
import {
AUTH_STATIC_PRINCIPAL_CODES,
AuthStaticPrincipalError,
} from "../error-codes/error-codes.js";
import type { StaticPrincipalResolverOptions } from "./options.types.js";
/**
* Zod schema for the `Principal` shape carried in options. Mirrors the
* `@suaveplan/api-types` interface but enforces non-empty `id` and a
* `readonly scopes` list at runtime. `tenantId` is accepted as any non-empty
* string (branded `TenantId` upstream — Zod can't validate the brand without
* dropping into runtime nominal typing).
*/
const PrincipalSchema = z.strictObject({
id: z.string().min(1, "principal.id must be a non-empty string"),
scopes: z.array(z.string()),
tenantId: z.string().min(1).optional(),
claims: z.record(z.string(), z.unknown()).default({}),
});
/**
* Zod schema for the outer options bag. `principal` is required; `applyTo`
* and `nullForPaths` are optional. `applyTo` is validated as a generic
* callable via {@link z.custom}; Zod v4 does not run-time-validate function
* argument/return shapes (`z.function()` is type-level), so the test harness
* relies on TypeScript at the call boundary for that.
*/
export const StaticPrincipalResolverOptionsSchema = z.strictObject({
principal: PrincipalSchema,
nullForPaths: z.array(z.string().min(1)).optional(),
applyTo: z
.custom<(req: Request) => boolean>(
(val) => typeof val === "function",
"applyTo must be a function (req: Request) => boolean"
)
.optional(),
});
/**
* Parse + validate the caller-supplied options. Throws an
* `AuthStaticPrincipalError` with `INVALID_OPTIONS` or `INVALID_PRINCIPAL`
* synchronously on validation failure.
*/
export function parseStaticPrincipalResolverOptions(
raw: unknown
): StaticPrincipalResolverOptions {
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
"createStaticPrincipalResolver: options must be a plain object",
{ context: { received: typeof raw } }
);
}
if (
!("principal" in (raw as Record<string, unknown>)) ||
(raw as { principal?: unknown }).principal === undefined ||
(raw as { principal?: unknown }).principal === null ||
typeof (raw as { principal?: unknown }).principal !== "object"
) {
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
"createStaticPrincipalResolver: principal is missing or not an object"
);
}
const parsed = StaticPrincipalResolverOptionsSchema.safeParse(raw);
if (!parsed.success) {
const issues = parsed.error.issues;
const principalIssue = issues.find((i) => i.path[0] === "principal");
if (principalIssue !== undefined) {
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_PRINCIPAL,
"createStaticPrincipalResolver: principal failed schema validation",
{ context: { issues } }
);
}
throw new AuthStaticPrincipalError(
AUTH_STATIC_PRINCIPAL_CODES.INVALID_OPTIONS,
"createStaticPrincipalResolver: options failed schema validation",
{ context: { issues } }
);
}
return parsed.data as unknown as StaticPrincipalResolverOptions;
}

View File

@@ -0,0 +1,31 @@
import type { Principal } from "@suaveplan/api-types/principal";
/**
* Caller-side options for {@link createStaticPrincipalResolver}.
*
* `applyTo` and `nullForPaths` are mutually compatible — both return `null`
* when their condition matches. `applyTo` runs first; if it returns `false`,
* `nullForPaths` is not consulted. The most common Stage-5 usage configures
* neither (every request gets the Principal).
*/
export interface StaticPrincipalResolverOptions {
/**
* The fixed Principal returned from every `resolve()` call (unless
* `applyTo` or `nullForPaths` overrides it). Validated against the
* `Principal` Zod schema at construction time.
*/
readonly principal: Principal;
/**
* Optional path-pattern allowlist. Patterns are exact-match or simple
* suffix-glob (`/foo/*`). When supplied, requests whose URL pathname
* matches any pattern resolve to `null` instead of the configured
* Principal — letting tests exercise `publicRoutes` behaviour without
* rewiring the whole adapter chain.
*/
readonly nullForPaths?: readonly string[];
/**
* Optional predicate. When supplied and it returns `false` for a request,
* `resolve()` returns `null`. Runs before `nullForPaths`.
*/
readonly applyTo?: (req: Request) => boolean;
}

View File

@@ -0,0 +1 @@
export { staticPrincipalSecurityScheme } from "./security-scheme.js";

View File

@@ -0,0 +1,75 @@
# `security-scheme` — OpenAPI contribution for the static-principal adapter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the per-adapter `SecuritySchemeContribution` slot of
`AuthAdapter`. The static-principal adapter is a test-only construct: there
is no real authentication flow to document on routes that use it. This
module returns `undefined` deliberately — which `validateAuthAdapter` from
`@suaveplan/api-auth` permits — so the generated OpenAPI document emits no
security entry for routes under this adapter, mirroring how the anonymous
tail adapter is treated.
Wrapping the absence in a function gives the package a symmetric surface
with adapters that compute their contribution dynamically, and gives the
test suite a callable to target for the "intentionally absent" assertion.
## Features
- Returns `undefined` so the host omits a `security` entry on routes wired
to this adapter.
- Permitted by `validateAuthAdapter` (the anonymous-style topology).
- Callable surface for symmetry with adapters that compute their scheme.
## Usage: Basic
```ts
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
import type { AuthAdapter } from "@suaveplan/api-auth";
const adapter: AuthAdapter = {
resolver,
securityScheme: staticPrincipalSecurityScheme(),
csrf: false,
};
```
## Usage: Advanced
When composing static-principal with another adapter that supplies a real
scheme, drop this contribution and use the other adapter's scheme in the
composite — the host requires only one to be present per route. The
function still acts as documentation that the static-principal layer
contributes nothing of its own:
```ts
import { staticPrincipalSecurityScheme } from "@suaveplan/api-auth-static-principal/adapter";
if (staticPrincipalSecurityScheme() !== undefined) {
// unreachable in this version — the function is documented to return
// undefined. Tests assert this invariant.
}
```
## API Reference
| Symbol | Kind | Description |
| ----------------------------------- | -------- | ------------------------------------------------ |
| `staticPrincipalSecurityScheme()` | function | Returns `undefined` — no OpenAPI contribution. |
## Implementation Notes
`validateAuthAdapter` treats `securityScheme: undefined` as a permitted
topology (the "no auth configured" path used by `api-auth-anonymous`). The
generated OpenAPI document omits a `security` array on routes wired to an
adapter that omits the scheme; combined with the host's `publicRoutes`
decision, this matches the runtime behaviour of the static-principal
adapter in tests.
Changing this to return a real scheme in a future version would be a
breaking change for downstream consumers' OpenAPI snapshots; treat the
"returns undefined" behaviour as a load-bearing contract.

View File

@@ -0,0 +1,16 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { staticPrincipalSecurityScheme } from "./security-scheme.js";
describe("staticPrincipalSecurityScheme", () => {
it("returns undefined (no OpenAPI scheme contribution)", {
timeout: 5000,
}, () => {
expect(staticPrincipalSecurityScheme()).toBeUndefined();
});
it("is a function (symmetry with adapters that compute dynamically)", {
timeout: 5000,
}, () => {
expect(typeof staticPrincipalSecurityScheme).toBe("function");
});
});

View File

@@ -0,0 +1,23 @@
import type { SecuritySchemeContribution } from "@suaveplan/api-auth";
/**
* OpenAPI security-scheme contribution for the static-principal adapter.
*
* Static-principal is a test-only adapter; the generated OpenAPI document
* shouldn't advertise a real auth flow for it. Returning `undefined` here is
* permitted by `validateAuthAdapter`: the host emits no `security` entry for
* routes under this adapter, matching the way `api-auth-anonymous` is
* handled. Callers that need a documented scheme (e.g. for a sandboxed test
* portal) should compose this resolver with a separate adapter that
* contributes a scheme of its own.
*
* Returned as a function (rather than a constant `undefined`) so the surface
* is symmetrical with adapters that compute a scheme dynamically — and so
* the test asserting "this contribution is intentionally absent" can target
* a callable.
*/
export function staticPrincipalSecurityScheme():
| SecuritySchemeContribution
| undefined {
return undefined;
}

View File

@@ -0,0 +1,32 @@
/**
* §16 conformance proof at the package level — the static-principal
* resolver MUST pass `runAuthSessionConformance` from
* `@suaveplan/api-auth/testing` with both seed paths populated. Seeded
* with a known Principal so the harness exercises the "recognised request"
* scenario; `nullForPaths` lets the harness assert the "unrecognised"
* scenario without rewiring the factory.
*/
import { runAuthSessionConformance } from "@suaveplan/api-auth/testing";
import type { Principal } from "@suaveplan/api-types/principal";
import { createStaticPrincipalResolver } from "../../src/factory/create-static-principal-resolver.js";
const P: Principal = {
id: "u_pkg_conf" as Principal["id"],
scopes: ["billing:write" as Principal["scopes"][number]],
tenantId: "acme" as Principal["tenantId"],
claims: { sub: "u_pkg_conf" },
};
runAuthSessionConformance({
label: "api-auth-static-principal",
factory: () =>
createStaticPrincipalResolver({
principal: P,
nullForPaths: ["/__public/*"],
}),
seedNull: () => new Request("https://example.test/__public/health"),
seedPrincipal: () => ({
request: new Request("https://example.test/api/billing/invoices"),
expected: P,
}),
});

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "./dist",
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*.ts", "tests/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022", "DOM"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,30 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/api-auth-static-principal",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [/^@suaveplan\//, /^node:/, "tslib", "zod"],
},
},
});

View File

@@ -0,0 +1,31 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.types.ts",
"src/**/index.ts",
"src/**/*.bench.ts",
"src/index.ts",
"tests/**",
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
testTimeout: 30000,
},
});