Skip to content

docs(qa): re-point identity-auth.api-key-ui-lifecycle's revoke oracle to 403 API_KEY_REVOKED - #24012

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-23992-api-key-ui-revoke-oracle
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-23992-api-key-ui-revoke-oracle

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23992
Clause-②: no

The platform-checklist item identity-auth.api-key-ui-lifecycle expected a revoked API key's next x-api-key call to answer 401. Since the API-key lifecycle (#23367 ruling A, executed by #23541), a recognised but revoked key is refused 403 with error.code API_KEY_REVOKED. Only an unknown key still gets the generic 401. So the item scored a correct runtime red. This PR re-points step 4 and acceptance clause 2 to the shape that shipped, bumps revision 1 → 2, and appends a history row naming #23992, #23367 and #23541. The diff is docs/qa/platform-checklist/areas/identity-auth.json only (+10 / −8, one entry). There is no product code change.

The clause is not loosened. A revoked key that answers 200 or serves any row still fails it. So does a post-revoke refusal that is the generic 401, or a 403 carrying any other code. Step 5 (restore → 200) is unchanged. The oracle scores the status, the code and the absence of rows. It pins none of the refusal message's wording, because the lifecycle owner rewords that text (#23944, PR #23991). This matches what PR #24000 did for the MCP revocation clauses.

Old and new text

Step 4 (steps[4])

  • Old: revoke the key via the row action revoke_api_key (PATCH /api/v1/data/sys_api_key/{id} bodyExtra {revoked:true}); immediately re-call the data route with x-api-key and capture the 401
  • New: revoke the key via the row action revoke_api_key (PATCH /api/v1/data/sys_api_key/{id} bodyExtra {revoked:true}); immediately re-call the data route with x-api-key and capture the 403 whose body's error.code is API_KEY_REVOKED and which carries no rows (a recognised key its own lifecycle refuses — only an UNKNOWN key gets the generic 401)

Acceptance clause 2 (acceptance[2])

  • Old clause: revoke is immediate and enforced server-side: after revoke_api_key the same x-api-key call 401s on the very next request
  • Old verify: the post-revoke x-api-key request returns 401
  • Old evidence: the revoke PATCH response + the 401
  • New clause: revoke is immediate and enforced server-side: after revoke_api_key the same x-api-key call is refused on the very next request with the key's own lifecycle refusal — 403, error.code API_KEY_REVOKED, and no rows. Since #23367 (executed by #23541) a recognised but revoked key is refused 403 API_KEY_REVOKED; the generic 401 answers only a key the server does not recognise at all. The refusal's message is the key's own lifecycle message and its wording is NOT pinned here: the code is what a client branches on, and the lifecycle owner rewords that text (#23944)
  • New verify: the post-revoke x-api-key request returns HTTP 403 and its body's error.code is API_KEY_REVOKED, with no record rows in the body (packages/core/src/security/api-key.ts#lifecycleRefusalOf decides it; the data routes answer it through packages/rest/src/rest-server.ts#enforceAuth). Score the status, the code and the absence of rows, never the message's wording. A 200 or any served row, a generic 401, or a 403 carrying any other code is a FAIL — the last means the key was accepted and something later refused
  • New evidence: the revoke PATCH response + the 403 API_KEY_REVOKED refusal body

