5.5 KiB
Capability Spec Delta: api-auth (api-auth)
Package: @suaveplan/api-auth
Status: PROPOSED
Every requirement carries a
**Verified by:**line.
ADDED Requirements
Requirement: SessionResolver contract
The package SHALL export SessionResolver interface: resolve(req: Request): Promise<Principal | null>, dispose(): Promise<void>. A resolver returning null means "I don't recognise this request" — NOT "auth failed." Auth failure (401) is the host's decision based on chain outcome + route's publicRoutes membership.
Scenario: every method present
- WHEN
expectTypeOf<SessionResolver>()is checked - THEN
resolveanddisposeare declared with the documented signatures
Verified by: src/resolver/resolver.spec-conformance.test.ts::"SessionResolver declares resolve and dispose"
Requirement: import z from "zod" everywhere
Every Zod import MUST use the default form.
Scenario: no named import of z
- WHEN the test greps
import zfrom"zod" - THEN zero matches
Verified by: tests/zod-import-form.test.ts::"every src file uses default-import z"
Requirement: Resolver chain — first-match-wins
The package SHALL export SessionResolverChain(resolvers: readonly SessionResolver[]).resolve(req): Promise<Principal | null> running each resolver in order; the first non-null result wins; if all return null, the chain returns null. An empty chain MUST throw AUTH.CHAIN_EMPTY at construction. By default, a resolver throwing MUST surface AUTH.RESOLVER_THREW AND short-circuit the chain (fail-closed). An opt-in continueOnThrow: true flag lets the chain try the next resolver; this is documented as escape-hatch only.
Scenario: first non-null wins
- WHEN chain
[null, principalA, principalB]is run - THEN the chain returns
principalAAND resolver B is NOT called
Scenario: all null returns null
- WHEN every resolver returns null
- THEN the chain returns null
Scenario: throw is fail-closed by default
- WHEN resolver A throws AND
continueOnThrowis false (default) - THEN the chain throws
AUTH.RESOLVER_THREWAND resolver B is NOT called
Scenario: empty chain rejected at construction
- WHEN
new SessionResolverChain([])is called - THEN it throws
AUTH.CHAIN_EMPTY
Verified by: src/chain/chain.test.ts::"first non-null wins, later resolvers skipped" AND src/chain/chain.test.ts::"all null returns null" AND src/chain/chain.test.ts::"throw is fail-closed by default" AND src/chain/chain.test.ts::"empty chain rejected at construction"
Requirement: SecuritySchemeContribution
The package SHALL export SecuritySchemeContribution with { name: string, scheme: OpenApiSecurityScheme }. Every concrete SessionResolver MUST contribute exactly one scheme so api-openapi can emit the right securitySchemes block. An AuthAdapter whose securityScheme is absent at registration MUST surface AUTH.SECURITY_SCHEME_MISSING.
Scenario: missing scheme rejected
- WHEN an
AuthAdapterwithoutsecuritySchemeis registered - THEN registration throws
AUTH.SECURITY_SCHEME_MISSING
Verified by: src/security-scheme/security-scheme.test.ts::"registration rejects adapter without securityScheme"
Requirement: CSRF default policy
The package SHALL export CsrfPolicy.requiresCsrf(scheme: OpenApiSecurityScheme): boolean. Cookie-based schemes (type: "apiKey" AND in: "cookie") default ON; JWT/Bearer schemes (type: "http" AND scheme: "bearer") default OFF. Per-route csrf: false opt-out is honoured by the host (this package exposes the default; the host applies the override).
Scenario: cookie scheme defaults to CSRF ON
- WHEN
requiresCsrf({ type: "apiKey", in: "cookie", ... })is called - THEN the result is
true
Scenario: bearer scheme defaults to CSRF OFF
- WHEN
requiresCsrf({ type: "http", scheme: "bearer" })is called - THEN the result is
false
Verified by: src/csrf-policy/csrf-policy.test.ts::"cookie schemes default to CSRF ON" AND src/csrf-policy/csrf-policy.test.ts::"bearer schemes default to CSRF OFF"
Requirement: CapabilityId augmentation contributes "auth.session"
The package SHALL augment the CapabilityId union to include "auth.session".
Scenario: auth.session assignable to CapabilityId
- WHEN
expectTypeOf<"auth.session">().toMatchTypeOf<CapabilityId>()is checked - THEN the assertion passes
Verified by: src/capability-id/capability-id.spec-conformance.test.ts::"auth.session is a valid CapabilityId after augmentation"
Requirement: Error codes registry frozen
The package SHALL export AUTH_CODES containing exactly: RESOLVER_THREW, CHAIN_EMPTY, PRINCIPAL_INVALID_SHAPE, SECURITY_SCHEME_MISSING.
Scenario: every code reachable
- WHEN the test enumerates
AUTH_CODES - THEN every code has a production throw site
Verified by: src/error-codes/error-codes.spec-conformance.test.ts::"every AUTH_CODES entry thrown by production path"
Requirement: Conformance harness for resolvers
The package SHALL export runAuthSessionConformance({ factory, label }). Every concrete resolver in Stages 4/6/8 MUST pass it.
Scenario: harness covers resolve null, resolve principal, dispose
- WHEN the harness is run against a stub resolver
- THEN test categories exist for each documented behavior
Verified by: src/testing/conformance.test.ts::"harness covers resolve null, resolve principal, dispose lifecycle"