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,72 @@
# `adapter` — `ANONYMOUS_AUTH_ADAPTER`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Canonical `AuthAdapter` for fully public APIs. Pairs the trivial anonymous
resolver with the minimal adapter metadata the host needs to wire it into the
per-request pipeline. The adapter omits `securityScheme` on purpose:
`validateAuthAdapter` permits a missing scheme for the anonymous topology so
the emitted OpenAPI document contains no `security` entry on routes — the
correct shape for "no authentication required".
## Features
- Stateless singleton — safe to import once per host process.
- `csrf: false` baked in: anonymous traffic has no session to protect; the
generic CSRF middleware in `@suaveplan/api-auth/csrf-policy` skips routes
whose adapter pins `csrf: false`.
- Validates cleanly against `validateAuthAdapter` — no runtime error at host
registration time despite the omitted security scheme.
## Usage: Basic
```ts
import { ANONYMOUS_AUTH_ADAPTER } from "@suaveplan/api-auth-anonymous/adapter";
import { validateAuthAdapter } from "@suaveplan/api-auth";
const adapter = validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER);
host.registerAuth(adapter);
```
## Usage: Advanced
For a chain that mixes typed resolvers with the anonymous tail, do not use
`ANONYMOUS_AUTH_ADAPTER` directly — assemble a `SessionResolverChain` so the
typed resolvers fire first:
```ts
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const chain = new SessionResolverChain([
jwtResolver,
createAnonymousResolver(),
]);
const adapter = {
resolver: chain,
securityScheme: jwtSecurityScheme, // the typed resolver still emits its scheme
} as const;
```
## API Reference
| Symbol | Kind | Description |
|------------------------------|----------|------------------------------------------------|
| `ANONYMOUS_AUTH_ADAPTER` | const | `AuthAdapter` with anonymous resolver, no scheme, `csrf: false`. |
## Implementation Notes
- The constant is constructed once at module load and reused across requests
— the resolver itself is documented stateless, so a single instance is safe
to fan out across concurrent requests.
- `csrf: false` is deliberate. Anonymous requests carry no session cookie, so
applying CSRF defence would reject legitimate public traffic. Hosts that
want to additionally rate-limit anonymous traffic should layer
`api-rate-limit` (Stage 8) on top — that is not this package's concern.
- Test coverage exercises (a) `validateAuthAdapter(ANONYMOUS_AUTH_ADAPTER)`
returns the adapter unchanged, (b) the embedded resolver still resolves
null, and (c) the singleton survives repeated import without re-allocation.

View File

@@ -0,0 +1,77 @@
# `error-codes` — `AUTH_ANON.*` registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Stable error-code registry for `@suaveplan/api-auth-anonymous`. The anonymous
resolver has no per-request failure modes — every request resolves to `null` by
construction. The single `AUTH_ANON.INVALID_OPTIONS` code covers the only thing
that can fail, which is rejecting malformed options handed to
`createAnonymousResolver(opts)` at construction time. Keeping a dedicated
code-namespace per package (rather than re-using `AUTH.*`) means consumers can
disambiguate anonymous-side problems from general session-resolver issues when
multiple resolvers are mounted on the same chain.
## Features
- Defined via `defineCodes("AUTH_ANON", [...])` from `@suaveplan/error` so the
literal value is always `"AUTH_ANON.INVALID_OPTIONS"`.
- `AuthAnonError extends SuaveplanError<AuthAnonErrorCode>` — instance checks
narrow `error.code` to the registry type at the type level.
- Carries `authAnonCode` as a non-discriminating mirror so adapter code can
branch on `err.authAnonCode === AUTH_ANON_CODES.INVALID_OPTIONS` without
re-typing the parent class.
## Usage: Basic
```ts
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
throw new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"createAnonymousResolver: options is not an object"
);
```
## Usage: Advanced
Wrap a parser failure (e.g. zod) and forward its details into the
`context.issues` slot so observability pipelines can render the schema diff:
```ts
import { AUTH_ANON_CODES, AuthAnonError } from "@suaveplan/api-auth-anonymous/error-codes";
import z from "zod";
const OptionsSchema = z.object({}).strict();
export function parseOptions(input: unknown): Record<string, never> {
const out = OptionsSchema.safeParse(input);
if (!out.success) {
throw new AuthAnonError(
AUTH_ANON_CODES.INVALID_OPTIONS,
"Anonymous resolver options failed schema validation",
{ context: { issues: out.error.issues } }
);
}
return out.data;
}
```
## API Reference
| Symbol | Kind | Description |
|----------------------|----------|------------------------------------------------------------|
| `AUTH_ANON_CODES` | const | `{ INVALID_OPTIONS: "AUTH_ANON.INVALID_OPTIONS" }` |
| `AuthAnonErrorCode` | type | Union of values in `AUTH_ANON_CODES`. |
| `AuthAnonError` | class | `extends SuaveplanError<AuthAnonErrorCode>`. |
## Implementation Notes
- The single code is deliberate. The resolver is documented to never reject a
request; adding speculative codes for "unrecognised credential" or
"principal lookup failed" would be lying about a contract that the
implementation cannot break by construction.
- Tested in `error-codes.test.ts` for both string value and instance shape.

