Conversation
Legacy API keys grant no access: the data proxy has not accepted them since source-cooperative/data.source.coop#116, and this API has accepted only the proxy's signed tokens and session cookies since #283. Yet both create routes still minted plaintext secrets, and DELETE /api/v1/api-keys/{access_key_id} authorized the caller, deleted nothing, and answered "API key deleted successfully". POST /api/v1/accounts/{account_id}/api-keys and POST /api/v1/products/{account_id}/{repository_id}/api-keys now answer 410 Gone with a JSON error pointing to service-account API keys, without reading the session or the body. GET /api/v1/api-keys/{access_key_id}/auth answers 410 too: its only caller was the pre-Workers proxy, which authenticated with a credential this API no longer accepts, and it was the one route that returned a secret_access_key. The two list routes keep serving redacted keys for one release with Deprecation (RFC 9745) and Sunset (RFC 8594) headers, so owners can find their keys. DELETE now deletes the row, carries the same headers, and answers 500 rather than success when the delete fails. Code with no remaining caller goes: generateAccessKeyID and generateSecretAccessKey, APIKeyRequestSchema, the api_key:create action and its rule, and the table client's create, update and upsert. The served OpenAPI components drop APIKey and APIKeyRequest, and the ApiKeyAuth scheme that described the retired credential. The api-keys table, its CDK definition and its local seed stay until the routes are removed. Part of #549. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R1eiTse4416N6uTgAy4Ddd
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Claude finished @alukach's task in 1m 19s —— View job Review
✅ No blocking issues — safe to merge. Went through the full diff (
Nothing here reads as a correctness, security, or best-practice problem. Simplify (ponytail): nothing to flag — this PR is net −316 lines, the new Docs: The description already covers this per CLAUDE.md's requirement — it states docs.source.coop has no page describing legacy keys (checked 💰 Estimated review cost: $0.57 · 1m19s · 25 turns |
What I'm changing
The legacy API keys in the
api-keystable grant no access. The data proxy stopped accepting them in source-cooperative/data.source.coop#116, and this API has accepted only the proxy's signed tokens and session cookies since #283. Six routes still touch the table, though: two POSTs mint new plaintext secrets, andDELETE /api/v1/api-keys/{access_key_id}authorizes the caller, deletes nothing, and answers "API key deleted successfully", so every key a user believes they revoked is still in the table in plaintext.This PR retires those routes (paths under
/api/v1):POST /accounts/{account_id}/api-keysPOST /products/{account_id}/{repository_id}/api-keysGET /accounts/{account_id}/api-keysDeprecationandSunsetheadersGET /products/{account_id}/{repository_id}/api-keysDeprecationandSunsetheadersGET /api-keys/{access_key_id}/authDELETE /api-keys/{access_key_id}DeprecationandSunsetheaders; 500 rather than success if the delete failsEvery 410 answers
{"error": "Legacy API keys are retired and grant no access. For software that needs access, create a service account in your account settings and issue it an API key."}.Code with no remaining caller goes:
generateAccessKeyIDandgenerateSecretAccessKey(all ofsrc/lib/actions/crypto.ts),APIKeyRequestSchema, theapi_key:createaction with its authz rule and test, and the table client'screate,updateandupsert. The table client keepsfetchByIdandlistByAccountfor the routes that still serve, and gainsdelete. No UI ever managed these keys, so there is no UI or Storybook change.The table stays.
deploy/,scripts/init-local.ts,fixtures/api-keys.jsonand the fixture loader that seeds it are untouched; dropping the table is sequenced after the routes are gone (see Follow-ups).Decisions to flag
The list GETs keep serving for one release, with
DeprecationandSunsetheaders. They return only redacted fields, so serving them costs nothing, and they are how an owner finds a key's id in order to delete it. Answering 410 at once would break any script without notice and leave owners no way to find their keys. The headers areDeprecation: @1790294400(2026-09-25; RFC 9745) andSunset: Sun, 01 Nov 2026 00:00:00 GMT(RFC 8594). The sunset date is my choice: about five weeks out, which on the recent release cadence (v1.5.0 on 4 Aug to v1.6.1 on 24 Sep, with gaps of 9 to 25 days) spans the release carrying this PR and the one after. The follow-up that removes the routes should not ship before it; if this PR merges late, move the date so the window still spans a release.GET /api-keys/{access_key_id}/authanswers 410 now, which departs from the issue's "deprecation response for the GET routes". It is not a listing. It was the pre-Workers proxy's credential lookup, and it returns a key'ssecret_access_keyin plaintext to an admin. Its only caller was removed in source-cooperative/data.source.coop#116, and that caller authenticated with a raw API key header thatgetApiSessionstopped accepting in #283, so no client can depend on it. A deprecation window would keep serving plaintext secrets to protect a caller that cannot exist.DELETE deletes, rather than answering 410. It is the smallest honest fix, and with the list routes it lets owners purge their plaintext keys during the window instead of waiting for the table drop. It keeps its authorization (
api_key:revoke) and its 200 body, which is now true. That rule still refuses a non-admin on a key markeddisabled; no route sets the flag, and an admin can delete any such key. Rows deleted this way are gone before the audit below, though point-in-time recovery keeps them restorable for its retention window.fix, notchore. release-please leaveschoreout of the changelog, and the release notes are where the deprecation window gets announced.OpenAPI. Each changed route's
@openapiJSDoc marks itdeprecated: trueand describes its new behaviour; the account route gains the JSDoc it lacked. That JSDoc does not reach the served spec:/api/openapihas servedpaths: {}since swagger-jsdoc was removed in #369. The served spec changes only in its components.APIKey, which publishedsecret_access_key, andAPIKeyRequestgo;RedactedAPIKeystays for the list routes; and theApiKeyAuthsecurity scheme goes, because it described the<access-key-id> <secret-access-key>header this API stopped accepting in #283. No replacement scheme is added here.The 410 points to service-account API keys, which ship with #570. Service accounts are already on
main(#567). If this merges before #570, the pointer is one step ahead until #570 lands.A quirk left alone:
GET /accounts/{account_id}/api-keyslists the signed-in account's keys whateveraccount_idnames, as it always has. Its new JSDoc says "the signed-in account's". Fixing a route that is going away isn't worth it.How you can test it
What I ran on this branch:
npm run type-check: clean.npx jest api-keys src/lib/api/authz.test.ts "src/app/api/v1/products/\[account_id\]/route.test.ts" "src/app/api/v1/products/\[account_id\]/\[repository_id\]/route.test.ts" --forceExit: 8 suites, 80 tests pass.npx jest --forceExit(the whole suite): 79 suites, 813 tests pass.npm run lint: exits 0. It prints warnings only, and none on lines this PR adds.await apiKeysTable.delete(...)line, or only itsawait, fails the DELETE suite.The tests cover: 410 on each POST and on
/auth; the list GETs still return redacted keys and carry both headers; DELETE deletes the row when authorized, deletes nothing when the caller may not (401) or the key is missing (404), and answers 500 when the delete fails;APIKeysTable.deletesends aDeleteCommandkeyed byaccess_key_idto the right table.Not run:
npm run build(it needs AWS credentials), and nothing against the preview, staging or a local DynamoDB.By hand, from a signed-in browser console on the preview:
(await fetch("/api/v1/accounts/<you>/api-keys", { method: "POST" })).statusgives 410;(await fetch("/api/v1/accounts/<you>/api-keys")).headers.get("Sunset")gives the sunset date; and afterfetch("/api/v1/api-keys/<id>", { method: "DELETE" })the key no longer appears in the list.Docs and ADRs
mainfor API-key instructions. No page describes the legacy keys:docs/using-source/data-upload.mdcovers STS session credentials only, and already tells users not to request permanent access keys. Nothing is invalidated.mainand as revised in docs(adr): revise ADR-013 — API keys are opaque secrets resolved by the platform data.source.coop#234. ADR-013's context note says theapi-keysendpoints are legacy, unrelated to its design, and not accepted by the proxy. That still holds. This PR moves no decision, so no ADR changes.profile/README.mdinsource-cooperative/.github) listsapi-keys: Stores API keys for authentication.Follow-ups
These follow #549's sequence, each as its own change after this one ships:
APIKeySchemaandRedactedAPIKeySchema, the remainingapi_key:*and*:listAPIKeysauthz actions,fixtures/api-keys.jsonand its loader.deploy/lib/database-construct.ts, and the Vercel grant indeploy/lib/api-stack.ts) and fromscripts/init-local.ts. Production setsRETAIN, with point-in-time recovery on, so then delete the table out of band.Related
Part of #549: the routes are not gone yet, and the table drop, delete and audit remain. Part of #491. This re-cuts the stale draft #374 from current
maininstead of rebasing it; #374 is left as it is.🤖 Generated with Claude Code
https://claude.ai/code/session_01R1eiTse4416N6uTgAy4Ddd