v0.16.0 — Sandbox access: API keys + quotas (G19d)
Sandbox access (G19d, ADR 0011)
The fourth vertical of the integration sandbox: hand out per-consumer API keys so external teams can hit a tenant's mock surface without knowing anything about hosts or headers.
API keys — --sandbox-auth
POST /__admin/apikeysissues anmfk_…token (256-bit CSPRNG) shown exactly once — afterwards only a 12-character display prefix is stored and listed. At rest: salted SHA-256, verified in constant time.- A key presented as
X-Api-Key: mfk_…orAuthorization: Bearer mfk_…selects the key's tenant ahead of the host/header chain. No credentials → everything resolves exactly as before; an invalid or revoked key is an honest 401, never a silent fall-through to another tenant. - A sandbox key is not admin authentication —
/__admin/*refuses it on both carriers. - Keys survive restarts on every persistence backend (file root-dir, LiteDB, Postgres, Redis).
Quotas
- Optional
quotaPerHourper key: fixed hourly window, exact under parallel load — N requests across the budget never admit more than the budget. - Counted responses carry
X-RateLimit-Limit/X-RateLimit-Remaining/X-RateLimit-Reset; past the budget the caller gets a realistic 429 withRetry-After.usedThisHouris visible in the key listing.
Validation
No oracle exists for sandbox keys, so per the established precedent: 10 unit tests (constant-time verification, pinned hash format, exact window boundaries on a test clock, 8×50 parallel admits exactly 100 of a 100 budget, persistence round-trips) + 5 wire self-tests against a restarted real host (tenant isolation with two keys, admin refusal, an exact parallel 429 boundary, restart survival). Stryker on the new pure logic: 28/29 killed, the lone survivor analyzed as an equivalent mutant in docs/parity/g19-sandbox.md.
Full change: #213
🤖 Generated with Claude Code