Repository navigation
Commit 92493e3
fix(rest): security/explain refuses an empty object with 400 VALIDATION_FAILED instead of 500 (#27892)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D2T3MYD69CaE3sEdpqrhNgW)_
Fixes #27719
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:管理员调用访问解释接口(`POST`/`GET /api/v1/security/explain`)时,`object`
传空字符串现在直接回 400 `VALIDATION_FAILED`,不再回 500 `EXPLAIN_FAILED`。
- **为什么改**:这是调用方能自己改正的错误请求,却被当成服务器故障回给客户端、也按故障记日志;与上一张卡(空 `userId` 回
400)同一道门、同一种处理。
- **风险与回滚**:风险低——本来就失败的请求只是从 500 变成 400,合法请求一字节不变;回滚即 revert 本 PR。
- **先看**:`packages/rest/src/security-routes.ts`(`EMPTY_KEY_REMEDIES` 表与
handler 里那一个循环)、`packages/rest/src/security-routes.test.ts`(`[#27719]`
一组)。
- **两分钟验证**:`pnpm --filter @objectstack/rest exec vitest run --project
local src/security-routes.test.ts`,预期 22 个用例全部通过。
- **需要你做的**:无,席位落地。
## What changes
`ExplainRequestSchema.object` is `z.string()`, so an empty `object`
passed the schema, reached the explain engine, and the engine's plain
`Error('[Security] explain: request.object is required')` fell to the
route's catch-all `500 EXPLAIN_FAILED` arm.
The route now refuses it `400 VALIDATION_FAILED`, in its own envelope
and before any evaluation, through the check that already refused an
empty `userId`. That check is now one table, `EMPTY_KEY_REMEDIES`
(module-private, no export changes), with one row per key and the remedy
its refusal names. The handler loops over it once, so the `userId`
refusal is not copied. The `userId` message is byte-identical to before.
Per the triage ruling, the refusal is graded at the route.
`packages/spec` is not touched. Moving both keys into
`ExplainRequestSchema` (`min(1)`) is a separate spec narrowing that this
PR does not grade.
Files: `packages/rest/src/security-routes.ts`,
`packages/rest/src/security-routes.test.ts`,
`.changeset/27719-explain-empty-object.md` (`@objectstack/rest` patch).
## Tests
New `[#27719]` block in `packages/rest/src/security-routes.test.ts`. Its
fake service throws the engine's own plain error for an empty object:
- `POST` with `object: ""` gets 400, `success: false`, `error.code`
`VALIDATION_FAILED`, and no verdict. The engine is never called.
- `GET ?object=` is refused the same way.
- A request that names an object is still explained, and the engine gets
exactly the request it was sent (`POST` with `update`, and `GET`
defaulting to `read`).
The `[#27442]` empty-`userId` block runs unchanged and passes.
Run on head `4f5150d8` (`objectstack-ai/objectstack`):
<details><summary>Red first, on the unfixed route: 2 failed | 20
passed</summary>
```
pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 src/security-routes.test.ts
× POST object "" is refused 400 VALIDATION_FAILED in the route envelope, and the engine is never called
× GET ?object= is refused the same way
AssertionError: expected 500 to be 400 // Object.is equality
Tests 2 failed | 20 passed (22)
```
</details>
<details><summary>Green after the fix: 22/22; the whole package 347
files / 6737 tests; typecheck exit 0</summary>
```
src/security-routes.test.ts Tests 22 passed (22)
pnpm --filter @objectstack/rest test Test Files 347 passed (347) · Tests 6737 passed | 332 skipped (7069) · VERDICT command-exit 0
pnpm --filter @objectstack/rest typecheck VERDICT command-exit 0 (check:test-typecheck: 0 files / 0 errors)
```
</details>
<details><summary>Ablation: delete the object row from
EMPTY_KEY_REMEDIES, and both new refusal pins go red; restored
byte-identical</summary>
```
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 73f00e0 -> 88505ad43e41
× POST object "" is refused 400 VALIDATION_FAILED in the route envelope, and the engine is never called
× GET ?object= is refused the same way
Tests 2 failed | 20 passed (22)
ablation-replace: ok restored: blob == HEAD (73f00e0) and `git diff HEAD` is empty
```
The route test imports `./rest-server` relatively, so it runs against
`src/` and no dist rebuild was needed for this leg.
</details>
<details><summary>End to end: real showcase boot as the seed admin,
through a throwaway dogfood probe that was not committed</summary>
```
main 9ae6384:
POST object="" operation=read status=500 {"success":false,"error":{"code":"EXPLAIN_FAILED","message":"[Security] explain: request.object is required"}}
GET ?object=&operation=read status=500 (same body)
this branch 4f5150d (rest rebuilt):
POST object="" operation=read status=400 {"success":false,"error":{"code":"VALIDATION_FAILED","message":"Invalid explain request — object is empty. Give the name of the object whose access to explain."}}
GET ?object=&operation=read status=400 (same body)
controls, unchanged on both: POST object=showcase_task -> 200 allowed:true · POST userId="" -> 400 VALIDATION_FAILED (the userId message is byte-identical)
```
</details>
## Acceptance notes
- The empty `object` refusal names its remedy ("Give the name of the
object whose access to explain."). The test does not pin that wording:
it asserts the code, the status, and that the engine was never called.
- A missing `object` (the key absent) is still refused by the request
schema's own 400. This PR leaves it alone.
## Verification
Local subset on head `4f5150d8`: the gates derived by `node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from 3 changed paths (+93 / -10), run one
after another with each exit code captured before any pipe.
<details><summary>60 derived, 60 run, 60 exit 0; --ran reconciliation: 0
NOT-MEASURED, 0 UNRUN</summary>
- `pnpm check:dual-build-cjs-loads` first exited 3 (PREREQUISITE NOT
MET: 8 unrelated packages had no `dist/`). After building them it exited
0: 113 entry points across 70 packages load. The reconciliation records
that rerun.
- `--ran` with `:: exit N` per line: "60 derived famil(ies) accounted
for — 60 run, 0 NOT-MEASURED (a DERIVED zero — all 60 recorded an exit
code and none of them is 3)".
- `npx eslint --no-inline-config` on both touched source files: exit 0.
- Not run locally, and left to CI: `test:repo`, the full-tree `pnpm
lint`, and the type-check lanes and path-scheduled jobs the derivation
lists outside its total. No dogfood battery: the title has no `!` and
the clause has no `(narrowing)` arm.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4d1Np3hU7RQnA13YZcL
Co-authored-by: Claude <noreply@anthropic.com>1 parent 88d6020 commit 92493e3
3 files changed
Lines changed: 93 additions & 10 deletions
File tree
- .changeset
- packages/rest/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
202 | 202 | | |
203 | 203 | | |
204 | 204 | | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
205 | 256 | | |
206 | 257 | | |
207 | 258 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
103 | 114 | | |
104 | 115 | | |
105 | 116 | | |
| |||
204 | 215 | | |
205 | 216 | | |
206 | 217 | | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
217 | 235 | | |
218 | 236 | | |
219 | 237 | | |
| |||
0 commit comments