You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The contract half of the ruled objectui#5210 split (maintainer 2026-08-19,
option 1: producer-side opt-in). A hook author marks a refusal message
user-facing at throw time by setting userMessage on the thrown error; the
marking is a text-carrying field (never a flag), status-agnostic, and absent
by default — so the console's #3821 generic substitution is preserved by
construction for everything unmarked.
- spec: ApiErrorSchema.userMessage + EnhancedApiErrorSchema.userMessage
- types: declaredUserMessage() one-read + ThrownHttpError.userMessage
- rest: mapDataError/resolveErrorResponse ride the marking onto every
classified envelope (flat dialect, truncated at the #5423 bound)
- runtime: QuickJS side-channel carries userMessage across the sandbox
boundary; dispatcher door emits it as a declared sibling (nested dialect)
- client: err.userMessage attached from both wire dialects
Pins: spec parse pins, types resolver pins, rest door pins (marked/unmarked,
status-agnostic, fault terminals unchanged when unmarked), sandbox round-trip
pins, dispatcher conformance pin, and a dogfood integration test through the
real runtime hook path (handler hook 403 + sandboxed body hook).
Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw
Co-authored-by: Claude <noreply@anthropic.com>
feat(contract): a hook refusal can mark its message user-facing — `userMessage`, the producer-side opt-in channel (#9934, producer half of objectui#5210)
10
+
11
+
<!-- adr-0087: not-required (no-migration-prescription) Purely additive: one
12
+
new OPTIONAL field on the two error-envelope schemas, a new shared reader in
13
+
@objectstack/types, and passthrough plumbing at the boundaries. Nothing
14
+
authorable is renamed, retired, aliased or tombstoned, so there is no
15
+
conversion to register. Unmarked errors produce byte-identical wire bodies. -->
16
+
17
+
The console form deliberately discards the server `message` on 403 and
18
+
substitutes a generic string — the recorded #3821 fix for platform diagnostics
19
+
leaking to end users. That substitution also suppressed every deliberate,
20
+
localized refusal an application hook author wrote (11 real hook guards in the
21
+
objectui#5210 report), and incentivized misusing 400 for permission refusals.
22
+
The maintainer-accepted ruling (2026-08-19, option 1): give the AUTHOR a
23
+
producer-side way to mark a refusal message user-facing, once, at the contract
24
+
level — status-agnostic, with #3821 preserved by construction for everything
25
+
unmarked.
26
+
27
+
**The marking**: set `userMessage` (non-empty string) on the thrown error at
28
+
throw time. It is a text-carrying field, not a boolean beside `message` — the
29
+
mark and the marked text are one value, so no boundary that rewraps or
30
+
substitutes `message` can promote platform prose into the marked channel, and
31
+
platform/driver code never sets it.
32
+
33
+
-`@objectstack/spec`: `ApiErrorSchema.userMessage` and
|**data**|`{ name: string; title?: string; measures: object[]; dimensions: object[] }[]`| ✅ | Available cubes, each as the `CubeMeta` discovery projection — the cube name, its title, and the measures/dimensions a client may name in a query. A bare array: there is no `cubes` wrapper object, and no cube `sql` is published. |
50
50
@@ -79,7 +79,7 @@ const result = AnalyticsEndpoint.parse(data);
79
79
| Property | Type | Required | Description |
80
80
| :--- | :--- | :--- | :--- |
81
81
|**success**|`boolean`| ✅ | Operation success status |
Copy file name to clipboardExpand all lines: content/docs/references/api/batch.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -55,7 +55,7 @@ const result = BatchConfigSchema.parse(data);
55
55
| :--- | :--- | :--- | :--- |
56
56
|**id**|`string`| optional | Record ID if operation succeeded |
57
57
|**success**|`boolean`| ✅ | Whether this record was processed successfully |
58
-
|**errors**|`{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; category?: string; … }[]`| optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). |
58
+
|**errors**|`{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }[]`| optional | Array of errors if operation failed. Branch on `errors[0].code` — an atomic batch that rolled back marks rows that were written then undone with code ROLLED_BACK and rows never reached with NOT_ATTEMPTED, while the causal row keeps its own error (#4793). A NON-atomic batch that stopped (the `continueOnError: false` default) marks its un-attempted tail with the same NOT_ATTEMPTED code — rows before the failure stay written and keep reporting success, since nothing was rolled back (#7539). |
59
59
|**data**|`Record<string, any>`| optional | Full record data (if returnRecords=true) |
60
60
|**index**|`number`| optional | Index of the record in the request array |
61
61
|**droppedFields**|`{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]`| optional | Write-observability (#3407/#3431/#3455): caller-supplied fields LEGALLY stripped from THIS row before it was written — static `readonly` (#2948) / TRUE `readonlyWhen` (#3042) on update, or the #3043 create-ingress strip. Per-row because a batch can drop different fields on different rows (`readonlyWhen` is record-state-dependent). Present ONLY when ≥1 field was dropped for this row; the row still succeeded (success unchanged). A single response header cannot express per-row drops, so this body field is the canonical bulk channel — REST does not emit `X-ObjectStack-Dropped-Fields` for batches. Optional — omit-when-empty keeps the shape backward-compatible. |
@@ -122,7 +122,7 @@ const result = BatchConfigSchema.parse(data);
122
122
| Property | Type | Required | Description |
123
123
| :--- | :--- | :--- | :--- |
124
124
|**success**|`boolean`| ✅ | Operation success status |
0 commit comments