Repository navigation
Commit 4ffdebe
fix(core): a revoked API key's refusal names the restore road instead of claiming permanence (#23991)
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_key`
declares a `restore_api_key` row 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 of `lifecycleRefusalOf`
now reads:
> This API key (PREFIX...) was revoked. Its owner or a platform
administrator can restore it (the Restore API Key action on the API Keys
list); otherwise create a new key (POST /api/v1/keys, or the API Keys
page) and replace this one wherever it is configured.
- `packages/core/src/security/api-key.ts`: the revoked message only.
Code, status, order and the carried `keyId`, `userId`, `at` are
untouched; the expired and inactive messages (and the shared
`API_KEY_REMEDY` they use) are unchanged.
- `packages/core/src/security/api-key-lifecycle.test.ts`: one pin (see
Evidence).
- `content/docs/api/error-catalog.mdx`: the `API_KEY_REVOKED` **Fix**
line only (declared to domain:devx in the claim).
- `.changeset/23944-revoked-key-refusal-text.md`: `@objectstack/core`
patch.
## 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_key`
predates it and its execution kept it, so nothing asks for a removal.
The product states the same intent twice more: `docs/NORTH-STAR.md` line
105 ("能撤也能恢复") and the platform checklist item
`identity-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.ts` is not touched.
## Who can restore (the wording is this set, measured)
`restore_api_key` is `PATCH /api/v1/data/sys_api_key/{id}` with `{
revoked: false }`, the same door as revoke. Measured at base `2054a0888`
with the real `PermissionEvaluator` over `defaultPermissionSets` after
`applyManagedWriteDenies` (the `kernel:ready` state), plus the sets' own
row policies:
| principal | `update` on `sys_api_key` | row scope | may restore |
|:---|:---|:---|:---|
| member (`member_default`) | true | `sys_api_key_self`, operation all,
`user_id == current_user.id` | own key only |
| organization admin (`organization_admin` + `member_default`) | true as
the union with `member_default` (the set's own entry for the table is
the managed-table deny, with neither super-user bit) | same self policy
| own key only; not another user's |
| platform admin (`admin_full_access`) | true | wildcard with
`viewAllRecords` and `modifyAllRecords`, no row policy; deliberately not
a managed-deny target | any key |
| read-only (`viewer_readonly`) | false | select-only self policy | none
|
The column is bounded by ADR-0092 D2's write whitelist for this table
(`revoked` alone). Setup's API Keys page, where an administrator sees
every key, is gated on `manage_platform_settings`, which
`organization_admin` withholds. 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) and `api-key-revoke-lifecycle.dogfood.test.ts`
(admin revoke and restore).
## Evidence
- **Red then green.** The pin fails on `origin/main`'s text and passes
on this one.
- Red: `node scripts/ablation-replace.mjs` put `origin/main`'s message
line back on disk (anchor hit 1 time, blob `bf7d10c8` to `fbb1c846`),
ran `vitest 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 HEAD` empty. Direction
observed: red, the normal one. The subject is imported by relative path
inside the package, so no `dist/` leg applies.
- Green: same file on this head, with `api-key.test.ts` and
`api-key-account-posture.test.ts`: 3 files, 60 tests passed. The whole
`@objectstack/core` local project: 102 files, 2504 tests passed.
- **The pin** asserts, through `resolveAuthzContext`, `code:
API_KEY_REVOKED` and `status: 403` (the envelope), then that the message
names the key's prefix, `Restore API Key`, who can restore it and `POST
/api/v1/keys`, and matches none of never, permanent, forever, for good.
- **Typecheck:** `pnpm --filter @objectstack/core typecheck` exit 0
(`tsc`, the examples program, `check:test-typecheck`).
- **Gates:** `dispatch-gates --commands --repo
objectstack-ai/objectstack` derived 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 after `turbo 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`) and
`check:error-code-provenance` also exit 0. Families CI runs are cited
from this PR's check-runs, not re-run.
- **No other copy of the sentence.** Re-grep of tracked files outside
`content/docs/releases/` for "never works again": before the edit, 2
hits (`api-key.ts` and the catalog Fix line), control "was revoked, and
a revoked key" 1 hit in `api-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_REVOKED` consumers 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") and `content/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.
- The checklist item `identity-auth.api-key-ui-lifecycle` still expects
"401" after revoke, while the runtime answers `403 API_KEY_REVOKED`
since the lifecycle ruling; its sibling item for the lifecycle policy
has the 403. Test-spec drift outside the claimed surface.
- A key revoked by an administrator's forced password reset
(`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.
- An owner whose only resolved set is `viewer_readonly` cannot restore
(update is false for that set); the text states the general capability.
- Live boot of an administrator restoring another user's key was not
run: that row of the table is the evaluator's reading of the sets and is
not exercised by an existing dogfood file.
## Deviations
- Commit trailers are the model-free pair AGENTS.md requires; the
harness attribution reminder asked for a model-named trailer, which the
pre-push hook refuses, so it was not used.
- Base is `2054a0888`. `origin/main` moved four commits during the run
(cli/runtime mkdir, objectui pin, a dogfood pin, platform-objects menu);
none touches this diff's four paths. `dispatch-gates` reports three
derivation inputs stale against `origin/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](https://claude.ai/code/session_01TQLy3Yq6vF4SDpTXdr3Uwi)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 61a6809 commit 4ffdebe
4 files changed
Lines changed: 51 additions & 3 deletions
File tree
- .changeset
- content/docs/api
- packages/core/src/security
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
446 | 446 | | |
447 | 447 | | |
448 | 448 | | |
449 | | - | |
| 449 | + | |
450 | 450 | | |
451 | 451 | | |
452 | 452 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
| |||
298 | 300 | | |
299 | 301 | | |
300 | 302 | | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
306 | 306 | | |
307 | 307 | | |
308 | 308 | | |
309 | | - | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
310 | 319 | | |
311 | 320 | | |
312 | 321 | | |
| |||
0 commit comments