@objectstack/types 17.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +349 -0
- package/dist/index.d.mts +98 -18
- package/dist/index.d.ts +98 -18
- package/dist/index.js +37 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,354 @@
|
|
|
1
1
|
# @objectstack/types
|
|
2
2
|
|
|
3
|
+
## 17.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2f65b1b: `error.code` is a closed vocabulary at every door (#9106, maintainer ruling
|
|
8
|
+
2026-08-16): the runtime dispatcher's thrown-error exits
|
|
9
|
+
(`HttpDispatcher.errorFromThrown`, `dispatcher-plugin`'s `errorResponseBase`,
|
|
10
|
+
`endpoint-executor`'s `endpointErrorAnswer` — the actions door among them) now
|
|
11
|
+
serve the narrowed `code` the shared resolver (`resolveThrownHttpError`,
|
|
12
|
+
`@objectstack/types`) has always computed, exactly as the REST door has since
|
|
13
|
+
#8016. A thrown code that is not a member of `StandardErrorCode ∪
|
|
14
|
+
ERROR_CODE_LEDGER` no longer reaches `error.code`.
|
|
15
|
+
|
|
16
|
+
It is not dropped: `ApiErrorSchema` declares a new optional `declaredCode`
|
|
17
|
+
field — the open, author-authored channel — and the demoted spelling rides
|
|
18
|
+
there. Presence means demotion: the field is absent whenever the producer's
|
|
19
|
+
code is a vocabulary member (it is already in `error.code`) or the producer
|
|
20
|
+
declared none. The #7867 sandbox passthrough capability is preserved — a
|
|
21
|
+
metadata app's own thrown `.code` still crosses the QuickJS boundary and still
|
|
22
|
+
reaches the wire.
|
|
23
|
+
|
|
24
|
+
For a metadata app that throws its own code (e.g.
|
|
25
|
+
`Object.assign(new Error('pick another'), { code: 'DUPLICATE' })` in an action
|
|
26
|
+
body) and reads it back from an actions-door failure:
|
|
27
|
+
|
|
28
|
+
- FROM: `error.code === 'DUPLICATE'`
|
|
29
|
+
- TO: `error.code` is the closed member the status derives (e.g.
|
|
30
|
+
`VALIDATION_ERROR` on a 400) and `error.declaredCode === 'DUPLICATE'`.
|
|
31
|
+
One-line fix: branch on `error.declaredCode` for app-specific spellings;
|
|
32
|
+
branch on `error.code` for platform conditions.
|
|
33
|
+
|
|
34
|
+
Platform producers are unaffected: every registered code reaches `error.code`
|
|
35
|
+
verbatim, as before (post-#8846 the dispatcher-vocabulary gate holds that set
|
|
36
|
+
registered). Measured before landing (the ruling's binding precondition): no
|
|
37
|
+
existing consumer of the actions door branches on author-authored strings in
|
|
38
|
+
`error.code`.
|
|
39
|
+
|
|
40
|
+
`@objectstack/types` adds `demotedDeclaredCode(thrown)` — the one definition of
|
|
41
|
+
"which spelling a boundary surfaces beside the closed `code`".
|
|
42
|
+
- 79c46da: feat(contract): a hook refusal can mark its message user-facing — `userMessage`, the producer-side opt-in channel (#9934, producer half of objectui#5210)
|
|
43
|
+
|
|
44
|
+
<!-- adr-0087: not-required (no-migration-prescription) Purely additive: one
|
|
45
|
+
new OPTIONAL field on the two error-envelope schemas, a new shared reader in
|
|
46
|
+
@objectstack/types, and passthrough plumbing at the boundaries. Nothing
|
|
47
|
+
authorable is renamed, retired, aliased or tombstoned, so there is no
|
|
48
|
+
conversion to register. Unmarked errors produce byte-identical wire bodies. -->
|
|
49
|
+
|
|
50
|
+
The console form deliberately discards the server `message` on 403 and
|
|
51
|
+
substitutes a generic string — the recorded #3821 fix for platform diagnostics
|
|
52
|
+
leaking to end users. That substitution also suppressed every deliberate,
|
|
53
|
+
localized refusal an application hook author wrote (11 real hook guards in the
|
|
54
|
+
objectui#5210 report), and incentivized misusing 400 for permission refusals.
|
|
55
|
+
The maintainer-accepted ruling (2026-08-19, option 1): give the AUTHOR a
|
|
56
|
+
producer-side way to mark a refusal message user-facing, once, at the contract
|
|
57
|
+
level — status-agnostic, with #3821 preserved by construction for everything
|
|
58
|
+
unmarked.
|
|
59
|
+
|
|
60
|
+
**The marking**: set `userMessage` (non-empty string) on the thrown error at
|
|
61
|
+
throw time. It is a text-carrying field, not a boolean beside `message` — the
|
|
62
|
+
mark and the marked text are one value, so no boundary that rewraps or
|
|
63
|
+
substitutes `message` can promote platform prose into the marked channel, and
|
|
64
|
+
platform/driver code never sets it.
|
|
65
|
+
|
|
66
|
+
- `@objectstack/spec`: `ApiErrorSchema.userMessage` and
|
|
67
|
+
`EnhancedApiErrorSchema.userMessage` (optional, additive).
|
|
68
|
+
- `@objectstack/types`: `declaredUserMessage(error)` — the ONE "is this
|
|
69
|
+
marked?" read (non-empty string, nothing invented) — and
|
|
70
|
+
`ThrownHttpError.userMessage` on `resolveThrownHttpError`.
|
|
71
|
+
- `@objectstack/rest`: `mapDataError` / `resolveErrorResponse` ride a declared
|
|
72
|
+
marking onto whatever envelope classification chose (flat body top-level
|
|
73
|
+
`userMessage`, truncated at the same #5423 bound as the 4xx message).
|
|
74
|
+
- `@objectstack/runtime`: the QuickJS side-channel carries `userMessage`
|
|
75
|
+
across the sandbox boundary (both directions, joining `code`/`fields`/
|
|
76
|
+
`status`), and the dispatcher door emits it as a declared sibling in the
|
|
77
|
+
nested envelope.
|
|
78
|
+
- `@objectstack/client`: the SDK attaches `err.userMessage` from both wire
|
|
79
|
+
dialects, so a UI renders it verbatim when present and keeps its generic
|
|
80
|
+
substitution when absent.
|
|
81
|
+
|
|
82
|
+
The consumer half — the console form rendering a marked message instead of the
|
|
83
|
+
generic `form.noPermissionToSave` — is objectui#5210.
|
|
84
|
+
|
|
85
|
+
### Patch Changes
|
|
86
|
+
|
|
87
|
+
- 2d0af57: fix(tests): give two default-vitest-timeout cases real margin instead of a bare default (#9311)
|
|
88
|
+
|
|
89
|
+
Two cases only passed `pnpm test` when they were not competing for CPU — the
|
|
90
|
+
same defect class as the already-closed precedents #3662, #4186, #4485,
|
|
91
|
+
#5421, #6329: a test running under vitest's **default** `testTimeout` /
|
|
92
|
+
`hookTimeout` with no margin for anything heavier than an idle box.
|
|
93
|
+
|
|
94
|
+
**`packages/types/src/node.test.ts`** — `"falls back to the importing
|
|
95
|
+
package's own resolution when the host does not declare"` is the only case
|
|
96
|
+
in the file that performs a real dynamic `import()` of `@objectstack/spec` (a
|
|
97
|
+
multi-megabyte package); every sibling in the same `describe` block resolves
|
|
98
|
+
a small on-disk fixture or fails fast, all under 10ms. Measured on this box:
|
|
99
|
+
~0.9-1.1s unloaded, already observed failing at 5061ms against the 5000ms
|
|
100
|
+
default under nothing heavier than `turbo run test --concurrency=2` (#9311's
|
|
101
|
+
own isolation runs). Gave that one case an explicit 30s `testTimeout` — the
|
|
102
|
+
same order of magnitude the repo already uses for subprocess/real-load cases
|
|
103
|
+
(`#3662` precedent) — and left every sub-10ms sibling alone.
|
|
104
|
+
|
|
105
|
+
**`packages/qa/dogfood/test/semantic-roles.dogfood.test.ts`** — its
|
|
106
|
+
`beforeAll` boots the full showcase stack (ObjectQL + ~45 plugins) through
|
|
107
|
+
`@objectstack/verify`'s `bootStack`, which does not fit vitest's 10s
|
|
108
|
+
`hookTimeout` default with any margin at all: observed failing at 10027ms
|
|
109
|
+
against the 10000ms budget, and this file's own isolated run measured 18.3s
|
|
110
|
+
(vitest `Duration`) / 19.5s wall clock for the whole file even with the box
|
|
111
|
+
otherwise idle. Gave the hook an explicit 180s timeout, matching this
|
|
112
|
+
package's own existing house pattern for the identical
|
|
113
|
+
`bootStack(showcaseStack, …)` call
|
|
114
|
+
(`admin-identity-audit-trail.dogfood.test.ts`'s `beforeAll(…, 180_000)`)
|
|
115
|
+
rather than inventing a new number for the same operation.
|
|
116
|
+
|
|
117
|
+
**No behaviour change** — both suites already pass; this only gives the two
|
|
118
|
+
timeout-sensitive cases room to finish on a loaded box. The repo's full test
|
|
119
|
+
suite is confirmed green at low concurrency (#9311), so this is margin
|
|
120
|
+
repair, not a product fix. `turbo.json`'s default concurrency is out of scope
|
|
121
|
+
for this change (a maintainer-level default, per #9311's own filing).
|
|
122
|
+
- 27a567d: fix(types): teach the internal-leak predicate MySQL's three error templates (#8739)
|
|
123
|
+
|
|
124
|
+
`looksLikeInternalErrorLeak` decides whether a message is a driver dump that
|
|
125
|
+
must not reach an API client. It is applied at three HTTP boundaries
|
|
126
|
+
(`@objectstack/rest`'s `mapDataError`, `@objectstack/runtime`'s
|
|
127
|
+
dispatcher-plugin and endpoint-executor, the hono adapter) and by
|
|
128
|
+
`@objectstack/objectql`'s log redactor. Its dialect list covered the SQLite
|
|
129
|
+
family and Postgres; on a MySQL deployment it returned `false` for every one of
|
|
130
|
+
these conditions — **silent, not clearing**.
|
|
131
|
+
|
|
132
|
+
Under the maintainer's 2026-08-15 ruling on #8739, **MySQL is a supported
|
|
133
|
+
deployment target**, not merely a tested dialect — the answer already implied by
|
|
134
|
+
what is published (`OS_DATABASE_DRIVER=mysql` as a documented deployment knob,
|
|
135
|
+
`MysqlConfig` as authorable datasource config, per-field MySQL DDL in
|
|
136
|
+
`types.mdx`) and by a required CI check that stands up a live `mysql:8.0`. A
|
|
137
|
+
supported target's driver text reaches those boundaries in production, so its
|
|
138
|
+
templates belong in the list.
|
|
139
|
+
|
|
140
|
+
**Now recognised** — one per condition the other two dialects were already
|
|
141
|
+
covered for, each anchored on MySQL's own errmsg template rather than on a bare
|
|
142
|
+
substring:
|
|
143
|
+
|
|
144
|
+
- `Table 'app.t' doesn't exist` (ER_NO_SUCH_TABLE 1146). MySQL's contracted
|
|
145
|
+
spelling quotes `db.table` as one identifier, so the Postgres
|
|
146
|
+
`relation "t" does not exist` limb could never reach it.
|
|
147
|
+
- `Unknown column 'c' in 'field list'` (ER_BAD_FIELD_ERROR 1054). Both quoted
|
|
148
|
+
parts are required; the second is MySQL's clause name (`field list`,
|
|
149
|
+
`where clause`, `order clause`, `on clause`), and it is what distinguishes the
|
|
150
|
+
driver's template from a sentence that merely calls a column unknown.
|
|
151
|
+
- `Duplicate entry 'x' for key 'i'` (ER_DUP_ENTRY 1062). The `for key` tail plus
|
|
152
|
+
a quoted index is the anchor. This is the one MySQL template whose text embeds
|
|
153
|
+
a **caller's value** rather than an identifier — SQLite's
|
|
154
|
+
`UNIQUE constraint failed: t.c` and Postgres' `violates unique constraint "…"`
|
|
155
|
+
both name only an index — which is why closing this gap was worth a behaviour
|
|
156
|
+
change rather than another comment.
|
|
157
|
+
|
|
158
|
+
**Deliberately still NOT recognised**, so the boundary of the change is on the
|
|
159
|
+
record rather than inferred:
|
|
160
|
+
|
|
161
|
+
- **MySQL's ACL family** — `Access denied for user 'u'@'h' to database 'd'`
|
|
162
|
+
(1044), `SELECT command denied to user … for table 't'` (1142) — the
|
|
163
|
+
counterpart of the Postgres `permission denied for table` limb. Nothing in
|
|
164
|
+
this repo has raised one off a live server, and the standing rule in this
|
|
165
|
+
neighbourhood (`unique-violation.ts`) is that a dialect's spelling is added
|
|
166
|
+
once it has been MEASURED off a thrown error, never from a reading of the
|
|
167
|
+
manual. `Access denied` also collides with this platform's own security prose
|
|
168
|
+
(`[Security] Access denied: …`), so a guessed pattern here would over-match —
|
|
169
|
+
and over-matching suppresses diagnostics an operator needs.
|
|
170
|
+
- **MSSQL and Oracle** — `Invalid object name 'sys_metadata'.`,
|
|
171
|
+
`ORA-00942: table or view does not exist` still return `false`.
|
|
172
|
+
- **Prose that shares the keywords without the driver's anchoring** — an import
|
|
173
|
+
summary saying `duplicate entry in the uploaded file`, a mapping message
|
|
174
|
+
saying `Unknown column in the uploaded CSV header`, `The table you selected
|
|
175
|
+
does not exist`. Pinned as negative cases, because a phrasing list that says
|
|
176
|
+
"leak" too often replaces real answers with `Internal server error`.
|
|
177
|
+
|
|
178
|
+
**The `false`-means-UNCOVERED rule survives the change and keeps a live
|
|
179
|
+
subject.** A `false` here has never meant the text is safe, only that the
|
|
180
|
+
predicate never learned that dialect — the reading a reviewer on PR #8737 got
|
|
181
|
+
wrong while sizing a disclosure residual, which is what produced this card. The
|
|
182
|
+
four `toBe(false)` pins PR #8824 planted as a tripwire for this exact moment
|
|
183
|
+
went red as designed and are rewritten, not deleted: the same three measured
|
|
184
|
+
messages now assert `true`, so a future change that silently drops MySQL
|
|
185
|
+
coverage fails there, and a second block keeps the original `false`-means-
|
|
186
|
+
uncovered shape pointed at MSSQL and Oracle. `declaresServerFault` remains the
|
|
187
|
+
phrasing-independent answer.
|
|
188
|
+
|
|
189
|
+
**No status mapping moves.** `@objectstack/rest` answers the 409 conflict
|
|
190
|
+
question with `isUniqueViolationError`, above and independently of this
|
|
191
|
+
predicate (#6250), so a MySQL duplicate-entry error is still `409
|
|
192
|
+
UNIQUE_VIOLATION` and a MySQL unknown-column error is still `400 INVALID_FIELD`
|
|
193
|
+
— both decided before the leak branch is reached. The log redactor is unchanged
|
|
194
|
+
too: a bare MySQL diagnostic carries no knex ` - ` separator, so there is no
|
|
195
|
+
statement to cut. Measured across the predicate's full consumer set — types,
|
|
196
|
+
objectql, rest, runtime, metadata-protocol, hono, service-package,
|
|
197
|
+
service-analytics — the only verdicts that moved are the two that measure this
|
|
198
|
+
predicate directly.
|
|
199
|
+
|
|
200
|
+
No live MySQL deployment leaking through these boundaries was measured; this
|
|
201
|
+
closes a gap in what the boundary recognises, and the card is explicit that no
|
|
202
|
+
leak was demonstrated.
|
|
203
|
+
- bbbfcfc: fix(types): `isUniqueViolationError` stops claiming the sentences that say a unique constraint is ABSENT (#8590)
|
|
204
|
+
|
|
205
|
+
The shared predicate's message limb was a bare `unique constraint`, and a word
|
|
206
|
+
pair is not a condition. Every dialect that can say "this row violated a unique
|
|
207
|
+
constraint" can also say "there is no unique constraint here", and the same two
|
|
208
|
+
words sit adjacent in both — so the predicate answered **true** for errors
|
|
209
|
+
meaning the exact opposite of what it detects. `rest-server.ts` maps that
|
|
210
|
+
verdict to `409 UNIQUE_VIOLATION`, which tells a client to change a value when
|
|
211
|
+
nothing was ever compared, on a status an SDK will not retry.
|
|
212
|
+
|
|
213
|
+
**Measured on live servers for this fix, all three supported dialect families**
|
|
214
|
+
— SQLite via better-sqlite3, PostgreSQL 16.13 via `pg` 8.22.0, MariaDB 10.11.14
|
|
215
|
+
via `mysql2` 3.23.1, all through knex 3.3.0 — driving each dialect through both
|
|
216
|
+
conditions plus the NOT NULL / FOREIGN KEY near misses:
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
sqlite ON CONFLICT clause does not match any PRIMARY KEY or UNIQUE constraint
|
|
220
|
+
-> was true, WRONG (the reported defect, #8590)
|
|
221
|
+
postgres there is no unique constraint matching given keys for referenced table "t"
|
|
222
|
+
-> was true, WRONG (42830 — found by this fix's dialect sweep)
|
|
223
|
+
postgres there is no unique or exclusion constraint matching the ON CONFLICT specification
|
|
224
|
+
-> false (the pair is not adjacent here)
|
|
225
|
+
mysql the condition cannot arise: knex compiles to ON DUPLICATE KEY UPDATE,
|
|
226
|
+
which carries no conflict target (confirmed against a live server)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Postgres was not clean either, and that chose the fix.** #8590 was filed
|
|
230
|
+
reading the collision as SQLite-only, with Postgres escaping "by luck of word
|
|
231
|
+
order". The sweep raised **42830** — a `FOREIGN KEY` referencing a non-unique
|
|
232
|
+
column — where Postgres puts `unique constraint` adjacent in its own absence
|
|
233
|
+
sentence. The card offered two candidate fixes; only one survives 42830. A
|
|
234
|
+
negative lookahead on SQLite's missing-index sentence is a blocklist that can
|
|
235
|
+
only enumerate absence sentences somebody already tripped over, and it answers
|
|
236
|
+
`true` on 42830. So the limb now requires a **violation phrasing** —
|
|
237
|
+
`unique constraint failed` (SQLite) or `violates unique constraint` (Postgres) —
|
|
238
|
+
which restores the module's own stated default, *unrecognised is `false`*, to
|
|
239
|
+
the message channel.
|
|
240
|
+
|
|
241
|
+
**Both spellings the retired limb covered are preserved exactly**, which was the
|
|
242
|
+
constraint on the fix: the limb was inherited verbatim from the REST branch
|
|
243
|
+
#6250 replaced and covered SQLite's `UNIQUE constraint failed: t.c` *and*
|
|
244
|
+
Postgres' `... violates unique constraint "..."`. The `unique violation`,
|
|
245
|
+
`duplicate key` and `duplicate entry` limbs are untouched, as are the `code` and
|
|
246
|
+
`errno` channels — MySQL's `Duplicate entry` path never went through the
|
|
247
|
+
narrowed limb at all.
|
|
248
|
+
|
|
249
|
+
**No user-visible behaviour changes today; this closes a latent inversion.** The
|
|
250
|
+
one site compiling a caller-supplied conflict target (`SqlDriver.upsert`)
|
|
251
|
+
recognises the unbacked target *first* in its catch and throws a refusal
|
|
252
|
+
declaring `status: 400`, and `mapDataError` reads `declaredHttpStatus` before it
|
|
253
|
+
reaches the unique-violation branch — so the 409 was gated off the wire by
|
|
254
|
+
ordering, not by the verdict. That ordering was the only thing standing between
|
|
255
|
+
this and a wrong status, which is why the verdict is now pinned rather than left
|
|
256
|
+
to it. A repo-wide scan of every string literal whose verdict moves found no
|
|
257
|
+
consumer relying on the old answer: all of them are prose, a different
|
|
258
|
+
predicate's vocabulary (`looksLikeInternalErrorLeak` keeps its own list), or
|
|
259
|
+
fixtures asserted through the status-passthrough path.
|
|
260
|
+
|
|
261
|
+
`unbacked-conflict-target.test.ts`'s pin — written by #8567 to point at itself
|
|
262
|
+
rather than go quietly green — is **inverted, not deleted**, and
|
|
263
|
+
`unique-violation-absence-sentences.test.ts` pins the absence sentences per
|
|
264
|
+
dialect in both directions, including the code channel, so re-reading `code`
|
|
265
|
+
cannot undo the message-side fix from the other side.
|
|
266
|
+
- Updated dependencies [56656aa]
|
|
267
|
+
- Updated dependencies [07e630e]
|
|
268
|
+
- Updated dependencies [2f65b1b]
|
|
269
|
+
- Updated dependencies [720ee95]
|
|
270
|
+
- Updated dependencies [f287435]
|
|
271
|
+
- Updated dependencies [9aa8890]
|
|
272
|
+
- Updated dependencies [7c9c1dd]
|
|
273
|
+
- Updated dependencies [75b7c24]
|
|
274
|
+
- Updated dependencies [d5552ca]
|
|
275
|
+
- Updated dependencies [d9813a9]
|
|
276
|
+
- Updated dependencies [8640fb2]
|
|
277
|
+
- Updated dependencies [2420641]
|
|
278
|
+
- Updated dependencies [2ad91c3]
|
|
279
|
+
- Updated dependencies [f57fb38]
|
|
280
|
+
- Updated dependencies [00777a0]
|
|
281
|
+
- Updated dependencies [d491625]
|
|
282
|
+
- Updated dependencies [420804d]
|
|
283
|
+
- Updated dependencies [716ac9b]
|
|
284
|
+
- Updated dependencies [62b1427]
|
|
285
|
+
- Updated dependencies [7ea1372]
|
|
286
|
+
- Updated dependencies [23abe27]
|
|
287
|
+
- Updated dependencies [985a9cd]
|
|
288
|
+
- Updated dependencies [a8189ae]
|
|
289
|
+
- Updated dependencies [26e70fb]
|
|
290
|
+
- Updated dependencies [42b05af]
|
|
291
|
+
- Updated dependencies [2b292ce]
|
|
292
|
+
- Updated dependencies [abcf853]
|
|
293
|
+
- Updated dependencies [8b9eba5]
|
|
294
|
+
- Updated dependencies [d575779]
|
|
295
|
+
- Updated dependencies [94f7ef8]
|
|
296
|
+
- Updated dependencies [c5ac5e4]
|
|
297
|
+
- Updated dependencies [a777944]
|
|
298
|
+
- Updated dependencies [dd88e1c]
|
|
299
|
+
- Updated dependencies [856527c]
|
|
300
|
+
- Updated dependencies [870f710]
|
|
301
|
+
- Updated dependencies [79c46da]
|
|
302
|
+
- Updated dependencies [7ff3975]
|
|
303
|
+
- Updated dependencies [29d055b]
|
|
304
|
+
- Updated dependencies [65589d6]
|
|
305
|
+
- Updated dependencies [2c86fe3]
|
|
306
|
+
- Updated dependencies [e196c6a]
|
|
307
|
+
- Updated dependencies [4ab7523]
|
|
308
|
+
- Updated dependencies [19539b4]
|
|
309
|
+
- Updated dependencies [11b779e]
|
|
310
|
+
- Updated dependencies [739fe5b]
|
|
311
|
+
- Updated dependencies [4bfe1a5]
|
|
312
|
+
- Updated dependencies [2065e31]
|
|
313
|
+
- Updated dependencies [b69d0f5]
|
|
314
|
+
- Updated dependencies [4d47afe]
|
|
315
|
+
- Updated dependencies [e4e5c6e]
|
|
316
|
+
- Updated dependencies [9a56784]
|
|
317
|
+
- Updated dependencies [d00d2f6]
|
|
318
|
+
- Updated dependencies [df0c12d]
|
|
319
|
+
- Updated dependencies [d31785f]
|
|
320
|
+
- Updated dependencies [c308a4f]
|
|
321
|
+
- Updated dependencies [e2899f6]
|
|
322
|
+
- Updated dependencies [3851f87]
|
|
323
|
+
- Updated dependencies [2a29caa]
|
|
324
|
+
- Updated dependencies [09a6eee]
|
|
325
|
+
- Updated dependencies [1a7f907]
|
|
326
|
+
- Updated dependencies [cd455c8]
|
|
327
|
+
- Updated dependencies [30d3752]
|
|
328
|
+
- Updated dependencies [c80e7ae]
|
|
329
|
+
- Updated dependencies [09a9a8a]
|
|
330
|
+
- Updated dependencies [07026cf]
|
|
331
|
+
- Updated dependencies [5d4f3d5]
|
|
332
|
+
- Updated dependencies [4d80e8b]
|
|
333
|
+
- Updated dependencies [30b1c63]
|
|
334
|
+
- Updated dependencies [079b457]
|
|
335
|
+
- Updated dependencies [e43b211]
|
|
336
|
+
- Updated dependencies [890b38f]
|
|
337
|
+
- Updated dependencies [8bee54b]
|
|
338
|
+
- Updated dependencies [7a537ce]
|
|
339
|
+
- Updated dependencies [593c4bf]
|
|
340
|
+
- Updated dependencies [ff08691]
|
|
341
|
+
- Updated dependencies [60e0f90]
|
|
342
|
+
- Updated dependencies [90c5285]
|
|
343
|
+
- Updated dependencies [7901b2d]
|
|
344
|
+
- Updated dependencies [56bca91]
|
|
345
|
+
- Updated dependencies [79394d7]
|
|
346
|
+
- Updated dependencies [730fd9a]
|
|
347
|
+
- Updated dependencies [44bc51d]
|
|
348
|
+
- Updated dependencies [73cfddf]
|
|
349
|
+
- Updated dependencies [d634e66]
|
|
350
|
+
- @objectstack/spec@17.1.0
|
|
351
|
+
|
|
3
352
|
## 17.0.0
|
|
4
353
|
|
|
5
354
|
### Minor Changes
|
package/dist/index.d.mts
CHANGED
|
@@ -393,7 +393,11 @@ declare const INTERNAL_ERROR_MESSAGE = "Internal server error";
|
|
|
393
393
|
* (a message that *starts* as `SELECT`/`INSERT INTO`/`UPDATE`/`DELETE FROM` —
|
|
394
394
|
* drivers prefix the offending SQL to their message), constraint-violation
|
|
395
395
|
* dumps, which name physical tables and columns, and the
|
|
396
|
-
* {@link DIALECT_LEAK_PHRASINGS}
|
|
396
|
+
* {@link DIALECT_LEAK_PHRASINGS} the list covers — the SQLite family, Postgres
|
|
397
|
+
* and, since #8739, MySQL/MariaDB. A dialect outside that coverage (MSSQL and
|
|
398
|
+
* Oracle are the standing examples) makes this return FALSE without meaning the
|
|
399
|
+
* text is safe; read {@link DIALECT_LEAK_PHRASINGS}' note before sizing
|
|
400
|
+
* anything on a `false`.
|
|
397
401
|
*
|
|
398
402
|
* Does NOT match ordinary business or validation messages, which is why the
|
|
399
403
|
* statement forms are anchored with `startsWith` and the dialect phrasings on
|
|
@@ -724,17 +728,33 @@ declare function sendError(res: EnvelopeResponse, status: number, code: ErrorCod
|
|
|
724
728
|
* unregistered code there is a failing test, not a wire answer.
|
|
725
729
|
*
|
|
726
730
|
* {@link ThrownHttpError.declaredCode} is the producer's own string, verbatim
|
|
727
|
-
* and un-narrowed
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
731
|
+
* and un-narrowed. Until #9106 it was what the dispatcher door put in
|
|
732
|
+
* `error.code`; since the #9106 ruling it is what BOTH doors surface as the
|
|
733
|
+
* wire's `declaredCode` when it is not a vocabulary member (see below).
|
|
734
|
+
*
|
|
735
|
+
* [#8087] The first ruling on that gap (maintainer, 2026-08-12) kept the
|
|
736
|
+
* dispatcher's verbatim spelling and delivered a GATE — the unregistered
|
|
737
|
+
* producers are measured and classified
|
|
738
|
+
* (`packages/runtime/src/dispatcher-error-vocabulary.ts`,
|
|
739
|
+
* `pnpm check:dispatcher-error-vocabulary`) instead of named in prose here.
|
|
740
|
+
* The gate's own first derivation then measured the limb no registration can
|
|
741
|
+
* close: a metadata app's action code crosses the sandbox boundary carrying
|
|
742
|
+
* the app's OWN `.code` (#7867), authored by tenants at runtime.
|
|
743
|
+
*
|
|
744
|
+
* [#9106] That limb was ruled (maintainer, 2026-08-16): **`error.code` is a
|
|
745
|
+
* closed vocabulary at every door.** The dispatcher door now takes
|
|
746
|
+
* {@link ThrownHttpError.code} — the demote this resolver has always computed,
|
|
747
|
+
* and the REST door's spelling since #8016 — and a producer's unregistered
|
|
748
|
+
* string rides the wire's `declaredCode` (declared on `ApiErrorSchema`)
|
|
749
|
+
* instead of `error.code`. #7867's capability is preserved: the author's code
|
|
750
|
+
* still crosses the sandbox and still reaches the wire — in the open,
|
|
751
|
+
* author-authored channel, not the closed one. Use
|
|
752
|
+
* {@link demotedDeclaredCode} to read the spelling a boundary should surface
|
|
753
|
+
* beside the closed `code`.
|
|
754
|
+
*
|
|
755
|
+
* So the doors agree on **status** and on **code** unconditionally now — both
|
|
756
|
+
* answers come from ONE function, which is what keeps agreement a construction
|
|
757
|
+
* rather than two suites agreeing about literals.
|
|
738
758
|
*
|
|
739
759
|
* ## What this deliberately does NOT decide
|
|
740
760
|
*
|
|
@@ -764,10 +784,12 @@ interface ThrownHttpError {
|
|
|
764
784
|
* to `status: 500`, so a caller that must tell "the producer said so" from
|
|
765
785
|
* "I supplied the default" cannot read it off the value. The workaround in
|
|
766
786
|
* the repo was to probe this function with a fallback no producer declares
|
|
767
|
-
* — `resolveThrownHttpError(e, 0).status !== 0
|
|
768
|
-
* `packages/rest`'s publish-classification suite. That is a magic number
|
|
787
|
+
* — `resolveThrownHttpError(e, 0).status !== 0`. That is a magic number
|
|
769
788
|
* standing in for a fact this function already computed, and it fails
|
|
770
|
-
* silently the day a producer declares the sentinel. So the fact is stated
|
|
789
|
+
* silently the day a producer declares the sentinel. So the fact is stated;
|
|
790
|
+
* `packages/rest`'s publish-classification suite now reads
|
|
791
|
+
* `resolveThrownHttpError(error).declaredStatus !== undefined` instead of
|
|
792
|
+
* hand-spelling the workaround.
|
|
771
793
|
*
|
|
772
794
|
* ## Who needs the distinction
|
|
773
795
|
*
|
|
@@ -788,12 +810,39 @@ interface ThrownHttpError {
|
|
|
788
810
|
code: ErrorCode;
|
|
789
811
|
/**
|
|
790
812
|
* The producer's own code, verbatim and un-narrowed, or `undefined` when it
|
|
791
|
-
* declared none.
|
|
792
|
-
*
|
|
813
|
+
* declared none. Never for `error.code` — that slot takes {@link code} at
|
|
814
|
+
* every door (#9106) — but for the wire's `declaredCode` channel when the
|
|
815
|
+
* spelling is not a vocabulary member ({@link demotedDeclaredCode}). See the
|
|
816
|
+
* module note on why there are two.
|
|
793
817
|
*/
|
|
794
818
|
declaredCode?: string;
|
|
795
819
|
/** The thrown message, UNSANITISED — see the module note on disclosure. */
|
|
796
820
|
message: string;
|
|
821
|
+
/**
|
|
822
|
+
* The producer's user-facing refusal text, verbatim — present exactly when
|
|
823
|
+
* the throw carried a non-empty string `userMessage` (#9934).
|
|
824
|
+
*
|
|
825
|
+
* This is the producer-side opt-in the objectui#5210 ruling asked for
|
|
826
|
+
* (maintainer, 2026-08-19, option 1): an application hook's refusal has no
|
|
827
|
+
* way to distinguish author-written user guidance from platform diagnostics,
|
|
828
|
+
* so the console substitutes a generic string on 403 (the recorded #3821
|
|
829
|
+
* fix) and every author-written remedy is suppressed with the diagnostics.
|
|
830
|
+
* A producer that sets `userMessage` on the thrown error is saying, at throw
|
|
831
|
+
* time, "this exact text is addressed to the END USER" — a consumer renders
|
|
832
|
+
* it verbatim and keeps the generic substitution for everything unmarked.
|
|
833
|
+
*
|
|
834
|
+
* Deliberately a FIELD carrying the text, not a boolean beside `message`:
|
|
835
|
+
* the mark and the marked text are one value, so a boundary that rewraps or
|
|
836
|
+
* substitutes `message` (sanitisation, truncation, the sandbox debug
|
|
837
|
+
* wrapper) can never accidentally promote platform prose into the marked
|
|
838
|
+
* channel — the #3821 protection holds by construction. Read through
|
|
839
|
+
* {@link declaredUserMessage}, never with an inline `typeof` probe.
|
|
840
|
+
*
|
|
841
|
+
* Status-agnostic on purpose (the ruling's second constraint): a 400, 403,
|
|
842
|
+
* 409 or 503 refusal may all carry it. It never REPLACES `message` — the
|
|
843
|
+
* diagnostic channel keeps its wording for logs and developers.
|
|
844
|
+
*/
|
|
845
|
+
userMessage?: string;
|
|
797
846
|
/**
|
|
798
847
|
* Structured context: spec-validation `issues[]`, record-validation
|
|
799
848
|
* `fields[]`. Absent rather than `{}` when the throw carried none, so an
|
|
@@ -814,6 +863,7 @@ interface ThrownHttpError {
|
|
|
814
863
|
* | code | `VALIDATION_FAILED` if it is one → a REGISTERED `.code` → derived from the status |
|
|
815
864
|
* | declaredCode | `VALIDATION_FAILED` if it is one → any non-empty string `.code` → absent |
|
|
816
865
|
* | message | `.message` when it is a string → `String(error)` |
|
|
866
|
+
* | userMessage | a non-empty string `.userMessage` → absent (see {@link declaredUserMessage}) |
|
|
817
867
|
*
|
|
818
868
|
* Both status spellings are read because both are produced in this repo:
|
|
819
869
|
* `plugin-approvals`' lifecycle hooks and `metadata-protocol` throw
|
|
@@ -822,6 +872,36 @@ interface ThrownHttpError {
|
|
|
822
872
|
* RECORD_LOCKED` until #7525.
|
|
823
873
|
*/
|
|
824
874
|
declare function resolveThrownHttpError(error: unknown, fallbackStatus?: number): ThrownHttpError;
|
|
875
|
+
/**
|
|
876
|
+
* The user-facing refusal text a thrown error DECLARED, or `undefined` when it
|
|
877
|
+
* declared none (#9934). See {@link ThrownHttpError.userMessage} for what the
|
|
878
|
+
* declaration means and why it is a text-carrying field rather than a flag.
|
|
879
|
+
*
|
|
880
|
+
* The ONE read every boundary applies — the REST classification door, the
|
|
881
|
+
* dispatcher door, and the sandbox side-channel all call this rather than
|
|
882
|
+
* probing `error.userMessage` themselves, so "what counts as marked" cannot
|
|
883
|
+
* fork per door the way the `status`/`statusCode` spelling once did (#7525).
|
|
884
|
+
*
|
|
885
|
+
* A non-string or blank `userMessage` is NOT a declaration: `undefined`, a
|
|
886
|
+
* number, `''` and whitespace-only all answer `undefined`, so nothing invents
|
|
887
|
+
* a marked message for a producer that never wrote one — absent means the
|
|
888
|
+
* consumer keeps its generic substitution (#3821 preserved by construction).
|
|
889
|
+
*/
|
|
890
|
+
declare function declaredUserMessage(error: unknown): string | undefined;
|
|
891
|
+
/**
|
|
892
|
+
* The producer's spelling a boundary should surface as the wire's
|
|
893
|
+
* `declaredCode` beside the closed `code` — or `undefined` when there is
|
|
894
|
+
* nothing to surface (#9106).
|
|
895
|
+
*
|
|
896
|
+
* Present exactly when the throw spelled a code that did NOT survive into
|
|
897
|
+
* {@link ThrownHttpError.code} — i.e. the demote happened. A registered code
|
|
898
|
+
* is already in `code`, so emitting it again would put two spellings of one
|
|
899
|
+
* fact on every refusal; a throw with no code has nothing to declare. Spelled
|
|
900
|
+
* once here rather than as three `!==` comparisons at three exits, so
|
|
901
|
+
* "presence means demotion" (`ApiErrorSchema.declaredCode`'s documented
|
|
902
|
+
* semantics) has one definition.
|
|
903
|
+
*/
|
|
904
|
+
declare function demotedDeclaredCode(thrown: ThrownHttpError): string | undefined;
|
|
825
905
|
|
|
826
906
|
/** The HTTP status a validation failure maps to when the error names none. */
|
|
827
907
|
declare const VALIDATION_FAILED_STATUS = 400;
|
|
@@ -1327,4 +1407,4 @@ interface RuntimePlugin {
|
|
|
1327
1407
|
onStart?: (ctx: RuntimeContext) => void | Promise<void>;
|
|
1328
1408
|
}
|
|
1329
1409
|
|
|
1330
|
-
export { type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, type RuntimeContext, type RuntimePlugin, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declaredIndexUniqueIsGlobal, declaresServerFault, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isMcpServerEnabled, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, stampSearchPinyinEnabled, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
|
|
1410
|
+
export { type EnvelopeResponse, GLOBAL_UNIQUE_CONFIRMATION_REQUIRED, GLOBAL_UNIQUE_ISOLATED_PRESCRIPTION, type GlobalUniqueAttestation, type GlobalUniqueFinding, type IKernel, INTERNAL_ERROR_MESSAGE, type KeysetPageQuery, type KeysetWalk, type KeysetWalkOptions, type RuntimeContext, type RuntimePlugin, type ThrownHttpError, VALIDATION_FAILED_STATUS, type ValidationFailureDetails, _resetEnvDeprecationWarnings, buildGlobalUniqueStopMessage, collectConfiguredLocales, collectGlobalUniques, declaredIndexUniqueIsGlobal, declaredUserMessage, declaresServerFault, demotedDeclaredCode, describeGlobalUniqueFinding, emitDegradedBootBanner, fieldUniqueIsGlobal, fieldsFromZodIssues, globalUniqueFindingId, isMcpServerEnabled, isModuleNotFoundError, isPlatformOwnedObject, isRelationSubObjectPhrase, isUnbackedConflictTargetError, isUniqueViolationError, keysetWalk, looksLikeInternalErrorLeak, matchMissingColumnOfRelation, postureGatesGlobalUniques, readEnvWithDeprecation, recordGlobalUniqueAttestation, resolveAllowDegradedTenancy, resolveAllowDevPlugin, resolveAllowDriverConnectFailure, resolveMcpStdioAutoStart, resolveMultiOrgEnabled, resolveOrgLimit, resolveSandboxTimeoutMs, resolveSearchPinyinEnabled, resolveTenancyPosture, resolveThrownHttpError, sendError, sendOk, stampSearchPinyinEnabled, unconfirmedGlobalUniques, uniqueViolationColumn, validationFailure, validationFailureDetails };
|