feat(auth): migrate API keys to a SQLite credential registry - #430
feat(auth): migrate API keys to a SQLite credential registry#430cursor[bot] wants to merge 4 commits into
Conversation
Store SHA-256 verifiers in api_credentials so request-time auth no longer reads CODEC_CARVER_API_KEYS. Bootstrap stays idempotent; rotate keeps the previous key valid until revoke. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
Named startup copies transport keys once. Middleware verifies X-API-Key against stored digests, ignores later env changes, and returns 401 without echoing secrets. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
Stale comment
This is the #373-complete credential landing on this head (
5a80b18). It adds the fail-closed public-bind policy,credential_eventsaudit rows, and public listings that omitkey_digest.Do this next:
- Keep #430 as the named credential head for issues #329/#373. Do not also merge #429 onto
main— the two registries will collide (lifecycle_statusvslifecycle_state, lifespan vson_event,verifyreturn type).- After required checks finish, port two #429 ideas onto this head if they are still missing: FastAPI
lifespaninstead of deprecatedon_event, andverifyreturningcredential_idsousage_meteringcan stop storing plaintext.GET /healthbelongs to #427, not this PR. Do not copy environment scripts here.- Do not self-approve. seonghobae is requested.
Local evidence on this head: 51 focused tests, 98
test_saas_web, 100% coverage oncredential_registry.py+saas_web.py, interrogate 100%.Sent by Cursor Automation: Fix Issues
Port the two #429 ideas onto this head: verify yields the stable credential_id for usage metering, and FastAPI lifespan runs the named bootstrap instead of a deprecated startup event. Co-authored-by: Seongho Bae <seonghobae@users.noreply.github.com>
There was a problem hiding this comment.
fe1023c on this head ports the two #429 ideas without taking #429's registry: verify_api_key returns credential_id, and FastAPI lifespan runs the named bootstrap.
Keep #430 as the named credential landing for #329/#373. Do not merge #429 — lifecycle_status vs lifecycle_state, on_event vs lifespan, and the verify return type will collide.
GET /health still belongs to #427. Do not copy environment scripts here.
The buyer gap this head left open is billing: usage still stored plaintext keys. The stacked follow-up on cursor/bc-9615934f-b2f0-472b-83ac-65cd089b0a4e-b5a7 rebinds that to usage_periods(credential_id, billing_period) and 429s a spent monthly plan. Merge this PR first, then that follow-up.
Do not self-approve. seonghobae is requested.
Local evidence on fe1023c plus the follow-up commits: 133 tests across credential, usage, and saas_web; 100% coverage on those three modules; interrogate 100%.
Sent by Cursor Automation: Fix Issues


What
Closes the request-time
os.getenv("CODEC_CARVER_API_KEYS")deviation tracked in #329 and #373.credential_registry.pystores SHA-256 verifiers inapi_credentials(3NF, two-word table names) withcredential_eventsandruntime_policies.saas_webbootstraps from env once at startup, then verifiesX-API-Keyagainst the registry. Later env changes are ignored.0.0.0.0) fail closed without keys. Empty-registry local use requires explicit loopback development mode.Cache-Control: no-storeand never echo secrets.docs/doctoring/api-credential-registry.md(APA 7: NIST SP 800-63B-4, OWASP API2:2023, RFC 2104, NIST SP 800-218).Why
Org rule: env is transport into a KV, never the runtime source. Comparing raw env strings on every request also first-match short-circuited and could raise on hostile headers.
Validation
python3 -m unittest tests.test_credential_registry tests.test_saas_web.TestApiKeyAuth tests.test_job_store -q— 51 passedpython3 -m unittest tests.test_saas_web -q— 98 passed withcredential_registry.pyandsaas_web.pyat 100% coveragepython3 -m interrogate credential_registry.py saas_web.py— 100%python3 -m py_compile ... credential_registry.py— OKNotes
usagestill stores plaintext keys; that rename/migration is a follow-up after this lands.