View File

@@ -0,0 +1,79 @@
# `resolver` — `createAnonymousResolver`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Factory for the trivial `SessionResolver` that always returns `null`. This is
the canonical tail-of-chain resolver every public-API host configures: it
makes the "no auth" choice explicit in `host.json` and lets the rest of the
host (RBAC gate, error envelope, OpenAPI emission) operate uniformly because
`principal` is `null` not `undefined`. The factory shape exists — rather than
exposing a constant — so future option flags (e.g. `logAnonymousAccess`) can
be threaded in without a breaking surface change.
## Features
- Always resolves `null` — no header inspection, no network I/O, no state.
- Idempotent `dispose()` no-op (resolver holds no resources).
- Strict zod options schema rejects non-object input and unknown keys with
`AUTH_ANON.INVALID_OPTIONS`.
- Returned type is `SessionResolver` from `@suaveplan/api-auth/resolver` so
it composes directly into `SessionResolverChain`.
## Usage: Basic
```ts
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const resolver = createAnonymousResolver();
// Inside a request pipeline:
const principal = await resolver.resolve(request); // → null
await resolver.dispose(); // safe to call again
```
## Usage: Advanced
Compose as the tail of a resolver chain so a JWT-typed resolver runs first
and the anonymous resolver supplies the `null` short-circuit for unrecognised
requests on public routes:
```ts
import { SessionResolverChain } from "@suaveplan/api-auth";
import { createAnonymousResolver } from "@suaveplan/api-auth-anonymous/resolver";
const chain = new SessionResolverChain([
jwtResolver, // recognises Bearer tokens, else returns null
apiKeyResolver, // recognises x-api-key, else returns null
createAnonymousResolver(), // tail: always null
]);
const principal = await chain.resolve(request);
// principal is now `Principal | null`. The host applies its `publicRoutes`
// decision: a non-public route 401s; a public route proceeds with null.
```
## API Reference
| Symbol | Kind | Description |
|------------------------------|----------|-------------------------------------------------------|
| `createAnonymousResolver` | function | `(opts?: AnonymousResolverOptions) => SessionResolver`. |
| `AnonymousResolverOptions` | type | Reserved-for-future-flags options bag. |
## Implementation Notes
- The resolver returns `Promise<null>` literally rather than the wider
`Promise<Principal | null>` so type narrowing in tests can use
`expectTypeOf<...>().toEqualTypeOf<Promise<null>>()`. The chain widens it
back to `Principal | null` at the boundary.
- `dispose()` is a `Promise<void>` not just any promise — the conformance
harness asserts the resolved value is `undefined` so this is load-bearing.
- The factory is the public surface (not a const) because future flags must
be addable without a breaking change to call sites. Even today, passing
`{}` is the canonical call.
- No OTel instrumentation is emitted from this resolver. Anonymous-access
observability lives at the host layer (per-request span tag) — adding
counters here would force a foundation-tier package to depend on
`@suaveplan/opentelemetry`, which is undesirable for a hot path.