Repository navigation
Commit 9b3c29d
fix(plugin-auth): the organization client-address read answers a revoked or expired API key with its own refusal, not a 500 (#31345)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X3cJu9y2sMJ8ikC4hnr8sRo)_
Fixes #31259
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:组织管理员读自身地址的接口 `GET /api/v1/auth/client-address`,在调用方用的 API key
已吊销、已过期或闲置失效时,改为原样返回该 key 自己的拒绝(`403` + `API_KEY_REVOKED` /
`API_KEY_EXPIRED` / `API_KEY_INACTIVE` + 原拒绝文案),不再答 `500
INTERNAL_ERROR`、也不再记一条 error 日志。
- 为什么改:这条路由的调用方解析接受 API key,但 catch 只接住了权限库不可达(503),其余一律落到 500;设置接口、REST
对同一把 key 早已答它自己的 403。
- 风险与回滚:只在 catch 里加一个分支,用 `@objectstack/core` 已导出的唯一判别器
`isApiKeyLifecycleRefusalError`,没有复制;503、401、403 与正常 200 的分支都没动。回滚即
revert。
- 先看:`packages/plugins/plugin-auth/src/auth-plugin.ts` 中
`client-address` 路由的 catch(新增的 lifecycle
分支);`packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts`
新增的顶层 describe。
- 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run
src/allowed-ip-ranges.pin.test.ts -t 'lifecycle refuses'`,预期 2 条全绿(吊销
key 答 403 `API_KEY_REVOKED`,过期 key 答 403 `API_KEY_EXPIRED`,均无 error 日志)。
- 需要你做的:无,席位落地。
## What changes
`GET /api/v1/auth/client-address` (the organization administrator's
address read, `auth-plugin.ts`) resolves its caller through
`resolveOrganizationSettingsCaller`, the settings door's composition
with `credentials: 'api-key-and-session'`. That composition propagates
the branded `ApiKeyLifecycleRefusalError` for a key that is revoked,
expired or inactive. The route's catch relayed only
`AuthzStoreUnavailableError` (503), so such a key was answered `500
INTERNAL_ERROR` ("Your client address could not be read.") with an
`[AuthPlugin] client-address failed` error log line.
The catch now relays that refusal beside the 503 arm, as the error
declares it: its `status` (403), `code` and located `message`, in the
route's existing `{ success: false, error: { code, message } }`
envelope, with no log line. The classifier is the one
`@objectstack/core` exports, `isApiKeyLifecycleRefusalError`, already
read the same way by the settings routes
(`relayApiKeyLifecycleRefusal`), plugin-sharing's shareable-objects
route and core's own-row delete routes. No copy, no new dependency edge
(plugin-auth already depends on `@objectstack/core`).
Unchanged: the 503 store-outage arm, the 401 / 403 admission arms
(`judgeOrganizationAddressReader`), the 400 closed-query arm and an
admitted organization administrator's 200 answer. The platform-admin
twin `GET /api/v1/auth/admin/client-address` reads its caller through
`gateAdmin` (session only, no API-key door), so it is not touched.
## Tests
New top-level `describe` in the route's shared harness file
`allowed-ip-ranges.pin.test.ts` ("an API key its own lifecycle refuses
is answered that refusal at the organization address read, never a
500"). It leaves the route's caller resolution unstubbed (new `caller:
'resolved'` option on the rig's `orgRead`), so the real
`resolveRequestExecutionContext` composition reads a `sys_api_key` row
from a stubbed data engine by the key's at-rest hash:
- a revoked key: `403`, `API_KEY_REVOKED`, the message's first sentence
`This API key (osk_client…) was revoked.`;
- an expired key (`expires_at` 2000-01-01): `403`, `API_KEY_EXPIRED`,
first sentence `This API key (osk_client…) expired on
2000-01-01T00:00:00.000Z.`;
- both: no `data`, the address never read (`trustsForwardedHeaders` not
called), and `logger.error` never called.
The existing org-read cases (200 answer across three transports, 401 /
403 / held 403, 503 outage, 400 closed query set) run unchanged as the
controls.
## Acceptance notes
- The `API_KEY_INACTIVE` arm goes through the same branch; it is not
pinned separately here (the card asks for revoked and expired; the
settings door's own pin covers the inactive verdict of the shared
classifier).
- The test rig gains one option (`caller: 'resolved'`); every existing
caller of `orgRead` is unchanged.
## Verification
Head `e85053f3`. The branch merged `origin/main` once before opening;
`origin/main` was still at `f5e23e2f`, so the merge was a no-op. Every
heavy run went through `scripts/pm/os-verify-lock.sh`.
<details><summary>Red first and ablation</summary>
- Red on main's route (pin commit `dc35c35e` before the fix): 2 failed,
each
`{"status":500,"body":{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Your
client address could not be read."}}}` with one `logger.error` call.
- Ablation on the fix commit, via `scripts/ablation-replace.mjs`: the
anchor `if (isApiKeyLifecycleRefusalError(error)) {` replaced by `if
(false && isApiKeyLifecycleRefusalError(error)) {` (anchor 1 → 0, blob
`5d15e7c4` → `575d2eaf`): `Tests 2 failed | 44 passed (46)`, the same
500 answer. Restored: blob == HEAD (`5d15e7c4`), `git diff HEAD` empty.
The test imports `./auth-plugin.js` from source, so no build sits
between the mutation and the run.
</details>
<details><summary>Suites, type-check, lint</summary>
- `pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2
src/allowed-ip-ranges.pin.test.ts`: exit 0, Tests 46 passed (46).
- `pnpm --filter @objectstack/plugin-auth test`: `VERDICT command-exit
0`, Test Files 240 passed (240), Tests 4248 passed, 10 skipped.
- `pnpm --filter @objectstack/plugin-auth typecheck` (with
`tsconfig.examples.json` and `check:test-typecheck`), after `pnpm
--filter @objectstack/plugin-auth build`: `VERDICT command-exit 0`.
- `pnpm exec eslint packages/plugins/plugin-auth --no-inline-config`:
exit 0, no output.
</details>
<details><summary>Derived gates</summary>
On `e85053f3`: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 75 commands. Each ran with stdin
from `/dev/null` and its exit captured before any pipe: 75 exit 0. Three
first answered exit 3 (prerequisite, nothing measured) and were re-run
to exit 0: `check-plugin-teardown-shape.mjs --self-test` (after fetching
its pinned fixture commit), `check:dual-build-cjs-loads` (after a full
`turbo run build`, `VERDICT command-exit 0`; then 122 entry points
across 72 packages load) and `check:type-check-debt` (lock queue timeout
while that build held the lock). `--ran`: "75 derived famil(ies)
accounted for — 75 run, 0 NOT-MEASURED (a DERIVED zero — all 75 recorded
an exit code and none of them is 3)".
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(Not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (scoped to plugin-auth).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (Except the maintainer
digest, which os-dev.md writes in Chinese.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5kvEQydxRtYLVRTfvo)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent d0512c2 commit 9b3c29d
3 files changed
Lines changed: 73 additions & 8 deletions
File tree
- .changeset
- packages/plugins/plugin-auth/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
49 | | - | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
92 | | - | |
| 92 | + | |
| 93 | + | |
93 | 94 | | |
94 | | - | |
| 95 | + | |
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
| |||
184 | 185 | | |
185 | 186 | | |
186 | 187 | | |
187 | | - | |
188 | | - | |
189 | | - | |
190 | | - | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
191 | 194 | | |
192 | 195 | | |
193 | 196 | | |
| |||
196 | 199 | | |
197 | 200 | | |
198 | 201 | | |
199 | | - | |
| 202 | + | |
200 | 203 | | |
201 | 204 | | |
202 | 205 | | |
| |||
552 | 555 | | |
553 | 556 | | |
554 | 557 | | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
555 | 603 | | |
556 | 604 | | |
557 | 605 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
| |||
4100 | 4101 | | |
4101 | 4102 | | |
4102 | 4103 | | |
| 4104 | + | |
| 4105 | + | |
| 4106 | + | |
| 4107 | + | |
| 4108 | + | |
| 4109 | + | |
4103 | 4110 | | |
4104 | 4111 | | |
4105 | 4112 | | |
| |||
0 commit comments