Skip to content

Commit 1c7adc7

Browse files
os-trumpclaude
andauthored
fix(rest): the by-id /data door stops shipping the QuickJS wrapper out of a declared-code arm (#15065)
`classifyDataError` surfaces the bespoke structured arms above the sandbox unwrap door on purpose, so the structured fields survive the generic catch-alls. Every arm built its sentence from `error?.message`, which for a sandboxed producer is the `<kind> '<name>' threw: <msg>` debug wrapper, and the unwrap door that would have read `.innerMessage` sits below them and was never reached. One hook refusal therefore came back as two different sentences depending on which route caught it. The arms now ask the two-read rule the unwrap door and the declared-status passthrough already share, named once as `armSentence` rather than re-opined per arm. `resolveErrorResponse` declines the shared consult for a sandbox-origin error, so the bulk / metadata / UI doors answer byte for byte what they answered before and the repair lands on `mapDataError` alone. A sandboxed CRASH carrying a declared code is deliberately unchanged: `sandboxBusinessMessage` declines a crash, so the fallback hands the arm the wrapper exactly as today. The `DUPLICATE_RECORD` arm is untouched — it is gated on the engine's envelope class and `SandboxError` sets `name` unconditionally, so no sandboxed producer can reach it. The parity file's sandboxed-producer case is FLIPPED from ACCEPTED DIVERGENCE to CONVERGED rather than deleted; it is the only thing that would notice the divergence returning. Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza Co-authored-by: Claude <noreply@anthropic.com>
1 parent ad35745 commit 1c7adc7

4 files changed

Lines changed: 513 additions & 17 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
"@objectstack/rest": patch
3+
---
4+
5+
fix(rest): the by-id `/data` door stops shipping the QuickJS debug wrapper out of a declared-code structured arm (#14704)
6+
7+
**Response-contract change on a shipped public route**, in the direction of the
8+
answer the other door already gives. A sandboxed hook or action body refusing a
9+
write throws a `SandboxError` whose `.message` is the
10+
`<kind> '<name>' threw: <msg>` **debug wrapper** written for the server log and
11+
whose `.innerMessage` is the sentence the author addressed to the end user. The
12+
bespoke structured arms in `classifyDataError` are surfaced ABOVE the sandbox
13+
unwrap door on purpose — "so the structured fields survive the generic
14+
catch-alls" — and every one of them built its sentence from `error.message`. The
15+
unwrap door that would have read `.innerMessage` sits below them and was never
16+
reached, so one refusal came back as two different sentences depending on the
17+
route:
18+
19+
| door | answer |
20+
|---|---|
21+
| `sendThrownError` / `handleRouteError` (bulk, metadata, UI) | `409``{"error":"Opportunity is closed.","code":"DELETE_RESTRICTED"}` |
22+
| `mapDataError` (single-record `/data`) | `409``{"error":"hook 'guard' threw: Error: Opportunity is closed.","code":"DELETE_RESTRICTED",…}` |
23+
24+
The bulk door is the reference and does not move: #11588 named
25+
`sandboxBusinessMessage` and taught the declared-status passthrough to read it,
26+
and #14541 declines the shared arm consult outright for a sandbox-origin error.
27+
This is that same rule reaching the arms — **asked once**, as `armSentence`,
28+
rather than re-opined per arm, because a third local opinion at this boundary is
29+
how the two doors came to disagree.
30+
31+
**What callers see change** — only on the single-record `/data` door, and only
32+
the human sentence. Status, `code` and every structured field are unchanged:
33+
34+
- `DELETE_RESTRICTED`, `CONCURRENT_UPDATE`, `ERR_DATASOURCE_UNAVAILABLE`,
35+
`VALIDATION_FAILED`, `FEEDS_DISABLED` / `FILES_DISABLED`,
36+
`ATTACHMENT_PARENT_ACCESS` / `ATTACHMENT_DELETE_DENIED`,
37+
`RECORD_NOT_ACCESSIBLE` and `PERMISSION_DENIED` thrown from a sandboxed body
38+
now answer with the author's sentence instead of the wrapper.
39+
- `VALIDATION_FAILED` is the ordinary case: an app-authored hook writing
40+
`throw Object.assign(new Error('Amount must be positive'), { code: 'VALIDATION_FAILED', fields: […] })`
41+
answered `400 {"error":"hook 'guard' threw: Error: Amount must be positive"}`
42+
and now answers `400 {"error":"Amount must be positive"}`.
43+
44+
**Unchanged, deliberately:**
45+
46+
- Every non-sandbox producer. The rule is a READ of the field the sandbox
47+
populated, never a pattern-strip of the wrapper off `.message`, so an error
48+
with no `.innerMessage` relays byte for byte what it relayed before.
49+
- Every bulk / metadata / UI route. Those reach the arms through
50+
`resolveErrorResponse`, which declines the consult for a sandbox-origin error.
51+
- A sandboxed **CRASH** carrying a declared code. `sandboxBusinessMessage`
52+
declines a crash (#7543), so such an error still answers with the arm's status
53+
and the wrapper prose, where the unwrap door's terminal for the same crash is
54+
the sanitised `500`. Choosing between those two answers is fault
55+
classification rather than message sourcing; it is pinned as a named
56+
divergence and carried as its own decision card.
57+
- The `DUPLICATE_RECORD` arm. It is gated on the engine's envelope class
58+
(`name === 'DuplicateRecordError'`) and `SandboxError` sets `name`
59+
unconditionally, so no sandboxed producer reaches it; converging its GATE
60+
would change the wire for two producer populations and reverse #14389 §5.

0 commit comments

Comments
 (0)