Repository navigation
Commit fd0e72a
docs(qa): re-point identity-auth.api-key-ui-lifecycle's revoke oracle to 403 API_KEY_REVOKED (#24012)
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) · e1e711a ·
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 #23991 (#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](https://claude.ai/code/session_016PVjjuVPte5XEJvaQx1E4t)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent fd9664f commit fd0e72a
1 file changed
Lines changed: 10 additions & 8 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
604 | 604 | | |
605 | 605 | | |
606 | 606 | | |
607 | | - | |
| 607 | + | |
608 | 608 | | |
609 | 609 | | |
610 | 610 | | |
| |||
620 | 620 | | |
621 | 621 | | |
622 | 622 | | |
623 | | - | |
| 623 | + | |
624 | 624 | | |
625 | 625 | | |
626 | 626 | | |
| |||
639 | 639 | | |
640 | 640 | | |
641 | 641 | | |
642 | | - | |
| 642 | + | |
643 | 643 | | |
644 | | - | |
645 | | - | |
| 644 | + | |
| 645 | + | |
646 | 646 | | |
647 | 647 | | |
648 | 648 | | |
| |||
665 | 665 | | |
666 | 666 | | |
667 | 667 | | |
668 | | - | |
| 668 | + | |
669 | 669 | | |
670 | 670 | | |
671 | 671 | | |
| |||
674 | 674 | | |
675 | 675 | | |
676 | 676 | | |
677 | | - | |
| 677 | + | |
| 678 | + | |
678 | 679 | | |
679 | 680 | | |
680 | | - | |
| 681 | + | |
| 682 | + | |
681 | 683 | | |
682 | 684 | | |
683 | 685 | | |
| |||
0 commit comments