Also in the same entry

  • negative[1] keeps its text and adds the other failure direction: a post-revoke refusal that is the generic 401, or carries any code other than API_KEY_REVOKED, is a FAIL.
  • source gains one row: packages/core/src/security/api-key.ts#lifecycleRefusalOf + packages/rest/src/rest-server.ts#enforceAuth. Both anchors resolve under check-platform-checklist.
  • history gains the revision: 2 row (ref: #23992).

The source lines (at origin/main e18b23fac)

  • packages/core/src/security/api-key.ts:217: const API_KEY_REVOKED_REFUSAL = { code: 'API_KEY_REVOKED', status: 403 } as const;
  • packages/core/src/security/api-key.ts:299 starts lifecycleRefusalOf. Its first arm (lines 306-321), if (row.revoked === true || row.revoked === 1), returns code: API_KEY_REVOKED_REFUSAL.code (line 308) with the located message. packages/core/src/security/resolve-authz-context.ts:501 raises it as the branded ApiKeyLifecycleRefusalError, whose status is API_KEY_LIFECYCLE_REFUSAL_STATUS = 403 (api-key.ts:228).
  • packages/rest/src/rest-server.ts:3493-3495: the context resolver remembers the branded refusal beside the request. Lines 2478-2481: enforceAuth answers res.status(lifecycle.status).json({ error: { code: lifecycle.code, message: lifecycle.message } }). A request with no remembered refusal falls through to the generic anonymous 401.
  • Permanent pin of that relay: packages/rest/src/rest-api-key-lifecycle.pin.test.ts, "revoked, expired and inactive answer 403 with three distinct codes".

Re-run record: #24011

QA run · identity-auth.api-key-ui-lifecycle (1/1) · e1e711ae · 2026-10-16 · 1 PASS / 0 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN. node scripts/qa/qa-rollup.mjs parses the title (row #24011, 1/1, sha e1e711ae).

The run used a real showcase boot built from this branch's tree (e1e711ae2; product tree origin/main e18b23fac). pnpm build ran 75/75 tasks. The console was built at the .objectui-sha pin f8267df93aba with pnpm objectui:build, and check:console-sha was green. The server ran on its own port (41873) and its own file DB, and was stopped by PID afterwards. Personas were the seeded admin and members minted by invite + sign-up. The browser legs were driven through Playwright (Chromium 141).

# clause verdict evidence (short)
0 secret shown once pass mint 201 with a raw key; get-by-id (owner and admin) and the mine list carry no key field, only prefix; the dialog showed the raw value, and after Done it is gone from the page
1 key = owner's view pass showcase_task via x-api-key 200, 10 rows, the same id set as the owner's session. On showcase_inquiry the admin sees 3 rows, the owner's session 0 and the owner's key 0
2 revoke → 403 API_KEY_REVOKED pass PATCH {"revoked": true} 200. The very next read answered 403 with body {"error": {"code": "API_KEY_REVOKED", "message": …}} and no rows, and a repeat answered the same. Through the console's Revoke API Key row action, the request body was {"revoked":true} and the next two reads answered 403 API_KEY_REVOKED. Control: an unknown key answers 401 UNAUTHENTICATED
3 restore → 200 pass PATCH {"revoked": false} 200, then a read answered 200 with 10 rows. The console's Restore API Key row action gave the same result
4 mine-view scoping pass The second member's mine list and unfiltered list both exclude the owner's key, and get-by-id answers 404. With two owners, each one's mine list shows only its own key. The admin's all_keys list (API and Setup → API Keys) shows every owner's key
5 dialog + list screenshots pass The dialog shows "not be shown again", the secret and Copy. The re-opened My Keys list shows name + prefix rows only

Gates (at e1e711ae2, the final commit)

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 13 commands. Each was run with its exit code captured before any pipe, and all 13 exited 0: check-ci-filter-parity, check-closing-keyword-parity (+ --self-test), check-comment-mask-corpus, @objectstack/lint check:doc-formula-expressions, check:cross-package-test-inputs, check:doc-authoring, check:driver-memory-census, check:gitlink-declared, check:nul-bytes, check:platform-checklist ("OK — 16 areas, 327 items …; symbol anchors: 834/844 resolved …"), check:refd-timer-probe, check:watch-hint-literal.
  • --ran reconciliation: "13 derived famil(ies) accounted for — 13 run, 0 NOT-MEASURED (a DERIVED zero — all 13 recorded an exit code and none of them is 3)".
  • node scripts/pm/check-governed-merges.mjs --branch claude/issue-23992-api-key-ui-revoke-oracle: "0 of 1 path(s) hit the register … NOT governed"; 18 changed lines.
  • This is a ledger-only change and publishes nothing, so there is no changeset.

Acceptance notes

  • PR fix(core): a revoked API key's refusal names the restore road instead of claiming permanence #23991 (API key refusal API_KEY_REVOKED says "a revoked key never works again", but the declared restore_api_key action brings the same key back to 200 #23944) landed before this branch was cut. The card suggested also checking that the message names "Restore API Key". This PR does not do that, because the dispatch ruling says the oracle never scores the prose. The live message does name the Restore API Key action, but that was observed, not scored.
  • Two dogfood files, api-key-revoke-lifecycle and api-key-owner-revoke, accept a generic 401 as well as 403 API_KEY_REVOKED for a revoked key. The strict 403 + code relay is pinned only at unit level (packages/rest/src/rest-api-key-lifecycle.pin.test.ts), and this item has no automated.ref. Carrier: none.
  • The item's source row for sys-api-key.object.ts still says apiMethods ['get','list'], but the object declares ['get', 'list', 'update']. This is a stale annotation that scores nothing, so it is outside this card's class and was left untouched. Carrier: none.
  • Scope: negative[1] and source[] are inside the claimed entry but outside the claim's parenthetical field list. Both carry the ruling's "do not loosen" rule and the citation it asks for.

Generated by Claude Code

… to 403 API_KEY_REVOKED

Step 4 and acceptance[2] expected a revoked key's next x-api-key call to
answer 401. Since the API-key lifecycle, a recognised revoked key is
refused 403 with error.code API_KEY_REVOKED, and only an unknown key gets
the generic 401, so the item scored a correct runtime red. The step and
the clause now capture the status, the code and the absence of rows; the
refusal's wording is not pinned. A 200, a generic 401 or a 403 with any
other code still fails. Revision 1 -> 2 with a history row.

Claude-Session: https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 16, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · PR #24012 (draft, head e1e711ae2, Fixes #23992) · domain:devx seat 2 · session_016PVjjuVPte5XEJvaQx1E4t · 2026-10-16T18:41Z

Checked on GitHub and against the sources, not on the report's say-so.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 18:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 18:44
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit fd0e72a Oct 16, 2026
43 of 44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23992-api-key-ui-revoke-oracle branch October 16, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants