Repository navigation
Commit 9859e4e
rest: the 20 REST write routes answer a sandboxed hook's refusal in the hook's own words (#22763)
Fixes #22719
Clause-②: no
## What this does
Twenty write routes in `@objectstack/rest` built their error answer by
hand from `.message`. For a sandboxed hook's refusal, `.message` is the
QuickJS debug wrapper (`hook 'NAME' threw: Error: SENTENCE`), so these
routes shipped the wrapper while the other 24 write doors answer the
hook's own sentence. Each family now answers through the shared read the
conforming doors use (triage `6101568631`), with no local unwrap and no
per-route text:
- **Sharing rules (3), suggested bindings and the overlay discard (3),
approvals (9)**, in `rest-server.ts`. Each family's own arms run first,
unchanged. Where its 500 arm used to relay `.message`, it now asks
`classifiedRefusalAnswer` first. That is the `/data` door's
classification, and it reads `sandboxBusinessMessage`. The answer is
re-dressed in the nested envelope through the family's own emitter. The
re-dress is one function, `nestedRefusalEnvelope` in
`error-response.ts`. It applies the same derivation the record-share
family's `respondSharingError` applies inline: `code` falls back to the
catalog's floor for the status, and `declaredCode` and `userMessage`
come from the classification.
- **`POST /packages/publish` (1)**, in `package-routes.ts`. This door
runs the dispatcher twin's rule (`resolveThrownHttpError`). It now takes
the status it falls back to from `classifiedRefusalAnswer`, and the
sentence from `sandboxBusinessMessage`. `code`, `declaredCode`,
`userMessage` and `details` stay this door's own mapping, so nothing it
already answered moves.
- **External datasources (4)**, in `external-datasource-routes.ts`. Both
catch arms read the sentence through `sandboxBusinessMessage`. Status
and code do not move (see "Not chosen" below).
The pin's `ANSWERS_THE_WRAPPER` map is empty. Its 20 rows sit in
`ANSWERS_THE_SENTENCE`, each pinned at the status and code it answers,
and §3 now holds the map empty.
## Rows whose status or code changed (a sandboxed refusal that declares
nothing)
| row | before | after |
|---|---|---|
| `POST /api/v1/sharing/rules` | 500 `RULE_DEFINE_FAILED`, flat | 400
`VALIDATION_ERROR`, nested |
| `DELETE /api/v1/sharing/rules/:idOrName` | 500 `RULE_DELETE_FAILED`,
flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/sharing/rules/:idOrName/evaluate` | 500
`RULE_EVALUATE_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/security/suggested-bindings/:id/confirm` | 500
`SUGGESTION_CONFIRM_FAILED` | 400 `VALIDATION_ERROR` |
| `POST /api/v1/security/suggested-bindings/:id/dismiss` | 500
`SUGGESTION_DISMISS_FAILED` | 400 `VALIDATION_ERROR` |
| `POST /api/v1/security/permission-sets/:id/discard-overlay` | 500
`INTERNAL` | 400 `VALIDATION_ERROR` |
| `POST /api/v1/approvals/requests/:id/approve` | 500
`APPROVAL_APPROVE_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/reject` | 500
`APPROVAL_REJECT_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/recall` | 500
`APPROVAL_RECALL_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/revise` | 500
`APPROVAL_REVISE_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/resubmit` | 500
`APPROVAL_RESUBMIT_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/reassign` | 500
`APPROVAL_REASSIGN_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/remind` | 500
`APPROVAL_REMIND_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/request-info` | 500
`APPROVAL_REQUEST_INFO_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/approvals/requests/:id/comment` | 500
`APPROVAL_COMMENT_FAILED`, flat | 400 `VALIDATION_ERROR`, nested |
| `POST /api/v1/packages/publish` | 500 `INTERNAL_ERROR` | 400
`VALIDATION_ERROR` |
| the 4 external-datasource rows | 400 `EXTERNAL_DATASOURCE_ERROR` /
`EXTERNAL_IMPORT_ERROR` | status and code unchanged |
On every row the message is now the hook's sentence instead of the
wrapper. `VALIDATION_ERROR` is `standardErrorCodeForHttpStatus(400)`,
the same code the record-share family answers for an undeclared
sandboxed refusal. A refusal that declares its own code keeps it, and an
unregistered spelling rides `declaredCode`. A declared `userMessage`
rides along too. §5 pins both on the 16 re-dressed rows. "Flat" means `{
code, error: 'TEXT' }`, and "nested" means `{ success: false, error: {
code, message } }`. Security's own emitter writes `{ error: { code,
message } }` on every arm, and still does.
## Ride-along (triage `6101568631`: no decision owed)
On the 15 sharing-rule, security and approval rows, a non-sandbox
refusal can declare its own `status` (or `statusCode`) and `code`
without matching any route-specific arm. It is now answered at its
declared status in the nested envelope, where it used to get `500
FAMILY_CODE`. §5 pins this with a declared `409 RESOURCE_CONFLICT`.
`POST /packages/publish` already answered a declared status, so it is
unchanged.
These are unchanged, and §5 pins them on all 20 rows: a plain fault
keeps each route's own terminal (its status, its code and its words).
The route-specific arms run first and answer exactly as before. Those
are the sharing-rule prefixes, the approvals mapping and the security
typed arm.
## Not chosen: the external-datasource status (dispatch assumption 5)
These four routes answer every refusal at `400` with their own code.
Adopting the shared classification there would change a 4xx domain
answer. The import route relays the metadata door's declared `409
DESTRUCTIVE_CHANGE` as `400 EXTERNAL_IMPORT_ERROR`, which
`external-import-destructive-remedy.dogfood.test.ts` pins, and the
classification would answer 409. So these rows take the shared read
only. One consequence remains: a sandboxed refusal that declares, for
example, `409` answers `400 EXTERNAL_*` here with the correct sentence,
where the conforming doors answer 409. Reported, not chosen.
## Verification (head `2525cfcf6`, after merging `origin/main`
`7aa8d51c0`)
- **The pin** (`rest-write-route-hook-refusal-sentence.ledger.test.ts`,
`--project local`): 105 passed (§1 1, §2 44, §3 1, §4 2, §5 57).
- **Reverse verification.** The fix was committed first (`b90ead477`).
Then the four sources were restored from the base `e7e6544ab` into the
working tree only. On-disk proof: the `nestedRefusalEnvelope` /
`refusalSentence` / `classifiedRefusalAnswer` counts in the reverted
files read 0. The pin then went red on 51 of 105 cases: the 20 moved §2
rows, the 15 declared-409 ride-along cases and the 16 declared-channel
cases. The other 54 stayed green: the 24 original §2 rows, the 20
plain-fault cases, the 5 publish and external declared-409 cases, §1,
§3, §4 and §5's roster case. The direction was red, as expected.
Restoring used `git checkout HEAD`, with a trap. After that, `git
hash-object` matched the HEAD blob for all four files and `git diff
HEAD` was empty.
- **`@objectstack/rest`:** `pnpm --filter @objectstack/rest test` (the
`local` project) gave 272 files, 5239 passed and 326 skipped.
`test:repo` gave 5 files, 191 passed and 1 skipped. `typecheck` (`tsc
--noEmit` plus `check:test-typecheck`) was green. Earlier, both projects
together at `150c886a6` gave 277 files, 5373 passed and 327 skipped.
- **Gates.** `dispatch-gates --commands` was re-derived on this change:
65 families, the dispatch's 57 plus 8 that the changeset brings. All 65
were run at `2525cfcf6`, and `--ran` reports 65 run, 0 NOT-MEASURED and
0 UNRUN. Selected verdict lines:
- `check:route-envelope`: `rest-server.ts` holds at stringError 39 and
siblingCode 54, so no new dialect site was added.
- `check:error-status-conformance`: every derivable runtime status is
documented.
- `check:issue-citations`: 9 citations, all resolve.
- `check:nul-bytes`: OK.
- `check-adr-0087-registration`: no declared-breaking changeset.
- **Lint, as a proven narrowing.** `eslint --no-inline-config --format
json` ran over the 5 touched TS files: 5 results, 0 errors, 0 warnings,
0 ignored. The population is read from eslint's own config. The
changeset `.md` matches no config object (`--print-config` answers
`undefined`). Invariance: `eslint.config.mjs` enables no type-aware
linting (no `parserOptions.project`, as its own note states). Its only
cross-file reads are two baseline JSONs this diff does not touch. So the
diff cannot change the verdict on any untouched file.
## Acceptance notes
- **File surface.** The claim names `rest-server.ts` for the publish
answer, but that answer is built in `package-routes.ts`, which this PR
edits. `error-response.ts` gains `nestedRefusalEnvelope`, which is not
exported from the package index. Both files are in `packages/rest`. No
public export, error code or key is added.
- **One re-dress, two spellings.** The record-share family's
`respondSharingError` keeps its inline copy of the derivation that
`nestedRefusalEnvelope` states. Moving it onto the function is a
byte-identical refactor of an arm outside this card's surface. Carrier:
none.
- **Measured but not repaired: a sandboxed body these rows answer as a
FAULT still relays the wrapper.** Measured at `2525cfcf6` through this
pin's harness, at the route handler:
- A body that crashed (`TypeError: boom`) gets `hook 'mz_guard' threw:
TypeError: boom` on all 20 rows: 500 with the family code on 16 rows,
and 400 `EXTERNAL_*` on the 4 external rows.
- A refusal that declared `503` gets 500 with the family code and the
wrapper on the 15 rest-server rows. It gets 503 with the sentence on
publish, and 400 with the sentence on the external rows.
- The data doors answer both shapes as faults with the prose withheld
(§4).
- This is the fault-side sibling of this card, the shape §4 holds for
the import doors. It is in the report for the seat.
- **Cross-lane.** Merging `origin/main` at `7aa8d51c0` brought no edit
to `packages/rest` from #22739's lane or any other lane.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019SvPnd2bzECRNmAU9i6E4k)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6981246 commit 9859e4e
6 files changed
Lines changed: 377 additions & 67 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| 74 | + | |
74 | 75 | | |
75 | 76 | | |
76 | 77 | | |
| |||
677 | 678 | | |
678 | 679 | | |
679 | 680 | | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
| 697 | + | |
| 698 | + | |
| 699 | + | |
| 700 | + | |
| 701 | + | |
| 702 | + | |
| 703 | + | |
| 704 | + | |
| 705 | + | |
| 706 | + | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
680 | 736 | | |
681 | 737 | | |
682 | 738 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
382 | 383 | | |
383 | 384 | | |
384 | 385 | | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
385 | 408 | | |
386 | 409 | | |
387 | 410 | | |
388 | 411 | | |
389 | 412 | | |
390 | 413 | | |
391 | 414 | | |
392 | | - | |
| 415 | + | |
393 | 416 | | |
394 | 417 | | |
395 | 418 | | |
| |||
466 | 489 | | |
467 | 490 | | |
468 | 491 | | |
469 | | - | |
470 | | - | |
471 | | - | |
472 | | - | |
473 | | - | |
474 | | - | |
| 492 | + | |
475 | 493 | | |
476 | 494 | | |
477 | 495 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
175 | 177 | | |
176 | 178 | | |
177 | 179 | | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
178 | 205 | | |
179 | 206 | | |
180 | | - | |
181 | | - | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
182 | 210 | | |
183 | 211 | | |
184 | | - | |
| 212 | + | |
185 | 213 | | |
186 | 214 | | |
187 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
413 | 413 | | |
414 | 414 | | |
415 | 415 | | |
| 416 | + | |
416 | 417 | | |
417 | 418 | | |
418 | 419 | | |
| |||
12095 | 12096 | | |
12096 | 12097 | | |
12097 | 12098 | | |
| 12099 | + | |
| 12100 | + | |
| 12101 | + | |
| 12102 | + | |
| 12103 | + | |
| 12104 | + | |
| 12105 | + | |
| 12106 | + | |
| 12107 | + | |
| 12108 | + | |
| 12109 | + | |
| 12110 | + | |
| 12111 | + | |
| 12112 | + | |
| 12113 | + | |
| 12114 | + | |
| 12115 | + | |
| 12116 | + | |
| 12117 | + | |
| 12118 | + | |
12098 | 12119 | | |
12099 | 12120 | | |
12100 | 12121 | | |
| |||
12269 | 12290 | | |
12270 | 12291 | | |
12271 | 12292 | | |
12272 | | - | |
12273 | | - | |
| 12293 | + | |
| 12294 | + | |
| 12295 | + | |
| 12296 | + | |
| 12297 | + | |
| 12298 | + | |
| 12299 | + | |
12274 | 12300 | | |
12275 | 12301 | | |
12276 | 12302 | | |
| |||
12283 | 12309 | | |
12284 | 12310 | | |
12285 | 12311 | | |
| 12312 | + | |
| 12313 | + | |
| 12314 | + | |
| 12315 | + | |
| 12316 | + | |
| 12317 | + | |
| 12318 | + | |
| 12319 | + | |
| 12320 | + | |
| 12321 | + | |
| 12322 | + | |
| 12323 | + | |
| 12324 | + | |
| 12325 | + | |
12286 | 12326 | | |
12287 | 12327 | | |
12288 | 12328 | | |
| |||
12513 | 12553 | | |
12514 | 12554 | | |
12515 | 12555 | | |
| 12556 | + | |
| 12557 | + | |
| 12558 | + | |
| 12559 | + | |
| 12560 | + | |
| 12561 | + | |
| 12562 | + | |
| 12563 | + | |
| 12564 | + | |
| 12565 | + | |
| 12566 | + | |
| 12567 | + | |
| 12568 | + | |
| 12569 | + | |
| 12570 | + | |
| 12571 | + | |
| 12572 | + | |
| 12573 | + | |
| 12574 | + | |
| 12575 | + | |
| 12576 | + | |
12516 | 12577 | | |
12517 | 12578 | | |
12518 | 12579 | | |
| |||
0 commit comments