Repository navigation
fix(core): a revoked API key's refusal names the restore road instead of claiming permanence - #23991
Conversation
… of claiming permanence The 403 API_KEY_REVOKED message said "a revoked key never works again", while sys_api_key's own restore_api_key action brings the same key back. The message now says the key was revoked, that its owner or a platform administrator can restore it, and otherwise to create a new key. Code, status, carried fields and the expired and inactive messages are unchanged; the error catalog's Fix line says the same. Claude-Session: https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5527571e7d6266647be1a5dff52a6cb4a666d610 && git checkout 5527571e7d6266647be1a5dff52a6cb4a666d610
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2e1562091cf850d7f3d1057f5ebb82e9ff8cb0be 07bc7cf89878dca355c3e4159c67576e00288f9d && git checkout -B drift-repro 2e1562091cf850d7f3d1057f5ebb82e9ff8cb0be && git merge --no-ff 07bc7cf89878dca355c3e4159c67576e00288f9d
node scripts/docs-audit/affected-docs.mjs --json 2e1562091cf850d7f3d1057f5ebb82e9ff8cb0be
|
席位核对 · ✅ ACCEPT: PR #23991 at
|
Fixes #23944
Clause-②: no
What changed
A call made with a revoked API key is refused
403 API_KEY_REVOKED, and the message said "a revoked key never works again".sys_api_keydeclares arestore_api_keyrow action whose PATCH{ revoked: false }brings the same key back at once, so the product's error text contradicted its own action. The revoked branch oflifecycleRefusalOfnow reads:packages/core/src/security/api-key.ts: the revoked message only. Code, status, order and the carriedkeyId,userId,atare untouched; the expired and inactive messages (and the sharedAPI_KEY_REMEDYthey use) are unchanged.packages/core/src/security/api-key-lifecycle.test.ts: one pin (see Evidence).content/docs/api/error-catalog.mdx: theAPI_KEY_REVOKEDFix line only (declared to domain:devx in the claim)..changeset/23944-revoked-key-refusal-text.md:@objectstack/corepatch.Which edit, and why
The issue offered two: reword the refusal, or retire
restore_api_key. The lifecycle ruling on #23367 (letter A) binds expiry, inactivity and distinct located codes and is silent on restore;restore_api_keypredates it and its execution kept it, so nothing asks for a removal. The product states the same intent twice more:docs/NORTH-STAR.mdline 105 ("能撤也能恢复") and the platform checklist itemidentity-auth.api-key-ui-lifecycle("the lifecycle is reversible"). The declared action stands and the text is made to agree with it.sys-api-key.object.tsis not touched.Who can restore (the wording is this set, measured)
restore_api_keyisPATCH /api/v1/data/sys_api_key/{id}with{ revoked: false }, the same door as revoke. Measured at base2054a0888with the realPermissionEvaluatoroverdefaultPermissionSetsafterapplyManagedWriteDenies(thekernel:readystate), plus the sets' own row policies:updateonsys_api_keymember_default)sys_api_key_self, operation all,user_id == current_user.idorganization_admin+member_default)member_default(the set's own entry for the table is the managed-table deny, with neither super-user bit)admin_full_access)viewAllRecordsandmodifyAllRecords, no row policy; deliberately not a managed-deny targetviewer_readonly)The column is bounded by ADR-0092 D2's write whitelist for this table (
revokedalone). Setup's API Keys page, where an administrator sees every key, is gated onmanage_platform_settings, whichorganization_adminwithholds. So the set is the key's owner and a platform administrator, and the text says exactly that: "an administrator" would have invited an organization admin to a PATCH their row policy refuses (a member gets 404 on another user's key in the dogfood pin). The Cause line of the same catalog entry still says "an administrator" (loose usage, outside the claimed one-line surface).Exercised in CI by the existing dogfood files:
api-key-owner-revoke.dogfood.test.ts(owner restores own key; cross-owner PATCH is 404) andapi-key-revoke-lifecycle.dogfood.test.ts(admin revoke and restore).Evidence
origin/main's text and passes on this one.node scripts/ablation-replace.mjsputorigin/main's message line back on disk (anchor hit 1 time, blobbf7d10c8tofbb1c846), ranvitest run --project local src/security/api-key-lifecycle.test.ts:1 failed | 20 passed (21),expected 'This API key (osk_lifecycl…) was revo…' to contain 'Restore API Key'. Restore proven: blob after restore equals blob at HEAD (bf7d10c8),git diff HEADempty. Direction observed: red, the normal one. The subject is imported by relative path inside the package, so nodist/leg applies.api-key.test.tsandapi-key-account-posture.test.ts: 3 files, 60 tests passed. The whole@objectstack/corelocal project: 102 files, 2504 tests passed.resolveAuthzContext,code: API_KEY_REVOKEDandstatus: 403(the envelope), then that the message names the key's prefix,Restore API Key, who can restore it andPOST /api/v1/keys, and matches none of never, permanent, forever, for good.pnpm --filter @objectstack/core typecheckexit 0 (tsc, the examples program,check:test-typecheck).dispatch-gates --commands --repo objectstack-ai/objectstackderived 94 families on this head; each was run with its exit code captured before any pipe and reconciled with--ran: 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. Six first answered 3 (PREREQUISITE NOT MET, packages unbuilt) and two timed out under box contention; all eight were re-run afterturbo run build --filter='./packages/*' --filter='./packages/*/*'and with a longer timeout, and exit 0. The four roster families keyed on this card's directories (check-changeset-fixed,check:authz-resolver,check:error-code-casing,check:filter-alias-parity) andcheck:error-code-provenancealso exit 0. Families CI runs are cited from this PR's check-runs, not re-run.content/docs/releases/for "never works again": before the edit, 2 hits (api-key.tsand the catalog Fix line), control "was revoked, and a revoked key" 1 hit inapi-key.ts; a wrapped-line variant search found no third. After the edit the only hits quote it on purpose (the changeset's FROM text and a comment in the pin). Nothing in the repository parses the message:API_KEY_REVOKEDconsumers match on the code, and the dogfood probes assert the code.Acceptance notes
Not filed; noted for the reader.
content/docs/ai/connect-mcp.mdx("The remedy is the same for all three: create a new key") andcontent/docs/api/index.mdx("the remedy is a new key") are consistent with rotation, which works for all three codes; they simply do not mention restore for the revoked one. Outside the claimed surface, left alone.identity-auth.api-key-ui-lifecyclestill expects "401" after revoke, while the runtime answers403 API_KEY_REVOKEDsince the lifecycle ruling; its sibling item for the lifecycle policy has the 403. Test-spec drift outside the claimed surface.revokeSessionsAndKeys) is restorable by its owner through the same row action, because restore is not conditioned on why the flag was set. The message cannot know the reason and states only the capability. Whether a compromise-response revocation should be owner-restorable is a product question this card does not decide.viewer_readonlycannot restore (update is false for that set); the text states the general capability.Deviations
2054a0888.origin/mainmoved four commits during the run (cli/runtime mkdir, objectui pin, a dogfood pin, platform-objects menu); none touches this diff's four paths.dispatch-gatesreports three derivation inputs stale againstorigin/main(check-cli-test-child-env.mjs,spec-shape-retirements.json,sdui-manifest.record.json), none of them in this card's derived families.Generated by Claude Code