Initial Commit
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# `@suaveplan/api-auth-bearer-static` — Static-token service auth
|
||||
|
||||
**Package**: `@suaveplan/api-auth-bearer-static`
|
||||
**Status**: PROPOSED
|
||||
**Type**: new-package
|
||||
**Stage**: 4 (Base adapters) — see umbrella `openspec/changes/api-plugin-foundation/`
|
||||
|
||||
## Why
|
||||
|
||||
Service-to-service callers (cron runners, sidecar daemons, internal admin tooling) often present a single static bearer token in lieu of a full JWT or OAuth flow. Per design doc §2.3, `api-auth-bearer-static` resolves these tokens against a fixed table: `token → Principal`. No provider, no DB lookup, no JWKS — just constant-time match.
|
||||
|
||||
Common in chained-resolver topologies: `api-auth-jwt` for users AND `api-auth-bearer-static` for service callers, first match wins. When paired with the host's admin API, this adapter SHOULD be gated by mutual-TLS (operator config) — the bearer token is necessary but not sufficient.
|
||||
|
||||
Stage 4 invariant: ships both direct and worker-proxy surfaces; both pass `runAuthSessionConformance` (§5.2a, §13.4).
|
||||
|
||||
## What changes
|
||||
|
||||
Net-new package depending on `@suaveplan/api-auth`, `@suaveplan/api-types`, `@suaveplan/api-worker-rpc`, `@suaveplan/crypto-utils` (constant-time equality), `@suaveplan/error`, `zod`.
|
||||
|
||||
- ADDED: `createBearerStaticResolver(opts)` — direct-surface factory.
|
||||
- ADDED: `createBearerStaticResolverWorkerProxy(port, opts)` — proxy-surface factory.
|
||||
- ADDED: `BearerStaticResolverOptions` — Zod schema with `tokens: Array<{ token: string; principal: Principal; mtlsRequired?: boolean }>`, `headerName?: string` (default `Authorization`), `prefix?: string` (default `Bearer`).
|
||||
- ADDED: `securitySchemeContribution()` — OpenAPI `securitySchemes` entry for HTTP Bearer with custom scheme name.
|
||||
- ADDED: `AUTH_BEARER_STATIC_*` error codes (`AUTH_BEARER_STATIC_NO_TOKEN`, `AUTH_BEARER_STATIC_UNKNOWN_TOKEN`, `AUTH_BEARER_STATIC_MTLS_REQUIRED`, `AUTH_BEARER_STATIC_INVALID_OPTIONS`).
|
||||
- ADDED: Optional mutual-TLS gate — when `mtlsRequired: true` for a token entry, the host MUST surface a verified client-cert subject; absence → reject. The host integration point is `request.tls?.peerCertificate`; this adapter checks for its presence and rejects otherwise.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Token rotation — operators reload host config to swap tokens; this adapter is intentionally static.
|
||||
- DB-backed token lookups — that's `api-auth-better-auth` or a future `api-auth-token-table` adapter.
|
||||
- Establishing the TLS context — the host's listener configures TLS; this adapter only reads what the listener attached.
|
||||
|
||||
## Source mapping
|
||||
|
||||
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
||||
|
||||
| Requirement | Design doc section |
|
||||
|---|---|
|
||||
| Service-to-service static tokens | §2.3 (`api-auth-bearer-static` bullet) |
|
||||
| Optional mTLS gate | §2.3 (final paragraph) |
|
||||
| Worker-proxy variant | §5.2a |
|
||||
| Conformance both surfaces | §13.4 |
|
||||
|
||||
## Verification approach
|
||||
|
||||
- `src/factory/create-bearer-static-resolver.test.ts` — token match returns Principal; unknown token → null; missing token → null.
|
||||
- `src/factory/create-bearer-static-resolver.spec-conformance.test.ts` — `runAuthSessionConformance("bearer-static-direct", ...)`.
|
||||
- `src/proxy/create-bearer-static-resolver-worker-proxy.test.ts` — proxy unit tests.
|
||||
- `src/proxy/create-bearer-static-resolver-worker-proxy.spec-conformance.test.ts` — `runAuthSessionConformance("bearer-static-worker-proxy", ...)`.
|
||||
- `src/mtls/mtls-gate.test.ts` — `mtlsRequired: true` rejects when peerCertificate missing.
|
||||
- `src/constant-time/token-match.test.ts` — constant-time comparison invariant (no early exit).
|
||||
|
||||
## Migration impact
|
||||
|
||||
None — additive only.
|
||||
@@ -0,0 +1,80 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: api-auth-bearer-static (api-auth-bearer-static)
|
||||
|
||||
**Package**: `@suaveplan/api-auth-bearer-static`
|
||||
**Status**: PROPOSED
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Static token table resolves to Principal
|
||||
|
||||
The package SHALL match incoming bearer tokens against a fixed table supplied at construction time. A matching token produces the associated `Principal`; an unknown token produces `null`. Token comparison MUST be constant-time so an attacker cannot probe valid token prefixes via timing.
|
||||
|
||||
#### Scenario: known token returns its Principal
|
||||
|
||||
- **WHEN** a request carries `Authorization: Bearer T1` AND the table contains `{ token: "T1", principal: P }`
|
||||
- **THEN** `resolveSession()` returns `P`
|
||||
|
||||
#### Scenario: unknown token returns null
|
||||
|
||||
- **WHEN** a request carries `Authorization: Bearer NOT_IN_TABLE`
|
||||
- **THEN** `resolveSession()` returns `null` AND emits `AUTH_BEARER_STATIC_UNKNOWN_TOKEN` on the failure counter
|
||||
|
||||
#### Scenario: token match is constant-time
|
||||
|
||||
- **WHEN** two unknown tokens of equal length are compared against the table
|
||||
- **THEN** the comparison loop runs to completion for both (no early exit on first mismatched byte)
|
||||
|
||||
**Verified by:** `src/factory/create-bearer-static-resolver.test.ts::"known token returns its Principal"` AND `src/factory/create-bearer-static-resolver.test.ts::"unknown token returns null and increments failure counter"` AND `src/constant-time/token-match.test.ts::"comparison runs to completion regardless of mismatch position"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Optional mutual-TLS gate
|
||||
|
||||
A token entry MAY carry `mtlsRequired: true`. When set, the resolver MUST require the request to carry a verified peer certificate (surfaced by the host listener); absence MUST produce `null` and emit `AUTH_BEARER_STATIC_MTLS_REQUIRED`.
|
||||
|
||||
#### Scenario: mTLS-gated token rejects requests without peer cert
|
||||
|
||||
- **WHEN** an mTLS-gated token is presented but `request.tls.peerCertificate` is undefined
|
||||
- **THEN** `resolveSession()` returns `null` AND emits `AUTH_BEARER_STATIC_MTLS_REQUIRED`
|
||||
|
||||
#### Scenario: mTLS-gated token accepts requests with peer cert
|
||||
|
||||
- **WHEN** the same token is presented AND `request.tls.peerCertificate` is present
|
||||
- **THEN** `resolveSession()` returns the Principal
|
||||
|
||||
**Verified by:** `src/mtls/mtls-gate.test.ts::"rejects mtls-required token without peer certificate"` AND `src/mtls/mtls-gate.test.ts::"accepts mtls-required token with peer certificate"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Direct and worker-proxy surfaces both pass conformance
|
||||
|
||||
The adapter SHALL pass `runAuthSessionConformance` under both surfaces.
|
||||
|
||||
#### Scenario: direct surface conforms
|
||||
|
||||
- **WHEN** `runAuthSessionConformance("bearer-static-direct", () => createBearerStaticResolver(...))` runs
|
||||
- **THEN** every invariant passes
|
||||
|
||||
#### Scenario: worker-proxy surface conforms
|
||||
|
||||
- **WHEN** `runAuthSessionConformance("bearer-static-worker-proxy", ...)` runs over a real `MessageChannel`
|
||||
- **THEN** every invariant passes
|
||||
|
||||
**Verified by:** `src/factory/create-bearer-static-resolver.spec-conformance.test.ts::"bearer-static-direct conforms to AuthSession contract"` AND `src/proxy/create-bearer-static-resolver-worker-proxy.spec-conformance.test.ts::"bearer-static-worker-proxy conforms to AuthSession contract"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Options Zod-validated
|
||||
|
||||
`BearerStaticResolverOptions` MUST be Zod-validated. Empty `tokens` array OR a non-string token entry MUST throw `AUTH_BEARER_STATIC_INVALID_OPTIONS` at factory construction.
|
||||
|
||||
#### Scenario: empty tokens array throws
|
||||
|
||||
- **WHEN** `createBearerStaticResolver({ tokens: [] })`
|
||||
- **THEN** a `SuaveplanError` with code `AUTH_BEARER_STATIC_INVALID_OPTIONS` is thrown synchronously
|
||||
|
||||
**Verified by:** `src/options/bearer-static-resolver-options.test.ts::"throws on empty tokens array"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
71
openspec/archive/2026-05-26-api-auth-bearer-static/tasks.md
Normal file
71
openspec/archive/2026-05-26-api-auth-bearer-static/tasks.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Tasks — `@suaveplan/api-auth-bearer-static`
|
||||
|
||||
> Every checkbox MUST reflect work actually done in this change.
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [x] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [x] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [x] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## 1. Spec authoring
|
||||
|
||||
- [x] 1.1 Read design doc (§2.3, §5.2a, §13.4)
|
||||
- [x] 1.2 Read `openspec/changes/api-auth/` and `openspec/changes/api-host/` for upstream contracts
|
||||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13 and 14
|
||||
- [x] 1.4 Draft `proposal.md`
|
||||
- [x] 1.5 Draft `specs/api-auth-bearer-static/spec.md`
|
||||
- [x] 1.6 Every requirement carries `**Verified by:**`
|
||||
|
||||
## 2. Implementation
|
||||
|
||||
- [x] 2.1 Scaffold `packages/api/api-auth-bearer-static/` (deps: api-auth, api-types, api-worker-rpc, crypto-utils, error, zod)
|
||||
- [x] 2.2 `src/options/bearer-static-resolver-options.ts` — Zod schema
|
||||
- [x] 2.3 `src/factory/create-bearer-static-resolver.ts` — direct surface
|
||||
- [x] 2.4 `src/proxy/create-bearer-static-resolver-worker-proxy.ts` — proxy surface
|
||||
- [x] 2.5 `src/constant-time/token-match.ts` — constant-time string equality via `@suaveplan/crypto-utils`
|
||||
- [x] 2.6 `src/mtls/mtls-gate.ts` — `mtlsRequired` enforcement
|
||||
- [x] 2.7 `src/security-scheme/security-scheme-contribution.ts` — OpenAPI block
|
||||
- [x] 2.8 `src/error-codes.ts` — `AUTH_BEARER_STATIC_*` codes via `defineCodes`
|
||||
- [x] 2.9 `src/index.ts` — explicit named re-exports
|
||||
|
||||
## 3. Tests
|
||||
|
||||
- [x] 3.1 `src/factory/create-bearer-static-resolver.test.ts`
|
||||
- [x] 3.2 `src/factory/create-bearer-static-resolver.spec-conformance.test.ts` — direct conformance
|
||||
- [x] 3.3 `src/proxy/create-bearer-static-resolver-worker-proxy.test.ts`
|
||||
- [x] 3.4 `src/proxy/create-bearer-static-resolver-worker-proxy.spec-conformance.test.ts` — proxy conformance
|
||||
- [x] 3.5 `src/mtls/mtls-gate.test.ts`
|
||||
- [x] 3.6 `src/constant-time/token-match.test.ts` — constant-time invariant (timing-equivalence sanity)
|
||||
- [x] 3.7 Every `it(...)` carries `{ timeout: 5000 }`
|
||||
|
||||
## 4. Documentation
|
||||
|
||||
- [x] 4.1 Sibling `.md` per implementation `.ts`
|
||||
- [x] 4.2 README documents the chained-resolver pattern with `api-auth-jwt`
|
||||
- [x] 4.3 README warns about token rotation: "static — rotate by reloading host config"
|
||||
|
||||
## 5. Pre-archive verification (5-gate triple)
|
||||
|
||||
- [x] 5.1 `bun run check` — clean
|
||||
- [x] 5.2 `bunx turbo lint typecheck test --filter=@suaveplan/api-auth-bearer-static` — green
|
||||
- [x] 5.3 `bunx turbo test:coverage --filter=@suaveplan/api-auth-bearer-static` — 100/100/100/100
|
||||
- [x] 5.4 `bun scripts/openspec-conformance.ts @suaveplan/api-auth-bearer-static` — exit 0
|
||||
- [x] 5.5 `bunx turbo build --filter=@suaveplan/api-auth-bearer-static` — green
|
||||
|
||||
## 6. Pre-archive 7-row checklist
|
||||
|
||||
- [x] 6.1 `expectTypeOf` assertions on Principal projection
|
||||
- [x] 6.2 Zod schema at `BearerStaticResolverOptions` boundary
|
||||
- [x] 6.3 Every `AUTH_BEARER_STATIC_*` code thrown by at least one production path
|
||||
- [x] 6.4 Strict-mode behaviors (mTLS required vs optional) tested both on and off
|
||||
- [x] 6.5 OTel instruments (`api.auth.bearer_static_match_total{outcome}`) asserted with in-memory `MeterProvider`
|
||||
- [x] 6.6 Lifecycle dedup — N/A (no state)
|
||||
- [x] 6.7 No retroactive tick
|
||||
|
||||
## 7. Archive
|
||||
|
||||
- [x] 7.1 Fold into `openspec/specs/api-auth-bearer-static/spec.md`
|
||||
- [x] 7.2 Move to `openspec/archive/<yyyy-mm-dd>-api-auth-bearer-static/`
|
||||
- [x] 7.3 Tick Stage 4 box in umbrella tasks
|
||||
- [x] 7.4 Refresh `MANIFEST.md`
|
||||
Reference in New Issue
Block a user