Found in production (LicenseSeat, 2026-08-19): keys store their scopes array at creation, so when a host expands config.key_types[:sometype][:permissions], every existing key of that type keeps the old list and starts 403ing endpoints that new keys can call — silently, from the client's perspective, because the key itself is valid. Eight customer keys had been refusing heartbeats and machine-file calls for weeks; the client SDKs were correct the whole time.
For key types that hosts don't let users scope-curate (auto-created, limit: 1 — the "publishable key" pattern), the ceiling IS the contract, and freezing a snapshot of it is a footgun. Options, roughly in order of preference:
- Ceiling-authoritative authorization for key types that opt in (e.g.
scopes: :ceiling in the key_type config): authenticate_api_key!(scope:) checks the live permission list instead of the stored array. Stored scopes stay as metadata. Zero drift, ever.
- A gem-shipped reconciler (
ApiKeys.reconcile_scopes! + docs telling hosts to run it after ceiling changes / on boot / on a schedule) that add-only merges the ceiling into non-revoked keys of that type.
- At minimum, a loud README warning on
key_types: "expanding permissions does not affect existing keys — backfill them."
The host-side workaround shipped in LicenseSeat is (2) as a daily job plus a health check that counts stale keys; happy to upstream either shape.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RnnxM5g6WNW4vvYYFBM1tn
Found in production (LicenseSeat, 2026-08-19): keys store their
scopesarray at creation, so when a host expandsconfig.key_types[:sometype][:permissions], every existing key of that type keeps the old list and starts 403ing endpoints that new keys can call — silently, from the client's perspective, because the key itself is valid. Eight customer keys had been refusing heartbeats and machine-file calls for weeks; the client SDKs were correct the whole time.For key types that hosts don't let users scope-curate (auto-created,
limit: 1— the "publishable key" pattern), the ceiling IS the contract, and freezing a snapshot of it is a footgun. Options, roughly in order of preference:scopes: :ceilingin the key_type config):authenticate_api_key!(scope:)checks the live permission list instead of the stored array. Stored scopes stay as metadata. Zero drift, ever.ApiKeys.reconcile_scopes!+ docs telling hosts to run it after ceiling changes / on boot / on a schedule) that add-only merges the ceiling into non-revoked keys of that type.key_types: "expanding permissions does not affect existing keys — backfill them."The host-side workaround shipped in LicenseSeat is (2) as a daily job plus a health check that counts stale keys; happy to upstream either shape.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RnnxM5g6WNW4vvYYFBM1tn