@objectstack/plugin-webhooks 17.0.0-rc.6 → 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 CHANGED
@@ -1,5 +1,2024 @@
1
1
  # @objectstack/plugin-webhooks
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 90417a8: chore(plugin-webhooks): `sys_webhook` declares its data-API exposure explicitly — recording the posture, not narrowing it (#9756)
8
+
9
+ `sys_webhook` shipped with no `enable` block at all, so it kept the full default
10
+ data API. Three cards each noticed and each named the narrowing as the next
11
+ step — #7799 (the signing secret), #7986 (the custom headers), #8025 option 2
12
+ (the URL) — and each assumed a later one would write the line. None did, and the
13
+ last of them closed `completed` with the line still unwritten. The posture was
14
+ never a judgement; it was a default nobody had written down.
15
+
16
+ It is written down now:
17
+
18
+ ```ts
19
+ enable: { apiMethods: ['get', 'list', 'create', 'update', 'delete', 'bulk'] }
20
+ ```
21
+
22
+ **The effective surface is unchanged, and that is the honest headline.** The set
23
+ is derived from a census of who actually reaches the object, taken before
24
+ anything was edited:
25
+
26
+ | consumer | reaches it through | needs |
27
+ |:---|:---|:---|
28
+ | Setup/Studio console — `nav_webhooks`, four list views, `userActions` create/edit/delete | REST `/api/v1/data/sys_webhook` (gated) | `get` `list` `create` `update` `delete` |
29
+ | Operator predicate write — "deactivate every webhook on an object" (#4639) | REST `updateMany`/`deleteMany` (gated on `bulk`) | `bulk` |
30
+ | `AutoEnqueuer`, `bootstrapDeclaredWebhooks`, the provenance stamp, `redeliver-guard`, the secret sweep | `engine.*` and lifecycle hooks — ObjectQL directly, which never consults `enable.apiMethods` | ungated |
31
+
32
+ Every primitive is required by a real consumer, so the set is all six — whose
33
+ operation closure is what the absent block already produced. Nothing that was
34
+ reachable becomes unreachable, and `/me/permissions` reports the identical
35
+ `apiOperations` array. No caller needs to change anything.
36
+
37
+ ⛔ **Do not read this as the read-surface narrowing those three cards asked
38
+ for.** It is not one, and `apiMethods` cannot be one here: `url` (#8025 —
39
+ won't-fix on masking, because the URL is the routing key an operator must be
40
+ able to see, search, sort and edit) and a legacy row's un-migrated
41
+ `definition_json.headers` (#7986 — still read, and warned about, by
42
+ `readLegacyHeaders`) are served by `get`/`list`, which is exactly what the admin
43
+ console requires. Any set that removes them removes the admin surface too. The
44
+ sibling `sys_http_delivery` can hold `['get','list']` because it is engine-owned
45
+ and never authored; `sys_webhook` is a first-class admin authoring surface.
46
+
47
+ The equality above is pinned in `sys-webhook-api-exposure.test.ts` rather than
48
+ left as a claim, so a later change that does move the surface has to say so.
49
+ - b278695: fix(webhooks): refuse a malformed `sys_webhook.headers_secret` at the write door instead of at the next delivery (#8566)
50
+
51
+ <!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is
52
+ renamed, retired or tombstoned. This adds a runtime validation hook on one
53
+ plugin-owned object's existing column; the authoring envelope
54
+ (`webhook.zod.ts`), the field declaration and every stored shape are untouched.
55
+ The accept-set narrows, but only over values that were already unusable at
56
+ delivery time (see below), so there is no configuration for a migration to
57
+ prescribe a rewrite of. -->
58
+
59
+ `sys_webhook.headers_secret` is a `Field.secret()` whose plaintext is **not** an
60
+ opaque blob: it is a serialized header map with a required shape — a flat JSON
61
+ object of string values — and `parseStoredHeaders` is its only reader. Nothing
62
+ validated that shape on the way in. The ordinary data API accepted any string,
63
+ encrypted it like any other secret, minted a real `sys_secret` row, and left the
64
+ column holding a perfectly valid `secret:` ref that read back as the mask with
65
+ `active: true`.
66
+
67
+ Measured on a real engine through `engine.update()` — the ordinary data API, no
68
+ privileged access — every one of these was **accepted** and is a value the
69
+ plugin can never use: `{}`, `[]`, `{"X-Count": 5}`, a nested object, and
70
+ `{X-Team: crm}` (a typo). The field is directly admin-authorable and its own
71
+ description instructs the author to type a JSON object into it, which makes a
72
+ typo the *expected* failure rather than an exotic one.
73
+
74
+ **This is not an exposure fix and must not be read as one.** #8558/#8565 already
75
+ closed the consumer half: a webhook whose stored header map does not come back
76
+ as a flat string map parks the subscription and reports at `error`, rather than
77
+ delivering header-less with a valid signature. Nothing leaks, and nothing is
78
+ silently lost today. What this changes is **when the author finds out** — at the
79
+ write door where they typed it, instead of at the next matching record change,
80
+ an unbounded time later and in a different surface.
81
+
82
+ **What is refused:** a `headers_secret` plaintext that does not parse back as a
83
+ flat JSON object of string values with at least one entry, with a located
84
+ ADR-0112 `VALIDATION_ERROR` / 400 naming `sys_webhook.headers_secret`, quoting
85
+ the shape the field's own description asks for, and diagnosing the specific
86
+ spelling (invalid JSON / an array / an empty object / which key's value is not a
87
+ string). ⛔ The message never echoes the rejected value — this column carries
88
+ credentials, and quoting the input would print an `Authorization: Bearer …` into
89
+ logs and error bodies, re-opening in the diagnostic exactly the exposure #7986
90
+ moved this field onto the encrypted channel to close. It names header *keys* and
91
+ value *types* only.
92
+
93
+ **What stays accepted, byte for byte:** every valid flat string map (as JSON
94
+ text, or as an authored object the engine serializes into the same form); `null`
95
+ to clear; an omitted key to leave the stored value unchanged; and an **echoed
96
+ read-mask**, so the ordinary Setup-form round-trip (GET a row, edit an unrelated
97
+ field, PATCH it back) is untouched. `""` is deliberately passed through to
98
+ #8559's `EmptyCredentialWriteError` rather than re-refused here — one door, one
99
+ owner, one message.
100
+
101
+ **Where it runs, and why that is the whole mechanism:** a `beforeInsert` /
102
+ `beforeUpdate` hook on `sys_webhook`, bound by `WebhookOutboxPlugin` before its
103
+ first seeded write. It has to run *before* the engine's `encryptSecretFields` —
104
+ one step later the plaintext is gone and the column holds an opaque ref, so a
105
+ validator behind it would have nothing left to validate. The suite measures that
106
+ ordering rather than asserting it: every refusal pins that **no `sys_secret`
107
+ cipher row was minted**, which is only true if the gate ran first.
108
+
109
+ A hook rather than checks on the plugin's own write paths
110
+ (`bootstrapDeclaredWebhooks` / `headersPatch` / the migration sweep), because a
111
+ direct `PATCH /api/v1/data/sys_webhook` goes through none of them and that is
112
+ the measured trigger. Those paths inherit the validation through the hook and
113
+ deliberately carry no second check.
114
+
115
+ A general `secret`-channel plaintext validator — letting any `secret`-typed
116
+ field declare its own plaintext shape — is the principled generalization and is
117
+ recorded as the **promotion path**, not built here: it becomes the shape the
118
+ moment a second shaped-plaintext `secret` field exists (maintainer ruling
119
+ 2026-08-13; one consumer does not justify a general capability).
120
+ - Updated dependencies [56656aa]
121
+ - Updated dependencies [07e630e]
122
+ - Updated dependencies [2f65b1b]
123
+ - Updated dependencies [720ee95]
124
+ - Updated dependencies [f287435]
125
+ - Updated dependencies [2782805]
126
+ - Updated dependencies [e43d63a]
127
+ - Updated dependencies [9aa8890]
128
+ - Updated dependencies [7c9c1dd]
129
+ - Updated dependencies [75b7c24]
130
+ - Updated dependencies [d5552ca]
131
+ - Updated dependencies [d9813a9]
132
+ - Updated dependencies [8640fb2]
133
+ - Updated dependencies [2420641]
134
+ - Updated dependencies [2ad91c3]
135
+ - Updated dependencies [f57fb38]
136
+ - Updated dependencies [00777a0]
137
+ - Updated dependencies [d491625]
138
+ - Updated dependencies [420804d]
139
+ - Updated dependencies [716ac9b]
140
+ - Updated dependencies [a38408a]
141
+ - Updated dependencies [62b1427]
142
+ - Updated dependencies [7ea1372]
143
+ - Updated dependencies [23abe27]
144
+ - Updated dependencies [985a9cd]
145
+ - Updated dependencies [5f5e234]
146
+ - Updated dependencies [a8189ae]
147
+ - Updated dependencies [26e70fb]
148
+ - Updated dependencies [42b05af]
149
+ - Updated dependencies [2b292ce]
150
+ - Updated dependencies [abcf853]
151
+ - Updated dependencies [8b9eba5]
152
+ - Updated dependencies [d575779]
153
+ - Updated dependencies [94f7ef8]
154
+ - Updated dependencies [c5ac5e4]
155
+ - Updated dependencies [a777944]
156
+ - Updated dependencies [dd88e1c]
157
+ - Updated dependencies [856527c]
158
+ - Updated dependencies [870f710]
159
+ - Updated dependencies [79c46da]
160
+ - Updated dependencies [7ff3975]
161
+ - Updated dependencies [29d055b]
162
+ - Updated dependencies [65589d6]
163
+ - Updated dependencies [2c86fe3]
164
+ - Updated dependencies [e196c6a]
165
+ - Updated dependencies [24173e9]
166
+ - Updated dependencies [4ab7523]
167
+ - Updated dependencies [19539b4]
168
+ - Updated dependencies [f8eb736]
169
+ - Updated dependencies [11b779e]
170
+ - Updated dependencies [739fe5b]
171
+ - Updated dependencies [4bfe1a5]
172
+ - Updated dependencies [2065e31]
173
+ - Updated dependencies [b69d0f5]
174
+ - Updated dependencies [4d47afe]
175
+ - Updated dependencies [e4e5c6e]
176
+ - Updated dependencies [9a56784]
177
+ - Updated dependencies [d00d2f6]
178
+ - Updated dependencies [df0c12d]
179
+ - Updated dependencies [44738f7]
180
+ - Updated dependencies [d31785f]
181
+ - Updated dependencies [c308a4f]
182
+ - Updated dependencies [e2899f6]
183
+ - Updated dependencies [3851f87]
184
+ - Updated dependencies [2a29caa]
185
+ - Updated dependencies [09a6eee]
186
+ - Updated dependencies [1a7f907]
187
+ - Updated dependencies [cd455c8]
188
+ - Updated dependencies [e1bb0ca]
189
+ - Updated dependencies [30d3752]
190
+ - Updated dependencies [c80e7ae]
191
+ - Updated dependencies [09a9a8a]
192
+ - Updated dependencies [07026cf]
193
+ - Updated dependencies [5d4f3d5]
194
+ - Updated dependencies [4d80e8b]
195
+ - Updated dependencies [30b1c63]
196
+ - Updated dependencies [079b457]
197
+ - Updated dependencies [e43b211]
198
+ - Updated dependencies [890b38f]
199
+ - Updated dependencies [8bee54b]
200
+ - Updated dependencies [7a537ce]
201
+ - Updated dependencies [593c4bf]
202
+ - Updated dependencies [ff08691]
203
+ - Updated dependencies [60e0f90]
204
+ - Updated dependencies [90c5285]
205
+ - Updated dependencies [402c125]
206
+ - Updated dependencies [7901b2d]
207
+ - Updated dependencies [56bca91]
208
+ - Updated dependencies [79394d7]
209
+ - Updated dependencies [730fd9a]
210
+ - Updated dependencies [44bc51d]
211
+ - Updated dependencies [73cfddf]
212
+ - Updated dependencies [d634e66]
213
+ - @objectstack/spec@17.1.0
214
+ - @objectstack/core@17.1.0
215
+ - @objectstack/service-messaging@17.1.0
216
+
217
+ ## 17.0.0
218
+
219
+ ### Minor Changes
220
+
221
+ - f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003)
222
+
223
+ Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed
224
+ the set; this batch moves the emitters that still spoke lowercase `snake_case`
225
+ onto it.
226
+
227
+ **Wire-visible change.** Error codes on these surfaces change spelling. Generic
228
+ conditions collapse onto the standard catalog rather than keeping a synonym:
229
+ `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` →
230
+ `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` →
231
+ `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` →
232
+ `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes
233
+ registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`,
234
+ `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept:
235
+ `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`,
236
+ `service-messaging`, `service-automation`, `trigger-api`.
237
+
238
+ Branch on `error.code` values rather than pattern-matching their case: the
239
+ console's fix for the same rename (objectui#2977) reads codes case-insensitively
240
+ for exactly this reason, and that is the pattern to copy in your own consumers if
241
+ you support servers on both sides of the change.
242
+
243
+ **Four routes stop putting a code in the message slot.** The webhook redeliver
244
+ route, the API-trigger webhook, and two `rest` routes answered
245
+ `{ success: false, error: '<code>', message }` — the code occupying `error`, the
246
+ declared object envelope nowhere. They now emit `error: { code, message }`, and
247
+ three API-trigger branches gained a message they never had. Clients reading
248
+ `body.error` as a string on those routes must read `body.error.code`.
249
+
250
+ **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a):
251
+ `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and
252
+ two `RetryStrategy` types. The connector-side pair is renamed; importers of the
253
+ `integration` subpath update the name. Side effect: the api-side `ErrorCategory`
254
+ and `RetryStrategy` now appear in the generated API reference at all — the name
255
+ collision had been silently dropping them.
256
+
257
+ **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client
258
+ registration route put better-auth's arbitrary `body.error` string straight into
259
+ `error.code`. The code is now ours and the upstream discriminator moved to
260
+ `details.upstreamError`.
261
+
262
+ **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values
263
+ (ADR-0112 D6b): it is persisted audit history, and the same column holds
264
+ non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a
265
+ 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's
266
+ `--json` output contract.
267
+
268
+ A `check:error-code-casing` CI guard now fails on a new lowercase literal in a
269
+ code position, since the ledger's casing rule can only police codes that someone
270
+ registers.
271
+
272
+ - f2445c9: feat(spec,objectql,client,plugin-webhooks): predicate writes get an honest bulk event contract (#4639)
273
+
274
+ A `multi: true` update/delete reaches `IDataDriver.updateMany` / `deleteMany`,
275
+ which are contracted to resolve an affected row COUNT and nothing else. That
276
+ satisfies neither `DataEvent.recordId` (required) nor `before` / `after` /
277
+ `changes`, so before #4626 the engine fabricated a per-record event with
278
+ `recordId: ''` and `after: <count>` — an event every schema-compliant consumer
279
+ must reject, and one the webhook enqueuer's `?? 'unknown'` fallback turned into
280
+ a real delivery naming an unidentifiable record. #4626 removed the fabrication
281
+ and published nothing instead: honest, but it left webhooks, knowledge sync and
282
+ `subscribeData` silent for every predicate write.
283
+
284
+ Bulk writes now get their **own** contract rather than impersonating a
285
+ per-record one or going dark:
286
+
287
+ - **New `BulkDataEvent`** (`@objectstack/spec/api`): `data.records.updated` /
288
+ `data.records.deleted` — note the plural — carrying `id`, `type`, `object`,
289
+ `matched`, `userId?`, `timestamp`. Deliberately a separate schema from
290
+ `DataEvent`, not a widened one: a consumer that receives
291
+ `data.records.updated` knows from the type alone that no `recordId` is
292
+ coming, instead of discovering an empty string at runtime.
293
+ - **Engine** publishes it from the `multi: true` branches of `update()` /
294
+ `delete()`, validated with `BulkDataEventSchema.parse` before publish. A
295
+ predicate that matched **zero** rows publishes nothing (no data changed — this
296
+ is what keeps an idle background sweep from becoming an hourly "0 records"
297
+ delivery), and a driver that resolves a non-count publishes nothing and warns
298
+ rather than asserting a number it cannot verify. Per-record writes are
299
+ untouched, including a scalar `where.id` with `multi: true`, which is still a
300
+ single-record target and still emits `data.record.deleted`.
301
+ - **Webhooks**: two new opt-in triggers, `bulk_update` and `bulk_delete`
302
+ (`WebhookTriggerType`, and the `sys_webhook.triggers` multi-select). They are
303
+ **not** extra sources for `create` / `update` / `delete`: the delivered body
304
+ has no `recordId` and no record, so routing it to existing per-record
305
+ subscribers would hand them a payload missing every field they read — the
306
+ same class of breakage as the old `recordId: ''`, from the other direction. A
307
+ webhook that wants both subscribes to both. Bulk deliveries dedup on the
308
+ producer's event uuid, since two sweeps in the same millisecond are genuinely
309
+ different events that a timestamp-based key would collapse.
310
+ - **Client SDK**: new `client.events.subscribeBulkData(object, cb)`, with the
311
+ same loud boundary validation as `subscribeData`. Kept a separate method for
312
+ the same reason — delivering a `BulkDataEvent` to a `(event: DataEvent) =>
313
+ void` callback would recreate exactly the "typed field, `undefined` at
314
+ runtime" defect #4626 removed. `subscribeData`'s own guard was also tightened
315
+ from `data.` to `data.record.`, so an aggregate event is ignored rather than
316
+ rejected as off-contract.
317
+ - **Knowledge sync** now says out loud that a predicate write leaves its index
318
+ stale. A knowledge index is a per-record projection and `matched: 40` names no
319
+ record, so no event shape could drive it — the durable fix is reconciliation,
320
+ tracked in #4672.
321
+
322
+ The event carries no `where` predicate. The only one available at publish time
323
+ is the middleware-composed AST, whose filter embeds the security layer's
324
+ injected row scoping (RLS, sharing) — publishing it would ship tenant scoping
325
+ internals to whatever external URL a webhook points at.
326
+
327
+ Also pays off a measurement debt from #4655, which claimed the write-path cost
328
+ of event publishing had been measured but never published the numbers:
329
+ `packages/objectql/src/engine-data-events.bench.ts` measures it. Against an
330
+ in-memory driver, publishing costs ~7–9µs per event (insert 0.021ms vs 0.012ms,
331
+ single-id update 0.013ms vs 0.007ms). A bulk write pays that **once** regardless
332
+ of how many rows matched (0.040ms vs 0.034ms over a 100-row match set), so its
333
+ relative cost shrinks as the match set grows.
334
+
335
+ - 69f1dfd: fix(webhooks): materialize stack-declared webhooks into the dispatcher (#3461)
336
+
337
+ A webhook authored declaratively — `defineStack({ webhooks })` / `defineWebhook()`,
338
+ validated against the spec `WebhookSchema` — was a **silent no-op**. The runtime
339
+ dispatcher (`AutoEnqueuer`) fans out off `sys_webhook` DATA rows (`object_name` /
340
+ `active`), which until now were only ever written by hand through the object's
341
+ CRUD UI. Nothing turned a declared webhook (`object` / `isActive`) into a
342
+ dispatchable row, so authoring `webhooks:` on a stack produced `webhook` metadata
343
+ that never fired (ADR-0078). The showcase app itself shipped a `webhooks:` entry
344
+ that did nothing.
345
+
346
+ `@objectstack/plugin-webhooks` now bridges the two on boot:
347
+
348
+ - **`bootstrapDeclaredWebhooks`** reads declared `webhook` metadata from the
349
+ ObjectQL registry (where the manifest decomposition already parks
350
+ `stack.webhooks`), validates each through `WebhookSchema.parse()` — the spec
351
+ schema finally has a real consumer — and materializes it into a `sys_webhook`
352
+ row, mapping `object → object_name`, `isActive → active`, and stashing the full
353
+ envelope (headers / secret / retry / timeout) in `definition_json`. The
354
+ auto-enqueuer's first cache refresh then picks the row up and dispatches it.
355
+ - **Seed-not-clobber provenance** (mirrors `sys_sharing_rule`, #2909): `sys_webhook`
356
+ gains `managed_by` / `customized` columns. Declared webhooks re-seed every boot
357
+ as `managed_by: 'package'`, but a row an admin created (`managed_by: 'admin'`) or
358
+ edited in Setup (`customized: true`, stamped by a `beforeUpdate` hook) is never
359
+ overwritten — a deactivated noisy webhook survives redeploys.
360
+
361
+ Connector-declared `webhooks` remain not-yet-enforced (that is a separate seam,
362
+ #3197). Registering `webhook` as a first-class metadata type + enrolling it in the
363
+ liveness `GOVERNED` set is a tracked follow-up.
364
+
365
+ Migration: none required. Existing hand-authored `sys_webhook` rows default to
366
+ `managed_by: 'admin'` and are never touched by the seeder. Anyone who authored
367
+ `webhooks:` on a stack expecting it to fire will find it now does — review those
368
+ declarations (especially `url` / `isActive`) before upgrading.
369
+
370
+ ### Patch Changes
371
+
372
+ - 257d97a: ADR-0078 Phase 4, decided rather than deferred: the silent skips stop being silent at runtime. The registry — the one choke point every metadata door goes through — now emits a functional-completeness diagnostic at registration, and the webhook enqueuer's zero-trigger skip warns instead of returning `null` wordlessly.
373
+
374
+ **The Phase 4 ruling.** The phase had two halves, and they got opposite verdicts:
375
+
376
+ - **Generative rule sweep: rejected — not deferred.** A generator can enumerate candidates ("which optional keys might be load-bearing?") but cannot verify runtime skip sites, and a rule without its skip-site citation is a false prescription — this campaign shipped four of those and every one was caught by the verification pass a generator would skip. The route is structurally wrong; no amount of waiting produces the evidence that would fix it.
377
+ - **Registration-time diagnostics: built now.** The evidence was already in hand, not pending: #3896 (Setup authoring inserted `sys_sharing_rule` rows directly, bypassing the schema that "required" `criteria`) and cloud's `rowColor.mapping` (an `as never` cast bypassed tsc) prove that doors which skip Zod and lint are real. The author-time gate only protects metadata that passes through `os build` / `validate` / `lint`; `SchemaRegistry.registerObject` is where _every_ door converges — declared stacks, plugin objects, `extend` contributions, `saveMetaItem`, raw `registerObject` calls.
378
+
379
+ **Same predicate, same rule ids, different posture.** The registry calls the same `checkFieldCompleteness` that `validate-functional-completeness` uses, so the boot log carries the _same rule ids_ the lint reports (`field/summary-without-operations`, …) — an operator or an AI reading the log greps the id straight into the same docs and suppression story. But the registry **warns and never throws**: ADR-0078 §1's error severity means _the instance is dead_, not _the system is dead_ — an inert field must not kill a boot that thousands of healthy objects share. Errors block at author time; the registry's job is to make sure the silence never survives to runtime unobserved.
380
+
381
+ One line per object with every finding aggregated (not per request — the hot path stays free; not per finding — a three-dead-field object is one greppable line). Follows `warnStrippedLegacyApiMethods` (#3543) exactly: module-level once-per-object dedup, injectable `warn`, pure observation that never mutates the schema.
382
+
383
+ **The webhook skip now names itself.** `auto-enqueuer.ts`'s `if (triggers.size === 0) return null` sat under a comment blessing the empty case as "a manual-only webhook" — a mode #3196 removed (no manual fire path exists). The skip now warns with the author-time rule id (`webhook/without-triggers`), and the comment tells the truth. Only _active_ rows reach the parse (`where: { active: true }` — verified, not assumed), so a deliberately disabled webhook stays warning-free.
384
+
385
+ **Scope honesty:** field rules and the webhook rule get the runtime twin. `view/layout-without-binding` stays author-time-only — views don't register through this choke point and the renderer half of the evidence lives in objectui.
386
+
387
+ Tracked in #4544. This closes the ADR-0078 loop end to end: author-time error, runtime warning, one shared predicate deciding both.
388
+
389
+ - bb1ce2e: fix(plugin-auth,plugin-webhooks): retire a dead degrade branch and an implicit transitive dependency (ADR-0116 follow-ups, #4187)
390
+
391
+ Two concrete findings from the ADR-0116 consumer-side audit, plus the
392
+ authoring rule that would have prevented both.
393
+
394
+ **`plugin-auth` claimed a fallback it did not have.** `init()` ran
395
+ `const dataEngine = ctx.getService('data'); if (!dataEngine) { warn('No data
396
+ engine service found - auth will use in-memory storage') }`. That branch could
397
+ never execute: `getService` **throws** for an unregistered service rather than
398
+ returning `undefined`, and this plugin declares a hard dependency on ObjectQL
399
+ (which registers `data` unconditionally), so a kernel without the engine fails
400
+ even earlier with `Dependency … not found`. The branch is removed and the real
401
+ contract is declared — `requiresServices: ['data', 'manifest']` — which also
402
+ replaces a trailing `// manifest service required` comment with the
403
+ machine-checked form of the same claim. `AuthManager` keeps its own optional
404
+ `dataEngine` guards: it is usable outside the plugin.
405
+
406
+ **`plugin-webhook-outbox` was protected only transitively.** It resolves
407
+ `manifest` in `init()` with no fallback while depending on
408
+ `com.objectstack.service.messaging`, which in turn depends on ObjectQL, the
409
+ actual provider. That works today and would have broken silently the day
410
+ messaging stopped depending on the engine — surfacing as a crash inside an
411
+ unrelated plugin's init. It now declares `requiresServices: ['manifest']`
412
+ directly.
413
+
414
+ Neither change alters ordering or boot outcomes on any current composition:
415
+ both plugins were already ordered correctly. What changes is what a broken
416
+ composition _says_, and that the guarantees are now checked rather than
417
+ inherited.
418
+
419
+ Docs: `content/docs/plugins/anatomy.mdx` gains the three ADR-0116 fields and
420
+ the decision rule for resolving a service inside `init()` (hard dependency vs
421
+ `optionalDependencies` + `requiresServices`), including the two traps behind
422
+ these fixes — don't rely on a transitive provider, and don't write an
423
+ `if (!svc)` fallback after a bare `getService`. The api-registry example
424
+ declares the contract on all seven of its plugins instead of relying on
425
+ `kernel.use()` order.
426
+
427
+ - 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
428
+
429
+ The AGENTS.md post-task checklist requires breaking changesets to carry their
430
+ FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
431
+ inside the npm package and is what an upgrading agent greps after the tombstone
432
+ error." That delivery path was severed for 68 of the 69 publishable packages:
433
+ npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
434
+ older npm versions — not `CHANGELOG.md`, and the canonical
435
+ `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
436
+ 10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
437
+ 70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
438
+ explicitly.
439
+
440
+ The tombstone-error scenario is precisely the one where the repo is out of
441
+ reach — the upgrading agent has `node_modules` and nothing else — so the
442
+ migration text has to ride in the tarball. Every publishable package now
443
+ declares `CHANGELOG.md` in `files`, and the canonical whitelist is
444
+ `["dist", "README.md", "CHANGELOG.md"]`.
445
+
446
+ The other half is the gate: `check:published-files` gains a fifth invariant,
447
+ COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
448
+ always-required lint job, so the next package cannot silently sever the path
449
+ again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
450
+ into the canonical set.
451
+
452
+ Consumer-visible change: one more file per install (the package's changelog,
453
+ e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
454
+ node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
455
+ promised.
456
+
457
+ - 462b713: fix(objectql,client): `subscribeData` callbacks receive real `DataEvent`s — the producer now fulfils the declared contract (#4626)
458
+
459
+ `@objectstack/spec/api`'s `DataEvent` declares top-level `id` (uuid,
460
+ required), `type`, `object`, `recordId` (required), `changes?`, `before?`,
461
+ `after?`, `userId?`, `timestamp`. But the producer (the ObjectQL engine)
462
+ published a raw `RealtimeEventPayload` envelope with `{ recordId, after,
463
+ changes }` nested under `payload` and never generated `id`/`userId`, while the
464
+ client SDK force-cast that envelope into the callback (`callback(event as any
465
+ as DataEvent)`). Subscribers who wrote `event.recordId` / `event.changes` —
466
+ exactly what the types promised — compiled green and read `undefined` at
467
+ runtime. The data-side twin of #4602.
468
+
469
+ Producer now fulfils the contract:
470
+
471
+ - `ObjectQL.insert()` / `update()` / `delete()` build a true `DataEvent`
472
+ (generated uuid `id`, flattened top-level fields, `userId` from the
473
+ execution context when the write names an actor) and validate it with
474
+ `DataEventSchema.parse` before publishing. The transport envelope is
475
+ unchanged (`RealtimeEventPayload`, with `payload` carrying the complete
476
+ `DataEvent`), so subscribers keep receiving `{ type, object, payload,
477
+ timestamp }` on the wire.
478
+ - A batch insert publishes one event **per record** (as before), each with its
479
+ own event id.
480
+ - **A multi-row write (`multi: true` → `updateMany` / `deleteMany`) now
481
+ publishes nothing.** Those driver methods return only an affected count, so
482
+ there is no record for a required `recordId` to name; the engine logs a
483
+ warning naming the gap instead of publishing the previous fabrication
484
+ (`recordId: ''`, `after: <affected count>`), which every schema-compliant
485
+ consumer had to reject. **Consequence: webhooks and knowledge sync no longer
486
+ fire for bulk writes** — they previously fired once with an unusable body. A
487
+ real bulk event contract is tracked in #4639.
488
+
489
+ Consumers validate or read the fulfilled shape instead of guessing:
490
+
491
+ - `@objectstack/client`'s `subscribeData` (and therefore
492
+ `@objectstack/client-react`'s `useDataSubscription` /
493
+ `useDataSubscriptionCallback` / `useAutoRefresh`, which delegate to it)
494
+ unwraps the envelope and runs `DataEventSchema.safeParse` at the boundary.
495
+ An off-contract payload is rejected loudly (handler error, callback never
496
+ invoked) — never coerced or passed through. The `as any as DataEvent`
497
+ double-cast is gone, and the `recordId` option now filters on the fulfilled
498
+ event.
499
+ - `@objectstack/plugin-webhooks`' auto-enqueuer reads the required
500
+ `recordId` directly; its `recordId ?? id ?? after?.id ?? before?.id ??
501
+ 'unknown'` fallback chain is gone, and an off-contract event is dropped with
502
+ a warning rather than delivered under the literal id `'unknown'`. Delivered
503
+ webhook bodies now also carry the event's `id`/`type`/`userId`; the record
504
+ itself stays nested under `after` and the envelope keys (`object`,
505
+ `recordId`, `action`, `timestamp`) still win.
506
+ - `@objectstack/service-knowledge`'s event sync reads the record from `after`
507
+ (create/update) and the id from `recordId` (delete) for `data.record.*`.
508
+ It previously indexed the envelope itself as if it were the row, and never
509
+ resolved an id for deletes.
510
+
511
+ - a225ef5: fix(runtime,webhooks): the path object wins on /data/:object/query, and the webhook envelope owns its keys (#3946)
512
+
513
+ Follow-up sweep for the shape behind #3897 and #3933 — a trusted, server-derived
514
+ value written into an object literal with a caller-controlled bag spread OVER
515
+ it. Both of those were in the same block of REST code, so the pattern was swept
516
+ across all 1313 non-test TypeScript files in `packages/`. Nine candidate sites;
517
+ one real, one worth hardening, seven verified clean (recorded in #3946 so the
518
+ next sweep does not re-litigate them).
519
+
520
+ **`POST /data/:object/query` (runtime dispatcher).** The `/data` domain built
521
+ `{ object: objectName, ...body }`, so `{"object":"other", …}` in the body moved
522
+ the read to a different object than the URL named.
523
+
524
+ This is NOT an authorization bypass, and the tests pin why: `callData` gates
525
+ API exposure on `params.object`, so the gate followed the body and agreed with
526
+ the read — an object hidden by `apiEnabled: false` was refused either way. What
527
+ broke is that the URL stopped describing the operation (audit trails, logs, and
528
+ anything keyed on the request path saw object A while object B was read), and
529
+ that one endpoint spoke a second dialect of the contract the REST side had just
530
+ standardised on: the path object wins. The other handlers in that file never had
531
+ the problem — they nest caller data (`data: body`, `query: normalized`) instead
532
+ of splatting it, and the GET-by-id branch already allowlists its query params
533
+ against exactly this pollution.
534
+
535
+ **Webhook delivery envelope.** `auto-enqueuer` built
536
+ `{ object, recordId, action, timestamp, ...payload }`, letting an event payload
537
+ rewrite the envelope a subscriber receives. Behaviour-neutral for the engine's
538
+ own publishers — `data.record.*` payloads are `{ recordId, after, changes }`
539
+ with record fields nested under `after`, so none of those four keys collide
540
+ today — but the shape was wrong, and the `payload.id` fallback right above it
541
+ suggests publishers that flatten record fields do exist. Envelope keys are
542
+ written last now.
543
+
544
+ - b5f9397: fix(sharing,runtime): a `sort` passed straight to the engine never ordered anything; migrate every in-repo engine call to canonical QueryAST keys (#4346)
545
+
546
+ Two changes with different weights, from one sweep of every in-repo engine
547
+ call site that still speaks a deprecated alias.
548
+
549
+ **The bug — three dropped sorts.** #4346 made the engine fold `filter`→`where`
550
+ and `top`→`limit` on all six methods. The other four pairs in
551
+ `RPC_QUERY_ALIAS_SLOTS` (`select`, `sort`, `skip`, `populate`) are folded at
552
+ the RPC/wire layer only — their values need shape lowering that belongs to
553
+ those layers — and a **direct `engine.find()` never crosses that layer**. Three
554
+ call sites passed `sort` there, so it rode onto the AST untouched, every
555
+ driver's `Array.isArray(query.orderBy)` guard declined to emit an ORDER BY, and
556
+ the query returned an ordinary-looking, arbitrarily-ordered result:
557
+
558
+ | call site | asked for | actually got |
559
+ | ----------------------------------- | ------------------------------------------------- | --------------------------- |
560
+ | `share-link-routes.ts` | shared AI conversation messages, `created_at asc` | messages in arbitrary order |
561
+ | `runtime/domains/share-links.ts` | same route, runtime-domain copy | same |
562
+ | `share-link-service.ts` `listLinks` | the 200 most recent share links | an arbitrary 200 |
563
+
564
+ All three combine the dropped sort with a `limit` — the "latest N" shape whose
565
+ failure #4226 spelled out: an unapplied sort returns rows in arbitrary order,
566
+ which `limit` then slices into an arbitrary page. #4226 fixed that in the wire
567
+ normalizer; these calls sit one layer below it. `listLinks` had no test at all,
568
+ which is why it went unnoticed. Now pinned — on the option bag the engine
569
+ receives, not on row order, because the failure is that the key never becomes
570
+ `orderBy` and a fake engine honouring either spelling would pass either way.
571
+
572
+ **The cleanup — 27 no-op renames.** Every remaining in-repo engine call passing
573
+ `filter` now passes `where` (approvals 5, auth 2, reports 6, sharing 11,
574
+ webhooks 2, plus the one `filters` in a spec doc example). These are strict
575
+ no-ops since #4346 folds the alias — the point is that the framework stops
576
+ depending on a spelling it asks users to migrate off, which is a prerequisite
577
+ for ever retiring the aliases. Service-level `filter` PARAMETERS (each
578
+ service's own public API, e.g. `listRequests(filter)`) are deliberately
579
+ untouched — those are not engine option bags.
580
+
581
+ Two of the renamed calls were live victims of the #4346 bug rather than
582
+ cosmetic: `auth-manager`'s `stampIdentitySource` read the table's first row via
583
+ `findOne({filter})` and counted the whole table via `count({filter})`, so a
584
+ federated sign-in never stamped `source: 'idp_provisioned'`. #4346 already
585
+ corrected the behaviour; this makes the call say what it means.
586
+
587
+ - 8af76ae: The i18n extractor's default locale now tracks the source instead of merging (#8543), and the approval vocabularies carry authored English labels in the contract (#8580).
588
+
589
+ - `os i18n extract` merge mode no longer applies to the default locale: `en` is a copy of the source, not a translation, so an edited label/description/help now reaches the regenerated `en` bundle instead of being silently shadowed by the stale entry forever (53 stale entries had accumulated across 6 packages under the old behavior; all rewritten here). Translated locales (`zh-CN` / `ja-JP` / `es-ES`) keep merge semantics exactly as before — no existing translation is overwritten.
590
+ - Bare-string and label-less select options now seed through the extractor's derived channel: the machine value still seeds the skeleton, but the coverage gate no longer demands "translations" of machine identifiers, and a copied value can no longer masquerade as authored display text.
591
+ - New `@objectstack/spec/contracts` exports `APPROVAL_STATUS_LABELS` and `APPROVAL_ACTION_KIND_LABELS`: the authored English for `sys_approval_request.status` (previously living only in the generated `en` bundle) and `sys_approval_action.action` (previously shipping raw machine values such as `submit` / `request_info` — the #7232 humanization missed this sibling field). Both columns derive their option labels from these maps; the regenerated `en` bundles copy them verbatim.
592
+
593
+ - aff9e56: fix(i18n): translate the platform packages' declared surface, and gate all nine bundles instead of one (#3762)
594
+
595
+ Only `platform-objects` was wired into a translation-drift check. The other
596
+ **eight** packages shipped a `scripts/i18n-extract.config.ts` that nothing ever
597
+ ran — and four of them had already drifted out of sync with the schema, exactly
598
+ the rot `pnpm check:i18n` exists to catch, one directory over.
599
+
600
+ **Translated.** `plugin-security` (45 strings per locale), `plugin-webhooks`
601
+ (15), `plugin-audit` (8), `plugin-sharing` (7) and `service-storage` (7) are now
602
+ at **zero** untranslated declared strings in zh-CN / ja-JP / es-ES — 246
603
+ translations. Most were newly _visible_ rather than newly missing: #3753 taught
604
+ the coverage detector to walk action `params`, `resultDialog`, `listViews` and
605
+ the rest of the declared surface, and these are what it found.
606
+
607
+ Wording was harvested from the repo's own bundles wherever a string was already
608
+ translated somewhere (1382 unambiguous source strings), so `Created At` reads
609
+ `创建时间` here because that is what it reads everywhere else, rather than a
610
+ fresh invention. Protocol tokens are deliberately left identical across locales:
611
+ `GET` / `POST` / `PUT` / `PATCH` / `DELETE`, `ETag`, `ACL`, `URL`.
612
+
613
+ **Gated.** `scripts/check-i18n-bundles.mjs` replaces the single-package
614
+ `pnpm check:i18n` and checks all nine. It does not restate each package's
615
+ command — it parses the one already documented in that config's own docstring
616
+ and runs it, so the documented regenerate command and the gate cannot diverge.
617
+ The coverage ratchet grows the same way, from `examples/*` to twelve configs;
618
+ eight of them sit at zero, which makes it the strict gate there.
619
+
620
+ **Fixed a real truncation bug it exposed.** `os lint --json` on a large config
621
+ came out of a pipe cut off at exactly 65536 bytes — `console.log(big)` followed
622
+ by `process.exit(1)` tears the process down before an async pipe write drains,
623
+ while an interactive run (stdout is a TTY, written synchronously) looks perfect.
624
+ Every scripted consumer silently got invalid JSON. `emitJson` in
625
+ `packages/cli/src/utils/format.ts` waits for the write to drain and sets
626
+ `process.exitCode` instead; `lint`, `i18n check` and `i18n extract` use it.
627
+ Roughly 30 other CLI commands share the pattern and are not touched here.
628
+
629
+ The nine documented regenerate commands also gain `--no-metadata-forms` (added
630
+ in #3768), since the Studio metadata-form baseline belongs to `platform-objects`
631
+ alone, not to a copy in every plugin.
632
+
633
+ Not fixed here: `platform-objects`' own 77-per-locale gap is `apps.*` /
634
+ `dashboards.*` navigation and widget labels, which live outside the `objects`
635
+ subtree and cannot be scaffolded while the package extracts with
636
+ `--objects-only`. That needs an emit decision first — tracked in #3762.
637
+
638
+ - cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)
639
+
640
+ These 20 packages declared no `files` field, so npm fell back to packing the
641
+ whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
642
+ listed **21 files** — 15 under `src/`, three of them unit tests
643
+ (`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
644
+ build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
645
+ publish time rather than instead of it, so consumers were installing the
646
+ TypeScript sources and the test suite alongside the artifact they asked for.
647
+
648
+ Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
649
+ that already did. Nothing a consumer imports moves: every `main` / `types` /
650
+ `exports` target in all 20 already resolved inside `dist/`, which the new
651
+ `check:published-files` guard verifies rather than assumes. The visible change
652
+ is a smaller install and a smaller dependency-scanning surface — `npm pack` on
653
+ `@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.
654
+
655
+ The other half of the fix is the gate. Half the packages declaring `files` and
656
+ half not was the #3786 shape — a hand-copied convention with nothing enforcing
657
+ it, where whoever forgets the line gets no signal at all. `check:published-files`
658
+ (new, wired into the always-required `lint` job) holds every non-private
659
+ workspace package to four invariants: `files` is **declared**; it is
660
+ **sufficient** (covers every entry point, so tightening a whitelist cannot ship
661
+ a package that fails to resolve); it is **minimal** (admits no test, test-harness
662
+ config or build script); and anything beyond `dist` + `README.md` is
663
+ **registered** with a reason, reconciled in both directions so a stale exemption
664
+ is an error rather than dead text. `@objectstack/spec` is the one package with
665
+ registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
666
+ `CHANGELOG.md` are product, not build input.
667
+
668
+ This also closes an assumption #4206 was resting on. Excluding `<pkg>/scripts/**`
669
+ from the docs-drift implementation test is sound only while no package publishes
670
+ `scripts/` as runtime code; that held, but it held because someone read all three
671
+ offenders by hand. It is now checked on every PR.
672
+
673
+ - 52281b0: chore(i18n): purge the dead sys_webhook_delivery translation block and guard against recurrence
674
+
675
+ `sys_webhook_delivery` was removed when webhook delivery moved to
676
+ `@objectstack/service-messaging` (`sys_http_delivery`, ADR-0018 M3), but a full
677
+ translation block for it lingered in the four generated plugin-webhooks i18n
678
+ bundles (en/zh-CN/ja-JP/es-ES) — dead weight bound to an object that no longer
679
+ exists, and destined to be dropped silently (with any curated strings) on the
680
+ next `os i18n extract`.
681
+
682
+ - Removed the stale `sys_webhook_delivery` block from all four locale bundles
683
+ (surgical; the `sys_webhook` block is untouched).
684
+ - Corrected three stale `sys_webhook_delivery` doc comments (platform-objects
685
+ `integration/index.ts` + `setup.app.ts`, plugin-webhooks `sys-webhook.object.ts`)
686
+ that still named it as a plugin-webhooks-owned object.
687
+ - Rolled out the platform-objects `bundle-ownership` test guard (#2834 ⑤ /
688
+ ADR-0029 D8) to the eight packages that own i18n bundles, so a stray object
689
+ block in a generated bundle now fails the build instead of dying silently.
690
+ - That guard immediately surfaced a live-object omission: `sys_capability` was
691
+ present in plugin-security's bundles with curated translations but had been
692
+ dropped from its extract config — re-added to the config so the strings are
693
+ preserved, rather than deleted.
694
+
695
+ - 30f1b74: fix(plugins): a declared item reaches its schema intact — retire the `i?.content ?? i` unwrap from plugin read paths (#8378)
696
+
697
+ Ten production reads over `SchemaRegistry.listItems` unwrapped every declared
698
+ item as `i?.content ?? i`, presuming a `{ name, content }` storage envelope.
699
+ That envelope has **no producer**. Re-measured at these seams rather than
700
+ inherited from #7519's measurement of `MetadataFacade`:
701
+
702
+ - `registerMetadataCollections` (objectql) registers each stack-collection
703
+ element as-is — `registerItem(type, item, 'name')`, no boxing;
704
+ - `loadMetaFromDb` registers `convertStoredItem(JSON.parse(record.metadata))` —
705
+ the parsed body, never the `sys_metadata` row (whose body column is
706
+ `metadata`, not `content`);
707
+ - the facade's own interim boxing of non-object values, the one writer that ever
708
+ produced the shape, was removed by #8349.
709
+
710
+ **Removal is a fix, not a cleanup.** None of the types read through these seams
711
+ — `permission`, `position`, `capability`, `object`, `sharingRule`, `webhook`,
712
+ `emailTemplate` — declares a stored `content` key; every one of them rejects it
713
+ as an unrecognized key. So wherever the key did appear the unwrap replaced a
714
+ whole authoring document with one of its values, and `''` — falsy but
715
+ non-nullish — passed `??` and then died at the reader's own `filter(Boolean)`,
716
+ dropping the item with no warning, no count and no row.
717
+
718
+ **On email templates the harm was sharpest, and it is the one users will
719
+ notice.** `content` really is a spelling an author can write there:
720
+ `EmailTemplateDefinitionSchema` lists it in its `strictObject` **aliases** table
721
+ (`content: 'bodyHtml'`). That table is a _rejection_ facility, not a conversion —
722
+ it feeds `strictUnknownKeyError`, which runs only on the `unrecognized_keys`
723
+ path and only builds a message; nothing rewrites the key, and the ADR-0087
724
+ conversion layer has no `email_template` entry either. The schema was therefore
725
+ always ready with the author's fix, and the unwrap was the one thing standing
726
+ between the author and it: the HTML string reached
727
+ `EmailTemplateDefinitionSchema.parse()`, which answered `Invalid input: expected
728
+ object, received string`, and the boot warning's `name` field came back
729
+ `undefined` — so an operator could not even tell **which** template had failed.
730
+
731
+ A template authored with `content` now yields what it was always meant to:
732
+
733
+ > Unrecognized key(s) on this email template: `content`. Did you mean
734
+ > `content` → `bodyHtml`?
735
+
736
+ …named against the template it came from, and counted as `skipped` rather than
737
+ vanishing.
738
+
739
+ No behaviour changes for spec-valid metadata: the reads hand back exactly the
740
+ documents they always did.
741
+
742
+ - 2465133: fix(plugins): sweep the service-lookup erasures out of the plugin composition roots, and fix the two alias-only HTTP reads it exposed (#4251 B5)
743
+
744
+ Batch B5 of the #4251 sweep: the seven remaining `packages/plugins/*` composition
745
+ roots. 35 lookup sites that had been erased to `any` now carry the slot's
746
+ contract, so the compiler checks what each plugin actually calls on the service
747
+ it resolved. The ratchet drops 143 sites / 32 files to 108 / 25.
748
+
749
+ **Two real defects, both of the shape this sweep exists to find.** Approvals'
750
+ actionable-link pages (ADR-0043) and sharing's public share-link REST routes each
751
+ read the HTTP server under `http-server` _only_ — the deprecated alias. The
752
+ ledger records `http.server` as canonical and as the only name present on every
753
+ provider path: `runtime.ts`'s `config.server` path registers no alias at all. On
754
+ that path both lookups threw, the surrounding `catch` swallowed it, and the
755
+ routes silently never mounted — approval e-mail action links 404'd and the
756
+ share-link surface was absent, with nothing in the log to say so. Both reads are
757
+ now canonical-first with the alias as fallback, each name in its own `try`
758
+ because `getService` throws on an empty slot (so `a() ?? b()` inside one `try`
759
+ never reaches `b` — the same correction #4393 made in metadata and
760
+ cloud-connection).
761
+
762
+ Typing choices follow the batch method: pure data-plane consumers take the
763
+ narrow contract (`IDataEngine` in reports), consumers that bind hook or
764
+ middleware seams take the engine seen whole (`IObjectQLEngine` in approvals,
765
+ sharing and pinyin-search), and slots with no contract get a **named** local
766
+ surface rather than `any` — plugin-email's `MailSettingsSurface`, and the
767
+ surfaces the consuming packages already declared (`ApprovalMessagingSurface`,
768
+ `SharingSecurityProbe`, `ReportEmail`). A named surface that omits a member
769
+ still makes the compiler name every call site; `any` says nothing.
770
+
771
+ No behaviour change beyond the two alias reads. No contract changes.
772
+
773
+ - b45c71e: fix(plugin-security,plugin-sharing,plugin-webhooks,platform-objects,service-messaging,spec): five tenant-scoped declared unique indexes become per-organization (#8554)
774
+
775
+ Five platform objects declared their uniqueness as a table-level index with bare
776
+ `unique: true`. At the DECLARED-index level that is the positional spelling of
777
+ `'global'` — the listed columns verbatim — so on a tenant-scoped object each
778
+ materialized an **installation-wide** unique index. (Field-level `unique: true`
779
+ means the opposite, per-organization, and has since #3696; `packages/lint` names
780
+ that divergence "the #4986 trap" and warns on it via
781
+ `unique/unscoped-declared-index`.) These are the fourth act of the class ruled on
782
+ 2026-08-13, after `sys_user_preference` / `sys_capability` (#8461) and
783
+ `sys_position` (#8556).
784
+
785
+ | object | package | was | now |
786
+ | ----------------------------- | ------------------- | ---------------------------------- | --------------------------------- |
787
+ | `sys_permission_set` | `plugin-security` | `[name]` global | `[name]` per organization |
788
+ | `sys_sharing_rule` | `plugin-sharing` | `[name]` global | `[name]` per organization |
789
+ | `sys_webhook` | `plugin-webhooks` | `[name]` global | `[name]` per organization |
790
+ | `sys_email_template` | `platform-objects` | `[name, locale]` global | `[name, locale]` per organization |
791
+ | `sys_notification_preference` | `service-messaging` | `[user_id, topic, channel]` global | same, per organization |
792
+
793
+ Measured live on a real engine before the fix — two organizations, the same key,
794
+ `OS_TENANCY_POSTURE=isolated`, driving the real shipped declarations. All five
795
+ reproduced identically:
796
+
797
+ ```
798
+ org_jia POST the key → 201
799
+ org_yi POST the SAME → 409 UNIQUE_VIOLATION
800
+ org_yi POST an unused → 201 ← the control that makes it an oracle
801
+ org_yi GET the key → total 0 ← refused by a row it cannot see
802
+ ```
803
+
804
+ Two consequences, both removed. **A cross-tenant existence oracle:** the 409 is a
805
+ per-value answer about a row the caller cannot read, so an organization could
806
+ enumerate another organization's permission-set, sharing-rule, webhook and
807
+ template naming. **A functional dead end:** the second organization simply could
808
+ not use the name, and the refusal did not say why. For
809
+ `sys_notification_preference` the shape is the one #8323 measured on
810
+ `sys_user_preference` — a user belonging to two organizations could not hold
811
+ independent per-topic delivery toggles.
812
+
813
+ ## ⚠️ Operators: a migration is REQUIRED, and deploying this release is not it
814
+
815
+ Respelling a declared index changes its generated **name**. On an existing
816
+ database `initObjects` is additive: it creates the new per-organization composite
817
+ at boot and **never drops the old global index**, which goes on enforcing. Until
818
+ the retirement is applied, a deployed installation that has taken this release is
819
+ still enumerable — that is asserted as a test, not assumed.
820
+
821
+ Run the migration:
822
+
823
+ ```
824
+ os migrate plan # shows one `replace_unique_index` per object, categorised `safe`
825
+ os migrate apply # no --allow-destructive needed
826
+ ```
827
+
828
+ Each object plans as **one pure relaxation**, not as two findings. That matters:
829
+ if it read as "composite missing" (safe) plus "old global index orphaned"
830
+ (destructive, opt-in), an operator applying only the safe half would keep the
831
+ global index — keep the defect — while the plan read as applied. The `#8461`
832
+ `replace_unique_index` arm covers all five unchanged (no driver change in this
833
+ release), applies CREATE-before-DROP so uniqueness is never unenforced in
834
+ between, drops the legacy index only once the replacement is confirmed present,
835
+ preserves every row, and converges to no drift.
836
+
837
+ Two columns are worth an operator's attention:
838
+
839
+ - `sys_notification_preference`'s replacement index name is **hash-suffixed** —
840
+ `uniq_sys_notification_preference_a22d7d27` — because the natural name is 70
841
+ characters and the limit is 60. That is expected, not corruption.
842
+ - Rows with no `organization_id` (platform/seed rows) stay unique **among
843
+ themselves**: the organization key part is NULL-safe
844
+ (`COALESCE(organization_id, '__global__')`, ADR-0120 D3), so seeding by name
845
+ keeps working and a tenant may hold its own row of the same name.
846
+
847
+ ## Not breaking
848
+
849
+ A relaxation admits key pairs that were previously refused and refuses nothing
850
+ that previously succeeded, so no caller that worked before fails now. Every read
851
+ path for these five objects goes through the tenant-scoped data API, so no
852
+ consumer resolves one of these names across organizations expecting at most one
853
+ row. Shipped as `patch` for that reason — the same call #8556 made for the same
854
+ shape.
855
+
856
+ Published text carrying the bare uniqueness claim was corrected at its source and
857
+ the generated reference pages regenerated (`security/permission.mdx`,
858
+ `automation/webhook.mdx`, and `integration/connector.mdx`, which embeds the same
859
+ webhook schema), together with the `sys_permission_set` field description, its
860
+ clone-dialog help text, the `sys_webhook` field description, and the matching
861
+ translation bundles in all four shipped locales.
862
+
863
+ - f46e987: fix(plugin-webhooks): a webhook holding an encrypted signing secret re-arms the moment the CryptoProvider registers, instead of ~60s later (#8022)
864
+
865
+ For roughly **60 seconds after every server start**, a webhook whose
866
+ `signing_secret` is encrypted (the population #7799 created) was **not
867
+ subscribed**. A record change in that window produced no delivery **and no
868
+ `sys_http_delivery` row at all** — no dead letter, no retry, no durable trace
869
+ that anything was missed — while `GET /api/v1/data/sys_webhook/` kept reading
870
+ `active: true`, so the webhook looked armed in Setup the whole time. It
871
+ self-healed at the next periodic cache refresh, which is why it was invisible to
872
+ anyone not watching that window.
873
+
874
+ **The fail-closed behaviour is unchanged and is not the bug.** Dropping a
875
+ subscription whose stored key cannot be recovered — rather than delivering it
876
+ unsigned — is #7799's whole point and still holds: the signature is the
877
+ receiver's only proof of origin, and a webhook that stops arriving gets
878
+ investigated while one that keeps arriving unsigned teaches the receiver to
879
+ accept unauthenticated traffic. What was wrong is that a fail-closed drop
880
+ outlived its own cause.
881
+
882
+ **The ordering.** It was never a race that sometimes went the other way. Plugins
883
+ run inside `kernel:ready`, which `runtime.start()` completes; the host's
884
+ composition root calls `engine.setCryptoProvider(...)` only _after_
885
+ `runtime.start()` returns (`packages/cli/src/commands/serve.ts`,
886
+ `packages/verify/src/harness.ts`). So `AutoEnqueuer`'s first subscription-cache
887
+ build reliably preceded the capability it needs, dropped every secret-bearing
888
+ row on what it could see, and nothing re-read until the periodic refresh.
889
+
890
+ `ObjectQL` now reports the registration (`onCryptoProviderChange(listener)`,
891
+ fired after the provider is in place), and the auto-enqueuer subscribes
892
+ **before** its first build and rebuilds the cache when it fires. Re-arming is
893
+ immediate and event-driven — no polling, and no shorter-but-still-present
894
+ window. The re-arm deliberately does not join an in-flight refresh: the build
895
+ most likely running at that moment is the pre-registration one, and joining it
896
+ would report success having re-armed nothing.
897
+
898
+ The channel is feature-detected, as `resolveSecretField` already was — this
899
+ plugin takes no dependency on `@objectstack/objectql`. An engine without it keeps
900
+ the previous behaviour, with the periodic refresh as the backstop.
901
+
902
+ **The drop is also no longer quiet.** A subscription dropped for an unresolvable
903
+ key now reports at `error` with the consequence and the fix stated in the
904
+ message, and carries an ADR-0112 `code`/`status` pair (`INTERNAL_ERROR`/500) in
905
+ its metadata — the same pair the seeder's refusal for the same cause already
906
+ carried. Per AGENTS.md it is said **once** per outage per webhook rather than
907
+ every refresh cycle, and a webhook that recovers and breaks again is loud again.
908
+
909
+ Unaffected, and verified still true: the secret's bytes appear nowhere in
910
+ `sys_webhook` or in a delivery row, deliveries carry `signature` and never the
911
+ key (#7722), and a delivery whose key exists only as ciphertext after a restart
912
+ still produces the byte-identical HMAC receivers already verify.
913
+
914
+ - 1602949: fix(plugin-webhooks): webhook custom `headers` are encrypted at rest instead of riding `definition_json` in cleartext (#7986)
915
+
916
+ `#7799` moved the webhook **signing secret** out of `sys_webhook.definition_json`
917
+ into an encrypted `signing_secret` column. It did not move the custom **headers**
918
+ map — and `headers` is the ordinary place an `Authorization: Bearer …` goes.
919
+
920
+ `sys_webhook` declares **no `enable` block at all**, so it keeps the full default
921
+ data API: an ordinary `GET /api/v1/data/sys_webhook` handed the whole header map,
922
+ credentials included, to every persona that can read the object. Unlike the
923
+ delivery table's copies, nothing ages this out — the configuration row is
924
+ retained for the life of the webhook.
925
+
926
+ This is a **scope-of-the-original-fix** finding, not a regression: the exposure
927
+ predates `#7799` and nothing that card did made it worse. What was wrong was the
928
+ conclusion a reader would reasonably draw from it — that webhook credentials are
929
+ no longer in a blob.
930
+
931
+ **What changed.** The authored `headers` map now lands in a new
932
+ `sys_webhook.headers_secret` column on the engine's encrypted credential channel,
933
+ exactly as `signing_secret` does: the engine encrypts it into `sys_secret`, the
934
+ row keeps only an opaque `secret:<id>` ref, and every read path returns a mask.
935
+ `definition_json` carries the same envelope minus both credential passengers. The
936
+ auto-enqueuer recovers the map server-side through `engine.resolveSecretField()`
937
+ on the **same** cache refresh that recovers the signing key, and the existing
938
+ boot sweep (`migrateLegacyWebhookSecrets`) now moves already-persisted cleartext
939
+ headers out of the blob in the same single, idempotent update it uses for the
940
+ key.
941
+
942
+ **Nothing about authoring changes.** Authors still write
943
+ `headers: { … }` on `defineWebhook()`, `webhook.zod.ts` is untouched, and every
944
+ authored header is still delivered on the wire byte-for-byte.
945
+
946
+ **The whole map moves, not just the credential-looking entries.** Only some
947
+ entries are credentials and the platform cannot tell which. Guessing from the
948
+ header name (`authorization`, `x-api-key`, …) is fail-**open** on exactly the
949
+ custom spellings — `X-Acme-Token` — most likely to be one, and a heuristic that
950
+ silently passes the header that mattered is worse than none because it reads as
951
+ coverage. Letting the author declare which are sensitive is a change to the
952
+ authoring envelope and belongs to the spec surface. The cost this shape is
953
+ accused of is measured and small: `definition_json` is a raw JSON textarea
954
+ pending a real builder, so what an admin loses is the ability to read back a
955
+ `Content-Type` they typed.
956
+
957
+ **Fail-closed, and symmetric with `#7799`.** With no CryptoProvider the engine
958
+ refuses the write rather than storing cleartext; a stored map that cannot be
959
+ decrypted **drops** the subscription rather than delivering it with its headers
960
+ silently missing. That drop is deliberately the same trade the signing secret
961
+ makes: against an endpoint that does not require the header, a delivery missing
962
+ its `Authorization` **succeeds** while quietly deviating from the configuration
963
+ the author wrote, and nothing records that it went out incomplete. Subscriptions
964
+ dropped this way re-arm on CryptoProvider registration exactly as `#8022` made
965
+ them — the header map is resolved on the same rebuilt cache as the key, so a
966
+ re-arm can never produce a correctly-signed delivery with no headers on it.
967
+
968
+ **This does not close the exposure end to end.** The same headers are still
969
+ written in cleartext to `sys_http_delivery.headers_json` at enqueue time, and
970
+ that table is readable over the data API (`apiMethods: ['get','list']`, 30-day
971
+ retention). Measured after this change: the credential is still recoverable
972
+ there. Closing that half needs a decision outside this package and is tracked on
973
+ #7986; `sys_email.headers_json` (the same shape, on the email delivery row) is
974
+ untouched here for the same reason.
975
+
976
+ - 06306f1: fix(service-messaging): stop persisting webhook HMAC signing secrets on every delivery row (#7722)
977
+
978
+ `sys_http_delivery` carried the caller's `signingSecret` verbatim, once per
979
+ delivery attempt, in a plain `signing_secret` column — and that table is
980
+ readable over the ordinary data API (`GET /api/v1/data/sys_http_delivery`).
981
+ Anyone who could read deliveries recovered the shared key that authenticates
982
+ ObjectStack to the receiver, for **every** subscriber at once. The signature is
983
+ the receiver's only proof of origin, so the blast radius reaches outside the
984
+ deployment: a leaked key mints payloads the receiver accepts as genuine, and
985
+ rotating it means re-coordinating with every receiver operator.
986
+
987
+ **The row now carries the signature, not the key.** A delivery's body is
988
+ decided at enqueue and replayed byte-for-byte by every retry and by
989
+ `redeliver()` — so the HMAC has exactly one correct value for the row's whole
990
+ life. `enqueue()` computes it once from the producer's secret and stores only
991
+ the result (`signature`, `sha256=<hex>`); the secret is consumed and dropped.
992
+ The stored value is what the receiver is handed on the wire anyway and is
993
+ one-way in the key, so reading a delivery row tells you what was sent, not how
994
+ to forge something else.
995
+
996
+ Signing behaviour on the wire is unchanged: `X-Objectstack-Signature` still
997
+ carries `sha256=HMAC-SHA256(raw body, secret)` and verifies against the
998
+ subscriber's secret exactly as before — now pinned by tests that recompute the
999
+ HMAC over the delivered body rather than asserting a header is merely present,
1000
+ and by an at-rest guard that byte-scans every column of a real delivery table
1001
+ after a real delivery.
1002
+
1003
+ Producers are unaffected: `enqueueHttp({ …, signingSecret })` keeps its shape
1004
+ for both callers (webhook fan-out and the Flow `http` node), and the fix sits at
1005
+ the outbox, so both stop writing cleartext.
1006
+
1007
+ **Upgrading.** The `signing_secret` column is no longer declared, so an existing
1008
+ database keeps it as an unmapped column holding the old cleartext until it is
1009
+ dropped: run `os migrate plan` and apply the reported `drop_column` op (it is
1010
+ classified destructive, so it is never applied unattended). Until then those
1011
+ rows also age out on the table's existing 30-day telemetry retention. Rotate any
1012
+ signing secret that was exposed. Code reading `HttpDelivery.signingSecret` off a
1013
+ row should read `signature` instead — the secret is not available there by
1014
+ design.
1015
+
1016
+ - bbe05de: A dropped webhook subscription now leaves a durable record, and no operator action can turn that record into an unsigned delivery (#8069).
1017
+
1018
+ When the auto-enqueuer cannot decrypt a webhook's signing secret or its custom header map, it drops the subscription rather than delivering unsigned (#7799, #7986). Until now the drop left no `sys_http_delivery` row at all: every matching record change was discarded with nothing an operator reading the delivery table could find. `#8043` made that loud in the logs; it did not make it durable.
1019
+
1020
+ Each discarded event is now recorded as a `sys_http_delivery` row with `status: dead`, `attempts: 0`, and the cause and remedy in the existing `error` column — so it appears in the object's existing "Failures" view with no new lifecycle state and no migration.
1021
+
1022
+ The record is unsendable by construction, which is the half that matters:
1023
+
1024
+ - `redeliver()` refuses any terminal row with `attempts: 0`. Such a row was never sent, so re-sending it would be a **first** delivery — and a parked row carries no HMAC signature, because the secret that would have produced one is exactly what went missing. New error code `DELIVERY_NEVER_SENT` (409 on `POST /api/v1/webhooks/redeliver`).
1025
+ - `redeliver()` also consults a producer-registered guard, so a webhook row whose `sys_webhook` subscription was deleted, or whose stored signing secret can no longer be recovered, is refused rather than replayed. A guard whose own lookup fails refuses too.
1026
+ - The parked row never carries the authored header map, so a credential is not copied onto a row that will sit out the retention window without ever being sent.
1027
+
1028
+ Redelivery of a genuine dead-letter is unchanged: the same bytes, the same signature.
1029
+
1030
+ - bf1ea92: fix(plugin-webhooks): a stored header map that cannot be recovered parks the subscription instead of arming it and delivering the headers MISSING (#8558)
1031
+
1032
+ A webhook whose `sys_webhook.headers_secret` held a value that did not come back
1033
+ as a header map was treated as **authored without custom headers**. The
1034
+ subscription armed, every matching record change was delivered, and the entire
1035
+ authored map — the ordinary place an `Authorization: Bearer …` goes — was
1036
+ silently absent. Nothing logged, nothing dropped, and
1037
+ `GET /api/v1/data/sys_webhook` kept reporting `active: true` with the header
1038
+ column masked, so both the operator and the Setup UI still read "custom headers
1039
+ are configured".
1040
+
1041
+ Measured end to end against a real engine, what reached the receiver was worse
1042
+ than "a delivery with something missing": the request SUCCEEDED
1043
+ (`sys_http_delivery.status = 'success'`) carrying a byte-correct
1044
+ `X-Objectstack-Signature`. The signature is the receiver's proof the request is
1045
+ genuinely ours, so a receiver that authenticates by signature had every reason
1046
+ to accept a request that no longer matched the configuration its operator wrote.
1047
+ Against an endpoint that requires the credential the result is a 401 nobody
1048
+ attributes correctly; against one that does not — a routing `X-Tenant-Id`, an
1049
+ `X-Environment: staging` — the delivery is simply wrong and nobody finds out.
1050
+
1051
+ The cause was one return value carrying two facts. `resolveWebhookHeaders`
1052
+ answered `undefined` both for _"the author configured no custom headers"_ —
1053
+ legitimate, `headers` is optional on the envelope — and for _"a map is stored
1054
+ and did not come back as one"_. Its caller acts on the first reading, so the
1055
+ second became the first. This is the sibling of the signing-secret collapse
1056
+ (#8542) on the same seam's other credential, and the file's own header comment
1057
+ already promised the opposite: _"It does not deliver partially. A row whose
1058
+ stored headers cannot be resolved DROPS the subscription."_
1059
+
1060
+ **This path is wider than the signing-secret one, not symmetric to it.** A
1061
+ signing secret is an opaque scalar, so any non-empty answer is a usable key and
1062
+ only the empty string collapsed. A header map's CONTENT decides, and
1063
+ `parseStoredHeaders` answers `undefined` — correctly, for its own job — for every
1064
+ string that is not a flat JSON object of string values. Four states reach the
1065
+ seam, all confirmed against a real engine:
1066
+
1067
+ - the `sys_webhook` row is deleted between the enqueuer's cache read and the
1068
+ per-row dereference;
1069
+ - the column holds something that is not a `secret:` ref — reachable only
1070
+ through a write that bypasses the engine (a column edited in SQL, a dump
1071
+ restored without its `sys_secret` rows, a seed script writing at driver level);
1072
+ - the stored value decrypts to an **empty string**;
1073
+ - the stored value decrypts to a perfectly readable string that is **not a flat
1074
+ string map** — `{}`, `[]`, `{"X-Count": 5}`, a nested object, or any typo.
1075
+ This is the widest road rather than an exotic one: `headers_secret` is an
1076
+ admin-authorable field whose own description instructs the author to type a
1077
+ JSON object into it, and every one of these spellings is accepted by the
1078
+ ordinary data API, encrypted like any other value, and left behind a
1079
+ perfectly valid ref that reads back as the mask.
1080
+
1081
+ The fix is at the seam, so no consumer has to re-derive the rule: presence is
1082
+ already decidable there (`headers_secret` is a map only in the plaintext — at
1083
+ the storage layer it is an ordinary scalar `secret` column, so a set map comes
1084
+ back from the generic read path as the engine's mask and an unset one as `null`),
1085
+ and stored headers that do not come back as a map now raise rather than
1086
+ answering `undefined`. They therefore reach `AutoEnqueuer.attachHeaders` exactly
1087
+ the way a throwing resolver already did — the subscription is parked, the
1088
+ discarded event lands in `sys_http_delivery` with a cause (#8069), and the
1089
+ operator gets the existing remedy-bearing say-once `error` carrying
1090
+ `INTERNAL_ERROR` / `500` (ADR-0112) and naming `headers_secret`, so it cannot be
1091
+ confused with the signing secret's identical-looking drop.
1092
+
1093
+ **Unchanged:** a webhook authored with no custom headers at all still arms and
1094
+ delivers — that is a legitimate authored configuration, and it is pinned as the
1095
+ control for this change, as is a webhook whose stored map resolves normally and
1096
+ still delivers every header including the credential entry.
1097
+
1098
+ **What an operator sees after upgrading.** A webhook that was quietly delivering
1099
+ without its headers stops delivering and starts reporting. Re-save the headers
1100
+ as a flat JSON object of string values so the column holds a fresh ref, or
1101
+ **clear** the field to `null` if the webhook is meant to send no custom headers
1102
+ — an empty or unparseable header map is not the same thing as no header map,
1103
+ and only the second one means "send nothing extra".
1104
+
1105
+ - 719a21b: fix(plugin-webhooks): a stored signing secret that cannot be recovered parks the subscription instead of arming it and delivering UNSIGNED (#8542)
1106
+
1107
+ A webhook whose `sys_webhook.signing_secret` held a value that did not resolve
1108
+ was treated as **authored unsigned**. The subscription armed, every matching
1109
+ record change was delivered, and the HMAC signature — the receiver's only proof
1110
+ the delivery came from us — was silently absent. Nothing logged, nothing
1111
+ dropped, and `GET /api/v1/data/sys_webhook` kept reporting `active: true` with
1112
+ the secret column masked, so both the operator and the Setup UI still read
1113
+ "this webhook is signed".
1114
+
1115
+ The cause was one return value carrying two facts. `resolveWebhookSecret`
1116
+ answered `undefined` both for _"the author configured this webhook unsigned"_ —
1117
+ legitimate, `secret` is optional on the envelope — and for _"a key is stored and
1118
+ nothing came back"_. Its caller acts on the first reading, so the second became
1119
+ the first. That is the #7799 signing invariant failing **open**, immediately
1120
+ beside two adjacent failure modes that fail closed and loudly: a resolver that
1121
+ throws, and an engine with no encrypted-field channel, both of which drop the
1122
+ subscription and report at `error`.
1123
+
1124
+ Three states reach the silent path, all confirmed against a real engine:
1125
+
1126
+ - the `sys_webhook` row is deleted between the dispatcher's cache read and the
1127
+ per-row dereference;
1128
+ - the column holds something that is not a `secret:` ref — reachable only
1129
+ through a write that bypasses the engine (a column edited in SQL, a dump
1130
+ restored without its `sys_secret` rows, a seed script writing at driver
1131
+ level). The engine's own write path defends the two obvious routes: an echoed
1132
+ read-mask is dropped and cleartext is re-encrypted;
1133
+ - the stored value decrypts to an **empty string** — reachable through the
1134
+ ordinary data API, which accepts `signing_secret: ""`, encrypts it like any
1135
+ other value, and leaves the column holding a perfectly valid ref.
1136
+
1137
+ The fix is at the seam, so no consumer has to re-derive the rule: presence is
1138
+ already decidable there (a set secret comes back from the generic read path as
1139
+ the engine's mask, an unset one as `null`), and a stored key that does not
1140
+ resolve now raises rather than answering `undefined`. It therefore reaches
1141
+ `AutoEnqueuer.attachSecret` exactly the way a throwing resolver already did —
1142
+ the subscription is parked, the discarded event lands in `sys_http_delivery`
1143
+ with a cause (#8069), and the operator gets the existing remedy-bearing
1144
+ say-once `error` carrying `INTERNAL_ERROR` / `500` (ADR-0112).
1145
+
1146
+ **Unchanged:** a webhook authored with no secret at all still arms and delivers
1147
+ unsigned — that is a legitimate authored configuration, and it is pinned as the
1148
+ control for this change. The redelivery guard (#8069) keeps its behaviour in
1149
+ both directions: a stored-but-unresolvable key is still refused with its own
1150
+ reason, and any other failure still propagates, because "we could not check"
1151
+ must never read as "allowed".
1152
+
1153
+ **What an operator sees after upgrading.** A webhook that was quietly delivering
1154
+ unsigned stops delivering and starts reporting. If the deliveries were meant to
1155
+ be signed, re-save the secret so the column holds a fresh ref. If the webhook
1156
+ was meant to be unsigned, **clear** the field to `null` — an empty secret is not
1157
+ the same thing as no secret, and only the second one means "unsigned".
1158
+
1159
+ - e3a6f6e: fix(webhooks): the subscriber's HMAC signing secret is no longer readable from `sys_webhook` over the data API (#7799)
1160
+
1161
+ `bootstrapDeclaredWebhooks` persisted the whole validated `Webhook` envelope —
1162
+ **`secret` included** — as `definition_json: JSON.stringify(wh)`, and
1163
+ `AutoEnqueuer.parseRow` read `defn.secret` straight back out to sign deliveries.
1164
+ `definition_json` is an ordinary textarea on an admin-authorable object with no
1165
+ restrictive `enable.apiMethods`, so a plain `GET /api/v1/data/sys_webhook`
1166
+ returned the key to **every persona that can read the object**. That key is the
1167
+ receiver's only proof that a delivery came from us.
1168
+
1169
+ This is the remaining half of #7722, which removed the same secret's per-attempt
1170
+ copies from `sys_http_delivery`. Unlike the delivery table, no retention window
1171
+ ever aged these out.
1172
+
1173
+ **What changed.** The authored key now lands in `sys_webhook.signing_secret`, a
1174
+ new `type: 'secret'` column: the engine encrypts it into `sys_secret` on write,
1175
+ keeps only an opaque `secret:<id>` ref on the row, and returns a mask on every
1176
+ read path. `definition_json` carries the same envelope **minus** `secret`. The
1177
+ auto-enqueuer recovers the plaintext server-side when it refreshes its
1178
+ subscription cache.
1179
+
1180
+ **Nothing about authoring changes.** `packages/spec/src/automation/webhook.zod.ts`
1181
+ is untouched — `defineWebhook({ secret })` is written exactly as before, and the
1182
+ delivered `X-Objectstack-Signature` is byte-identical, so no receiver has to
1183
+ change anything.
1184
+
1185
+ **Existing rows are migrated.** A boot sweep moves any cleartext
1186
+ `definition_json.secret` into the encrypted column — including the rows the
1187
+ seeder deliberately never rewrites (`managed_by: 'admin'`, and package rows an
1188
+ admin froze with `customized: true`), which are the ones most likely to hold a
1189
+ real production key. The sweep is idempotent and stores the encrypted copy in
1190
+ the same update that strips the blob, so a failure can never leave a webhook
1191
+ stripped _and_ unsigned. Until a row is swept, signing keeps working from the
1192
+ legacy blob and the enqueuer warns that the value is still exposed.
1193
+
1194
+ **Fail-closed.** With no `ICryptoProvider` wired the engine refuses the write
1195
+ rather than storing cleartext, so a secret-bearing webhook is skipped — and a
1196
+ legacy row is left intact — with an actionable log line carrying an ADR-0112
1197
+ `code`/`status` pair. It is never seeded with an exposed key in a new column.
1198
+
1199
+ Also adds `ObjectQL.resolveSecretField(object, recordId, field)` — the privileged,
1200
+ driver-level dereference of one row's `secret`-typed field. `resolveSecret()` was
1201
+ already documented for "privileged consumers … against the stored ref", but the
1202
+ read mask meant no consumer could obtain that ref; this is why the webhook key
1203
+ can live in the encrypted channel at all. It refuses any field not declared
1204
+ `type: 'secret'`, so it cannot become a mask bypass over a `password` field
1205
+ (plaintext at rest by design — ADR-0100).
1206
+
1207
+ - c95ac80: chore(plugin-webhooks): drop the dead sys_webhook_delivery i18n blocks
1208
+
1209
+ `sys_webhook_delivery` was removed from `@objectstack/plugin-webhooks` when
1210
+ outbound delivery moved to `@objectstack/service-messaging` (`sys_http_delivery`,
1211
+ ADR-0018 M3), but its translation blocks lingered in all four generated locale
1212
+ bundles (en / zh-CN / ja-JP / es-ES) — loaded at runtime yet referenced by
1213
+ nothing, since the object no longer exists in this plugin.
1214
+
1215
+ - Removed the `sys_webhook_delivery` node from each `*.objects.generated.ts`
1216
+ bundle; `WebhooksTranslations` now carries only `sys_webhook`.
1217
+ - Corrected the stale ownership comment on `SysWebhook` that still named
1218
+ `sys_webhook_delivery` as a live sibling.
1219
+
1220
+ (The dangling `SysWebhookDelivery` import in `scripts/i18n-extract.config.ts`
1221
+ was fixed independently on `main` by #3489, so it is not part of this change.)
1222
+
1223
+ - Updated dependencies [50616d9]
1224
+ - Updated dependencies [430dcc2]
1225
+ - Updated dependencies [690ccf2]
1226
+ - Updated dependencies [6a67d7a]
1227
+ - Updated dependencies [333a374]
1228
+ - Updated dependencies [9fe9c1d]
1229
+ - Updated dependencies [3d5c090]
1230
+ - Updated dependencies [e5bd768]
1231
+ - Updated dependencies [08b5a3d]
1232
+ - Updated dependencies [e027b3e]
1233
+ - Updated dependencies [e6ac4bd]
1234
+ - Updated dependencies [c2429b0]
1235
+ - Updated dependencies [445a0c2]
1236
+ - Updated dependencies [d99aeb3]
1237
+ - Updated dependencies [f6609e6]
1238
+ - Updated dependencies [4727eb8]
1239
+ - Updated dependencies [a70358a]
1240
+ - Updated dependencies [0ecc656]
1241
+ - Updated dependencies [06772eb]
1242
+ - Updated dependencies [d4e0809]
1243
+ - Updated dependencies [80334c7]
1244
+ - Updated dependencies [f63cd09]
1245
+ - Updated dependencies [97e7e3c]
1246
+ - Updated dependencies [ce5242c]
1247
+ - Updated dependencies [a7163ea]
1248
+ - Updated dependencies [e6e9379]
1249
+ - Updated dependencies [5823d59]
1250
+ - Updated dependencies [3140f9c]
1251
+ - Updated dependencies [9500ba4]
1252
+ - Updated dependencies [fa3d0cf]
1253
+ - Updated dependencies [af5a224]
1254
+ - Updated dependencies [71f76e1]
1255
+ - Updated dependencies [37b1346]
1256
+ - Updated dependencies [99736a0]
1257
+ - Updated dependencies [fe67e34]
1258
+ - Updated dependencies [fdb4f50]
1259
+ - Updated dependencies [270650f]
1260
+ - Updated dependencies [3aef718]
1261
+ - Updated dependencies [1bd5652]
1262
+ - Updated dependencies [14252d3]
1263
+ - Updated dependencies [7fb436c]
1264
+ - Updated dependencies [879ea13]
1265
+ - Updated dependencies [8828b9e]
1266
+ - Updated dependencies [1ea6bce]
1267
+ - Updated dependencies [c1dcacd]
1268
+ - Updated dependencies [ad303ed]
1269
+ - Updated dependencies [32ccb23]
1270
+ - Updated dependencies [f5a4ef0]
1271
+ - Updated dependencies [2d3e255]
1272
+ - Updated dependencies [a8940e4]
1273
+ - Updated dependencies [7d7521f]
1274
+ - Updated dependencies [5dc4d02]
1275
+ - Updated dependencies [f724f69]
1276
+ - Updated dependencies [98877c9]
1277
+ - Updated dependencies [98877c9]
1278
+ - Updated dependencies [53068c1]
1279
+ - Updated dependencies [ee58392]
1280
+ - Updated dependencies [f16e54e]
1281
+ - Updated dependencies [06be54e]
1282
+ - Updated dependencies [28ad90e]
1283
+ - Updated dependencies [76d74ec]
1284
+ - Updated dependencies [201b31f]
1285
+ - Updated dependencies [e6b1b69]
1286
+ - Updated dependencies [259459d]
1287
+ - Updated dependencies [3f7f14e]
1288
+ - Updated dependencies [e2616e0]
1289
+ - Updated dependencies [6fdc5c6]
1290
+ - Updated dependencies [8b9d71e]
1291
+ - Updated dependencies [05154a1]
1292
+ - Updated dependencies [33f5e23]
1293
+ - Updated dependencies [259af21]
1294
+ - Updated dependencies [f8644c7]
1295
+ - Updated dependencies [306ca50]
1296
+ - Updated dependencies [978fed2]
1297
+ - Updated dependencies [cfc293f]
1298
+ - Updated dependencies [587fc91]
1299
+ - Updated dependencies [de70b42]
1300
+ - Updated dependencies [9b6fe7c]
1301
+ - Updated dependencies [fb3d99b]
1302
+ - Updated dependencies [1986594]
1303
+ - Updated dependencies [6968885]
1304
+ - Updated dependencies [eaed61f]
1305
+ - Updated dependencies [cdfbee2]
1306
+ - Updated dependencies [ad4af62]
1307
+ - Updated dependencies [debe2f6]
1308
+ - Updated dependencies [d44dbfa]
1309
+ - Updated dependencies [29c6c9d]
1310
+ - Updated dependencies [d21c001]
1311
+ - Updated dependencies [ad047d2]
1312
+ - Updated dependencies [8c711fb]
1313
+ - Updated dependencies [f1cc3a3]
1314
+ - Updated dependencies [09e4547]
1315
+ - Updated dependencies [97b0798]
1316
+ - Updated dependencies [474fe39]
1317
+ - Updated dependencies [0bc685a]
1318
+ - Updated dependencies [b949059]
1319
+ - Updated dependencies [2826d1e]
1320
+ - Updated dependencies [be1c52c]
1321
+ - Updated dependencies [c5ff96d]
1322
+ - Updated dependencies [5a84d41]
1323
+ - Updated dependencies [84e7be9]
1324
+ - Updated dependencies [91f4c78]
1325
+ - Updated dependencies [ddc2527]
1326
+ - Updated dependencies [820eff9]
1327
+ - Updated dependencies [a6c3f38]
1328
+ - Updated dependencies [debc23a]
1329
+ - Updated dependencies [0f8ad09]
1330
+ - Updated dependencies [553a47f]
1331
+ - Updated dependencies [43a7a8d]
1332
+ - Updated dependencies [a98085f]
1333
+ - Updated dependencies [20b1a9e]
1334
+ - Updated dependencies [344a22a]
1335
+ - Updated dependencies [4827e91]
1336
+ - Updated dependencies [8d895ff]
1337
+ - Updated dependencies [86f7a20]
1338
+ - Updated dependencies [a3a884d]
1339
+ - Updated dependencies [cfed092]
1340
+ - Updated dependencies [203a449]
1341
+ - Updated dependencies [8f9689f]
1342
+ - Updated dependencies [73f69dc]
1343
+ - Updated dependencies [04c56aa]
1344
+ - Updated dependencies [f6472d7]
1345
+ - Updated dependencies [57a3bb3]
1346
+ - Updated dependencies [b3efeb7]
1347
+ - Updated dependencies [ddd075a]
1348
+ - Updated dependencies [88154be]
1349
+ - Updated dependencies [e8dc61e]
1350
+ - Updated dependencies [9c82146]
1351
+ - Updated dependencies [5f9a987]
1352
+ - Updated dependencies [744b8f5]
1353
+ - Updated dependencies [ac37fc6]
1354
+ - Updated dependencies [2f3e793]
1355
+ - Updated dependencies [4820f55]
1356
+ - Updated dependencies [462d9c4]
1357
+ - Updated dependencies [78caf51]
1358
+ - Updated dependencies [7d21581]
1359
+ - Updated dependencies [37785ed]
1360
+ - Updated dependencies [62a789b]
1361
+ - Updated dependencies [2e284b2]
1362
+ - Updated dependencies [d8e8d9c]
1363
+ - Updated dependencies [789ad63]
1364
+ - Updated dependencies [f2445c9]
1365
+ - Updated dependencies [94e749b]
1366
+ - Updated dependencies [ea1d916]
1367
+ - Updated dependencies [2af1988]
1368
+ - Updated dependencies [0af50a3]
1369
+ - Updated dependencies [1b49eaf]
1370
+ - Updated dependencies [ae31a19]
1371
+ - Updated dependencies [2e836de]
1372
+ - Updated dependencies [e0f300b]
1373
+ - Updated dependencies [0161c7f]
1374
+ - Updated dependencies [e900015]
1375
+ - Updated dependencies [db02d47]
1376
+ - Updated dependencies [b5bdf48]
1377
+ - Updated dependencies [23338c3]
1378
+ - Updated dependencies [12a19a8]
1379
+ - Updated dependencies [5b843fb]
1380
+ - Updated dependencies [62b6a2f]
1381
+ - Updated dependencies [7e5af5c]
1382
+ - Updated dependencies [5b4780b]
1383
+ - Updated dependencies [a933452]
1384
+ - Updated dependencies [9d1d9c7]
1385
+ - Updated dependencies [8140915]
1386
+ - Updated dependencies [a019e52]
1387
+ - Updated dependencies [e8f8f6c]
1388
+ - Updated dependencies [41dcda3]
1389
+ - Updated dependencies [7b48cf9]
1390
+ - Updated dependencies [b5404f4]
1391
+ - Updated dependencies [64fc6d5]
1392
+ - Updated dependencies [b746aa0]
1393
+ - Updated dependencies [b4487aa]
1394
+ - Updated dependencies [1007379]
1395
+ - Updated dependencies [65ca83a]
1396
+ - Updated dependencies [0bfdf46]
1397
+ - Updated dependencies [947d4f9]
1398
+ - Updated dependencies [f764691]
1399
+ - Updated dependencies [e120a5a]
1400
+ - Updated dependencies [e5bd2f6]
1401
+ - Updated dependencies [e650d67]
1402
+ - Updated dependencies [04476e7]
1403
+ - Updated dependencies [67bf2e2]
1404
+ - Updated dependencies [eaaf03c]
1405
+ - Updated dependencies [d17df80]
1406
+ - Updated dependencies [7d0e7b5]
1407
+ - Updated dependencies [c6d1cb4]
1408
+ - Updated dependencies [6513c17]
1409
+ - Updated dependencies [36030ff]
1410
+ - Updated dependencies [79228cd]
1411
+ - Updated dependencies [6117f7b]
1412
+ - Updated dependencies [e533b0b]
1413
+ - Updated dependencies [cdf4d9a]
1414
+ - Updated dependencies [aee1806]
1415
+ - Updated dependencies [c13350b]
1416
+ - Updated dependencies [c13350b]
1417
+ - Updated dependencies [2c1988c]
1418
+ - Updated dependencies [9ca2d85]
1419
+ - Updated dependencies [c13350b]
1420
+ - Updated dependencies [891d345]
1421
+ - Updated dependencies [c8124e5]
1422
+ - Updated dependencies [a52e2ef]
1423
+ - Updated dependencies [5293114]
1424
+ - Updated dependencies [376a061]
1425
+ - Updated dependencies [c142ced]
1426
+ - Updated dependencies [211abdb]
1427
+ - Updated dependencies [b3363e9]
1428
+ - Updated dependencies [eda599e]
1429
+ - Updated dependencies [a1a4140]
1430
+ - Updated dependencies [7c7e246]
1431
+ - Updated dependencies [2ef1807]
1432
+ - Updated dependencies [c519533]
1433
+ - Updated dependencies [f9a5c59]
1434
+ - Updated dependencies [f35cdc5]
1435
+ - Updated dependencies [d03fe25]
1436
+ - Updated dependencies [217e2e6]
1437
+ - Updated dependencies [2672f85]
1438
+ - Updated dependencies [20bc357]
1439
+ - Updated dependencies [11066f6]
1440
+ - Updated dependencies [916af17]
1441
+ - Updated dependencies [84c86fb]
1442
+ - Updated dependencies [2a2a9fb]
1443
+ - Updated dependencies [86a71d1]
1444
+ - Updated dependencies [c001422]
1445
+ - Updated dependencies [77022a9]
1446
+ - Updated dependencies [d5c75e2]
1447
+ - Updated dependencies [03d26f7]
1448
+ - Updated dependencies [5966c2a]
1449
+ - Updated dependencies [2382580]
1450
+ - Updated dependencies [9ea2bc5]
1451
+ - Updated dependencies [a2e157c]
1452
+ - Updated dependencies [95c4227]
1453
+ - Updated dependencies [2a61116]
1454
+ - Updated dependencies [52760bf]
1455
+ - Updated dependencies [5543020]
1456
+ - Updated dependencies [880d343]
1457
+ - Updated dependencies [6e82972]
1458
+ - Updated dependencies [d4df105]
1459
+ - Updated dependencies [4615a18]
1460
+ - Updated dependencies [f505689]
1461
+ - Updated dependencies [d9fa683]
1462
+ - Updated dependencies [606d577]
1463
+ - Updated dependencies [4384921]
1464
+ - Updated dependencies [e2798fa]
1465
+ - Updated dependencies [3c628ce]
1466
+ - Updated dependencies [c2d9098]
1467
+ - Updated dependencies [0fd8556]
1468
+ - Updated dependencies [3c7bcc0]
1469
+ - Updated dependencies [4b6cac7]
1470
+ - Updated dependencies [7631964]
1471
+ - Updated dependencies [ac471a0]
1472
+ - Updated dependencies [60ae58e]
1473
+ - Updated dependencies [7f62706]
1474
+ - Updated dependencies [667fa44]
1475
+ - Updated dependencies [37e38d1]
1476
+ - Updated dependencies [e906126]
1477
+ - Updated dependencies [ce92674]
1478
+ - Updated dependencies [08363a0]
1479
+ - Updated dependencies [444de5b]
1480
+ - Updated dependencies [a227ed7]
1481
+ - Updated dependencies [7cb922e]
1482
+ - Updated dependencies [1d22114]
1483
+ - Updated dependencies [1eb13a0]
1484
+ - Updated dependencies [c52e608]
1485
+ - Updated dependencies [9613396]
1486
+ - Updated dependencies [3f7b4ff]
1487
+ - Updated dependencies [74155c7]
1488
+ - Updated dependencies [b5f9397]
1489
+ - Updated dependencies [ed77493]
1490
+ - Updated dependencies [6908830]
1491
+ - Updated dependencies [8b06bba]
1492
+ - Updated dependencies [58a03d2]
1493
+ - Updated dependencies [2bacd1a]
1494
+ - Updated dependencies [e47b342]
1495
+ - Updated dependencies [4c54037]
1496
+ - Updated dependencies [dc530b4]
1497
+ - Updated dependencies [9f601e8]
1498
+ - Updated dependencies [6a9dec6]
1499
+ - Updated dependencies [0f7157b]
1500
+ - Updated dependencies [4dc1c7d]
1501
+ - Updated dependencies [d9bef45]
1502
+ - Updated dependencies [4dfd002]
1503
+ - Updated dependencies [f549a0d]
1504
+ - Updated dependencies [51c5227]
1505
+ - Updated dependencies [82da264]
1506
+ - Updated dependencies [f586f1a]
1507
+ - Updated dependencies [77be690]
1508
+ - Updated dependencies [4ed7ed4]
1509
+ - Updated dependencies [9b9b70f]
1510
+ - Updated dependencies [f5a9bc2]
1511
+ - Updated dependencies [e59786e]
1512
+ - Updated dependencies [2fa4ca1]
1513
+ - Updated dependencies [bcf1112]
1514
+ - Updated dependencies [baeb4f0]
1515
+ - Updated dependencies [29488cc]
1516
+ - Updated dependencies [881a3cc]
1517
+ - Updated dependencies [f5a2320]
1518
+ - Updated dependencies [ad6317b]
1519
+ - Updated dependencies [811c30c]
1520
+ - Updated dependencies [a4a85c8]
1521
+ - Updated dependencies [859cb83]
1522
+ - Updated dependencies [07a4e26]
1523
+ - Updated dependencies [9774b78]
1524
+ - Updated dependencies [8a88885]
1525
+ - Updated dependencies [deb538f]
1526
+ - Updated dependencies [b49ccfd]
1527
+ - Updated dependencies [5b89711]
1528
+ - Updated dependencies [85d95e7]
1529
+ - Updated dependencies [08cd163]
1530
+ - Updated dependencies [0c8a22f]
1531
+ - Updated dependencies [5f7669e]
1532
+ - Updated dependencies [becbe53]
1533
+ - Updated dependencies [b127c8b]
1534
+ - Updated dependencies [763931e]
1535
+ - Updated dependencies [ec975f1]
1536
+ - Updated dependencies [168f60f]
1537
+ - Updated dependencies [b07d829]
1538
+ - Updated dependencies [de9af8a]
1539
+ - Updated dependencies [eb4204b]
1540
+ - Updated dependencies [a80302a]
1541
+ - Updated dependencies [a648e96]
1542
+ - Updated dependencies [a47ac06]
1543
+ - Updated dependencies [e4c61a7]
1544
+ - Updated dependencies [cc60165]
1545
+ - Updated dependencies [474f131]
1546
+ - Updated dependencies [081aa6f]
1547
+ - Updated dependencies [91f4c78]
1548
+ - Updated dependencies [050cd82]
1549
+ - Updated dependencies [4d552af]
1550
+ - Updated dependencies [44d677c]
1551
+ - Updated dependencies [c32944d]
1552
+ - Updated dependencies [1dd780f]
1553
+ - Updated dependencies [e8d0c21]
1554
+ - Updated dependencies [244ca86]
1555
+ - Updated dependencies [546ab3c]
1556
+ - Updated dependencies [c4df271]
1557
+ - Updated dependencies [c8d6f6e]
1558
+ - Updated dependencies [0b51bb6]
1559
+ - Updated dependencies [d9971d3]
1560
+ - Updated dependencies [7dc1067]
1561
+ - Updated dependencies [4f13be2]
1562
+ - Updated dependencies [a41ba5c]
1563
+ - Updated dependencies [189854c]
1564
+ - Updated dependencies [0e3a226]
1565
+ - Updated dependencies [92a67f2]
1566
+ - Updated dependencies [9136327]
1567
+ - Updated dependencies [bf0ae99]
1568
+ - Updated dependencies [eb3e650]
1569
+ - Updated dependencies [abeb375]
1570
+ - Updated dependencies [cb3b6cd]
1571
+ - Updated dependencies [73b7234]
1572
+ - Updated dependencies [d2b97c3]
1573
+ - Updated dependencies [61cc079]
1574
+ - Updated dependencies [45dc446]
1575
+ - Updated dependencies [0e96e46]
1576
+ - Updated dependencies [c1d44f7]
1577
+ - Updated dependencies [59b794f]
1578
+ - Updated dependencies [ef4efa8]
1579
+ - Updated dependencies [cbb6a5c]
1580
+ - Updated dependencies [fc3a36a]
1581
+ - Updated dependencies [ab9fb5c]
1582
+ - Updated dependencies [69787f0]
1583
+ - Updated dependencies [5d022a1]
1584
+ - Updated dependencies [042b9ee]
1585
+ - Updated dependencies [f985b3f]
1586
+ - Updated dependencies [795b6e1]
1587
+ - Updated dependencies [d52d4fe]
1588
+ - Updated dependencies [742cebb]
1589
+ - Updated dependencies [175d789]
1590
+ - Updated dependencies [f549a0d]
1591
+ - Updated dependencies [427344c]
1592
+ - Updated dependencies [8af76ae]
1593
+ - Updated dependencies [1d4756e]
1594
+ - Updated dependencies [720c5ad]
1595
+ - Updated dependencies [a8d1e24]
1596
+ - Updated dependencies [b85cc54]
1597
+ - Updated dependencies [a36db28]
1598
+ - Updated dependencies [7a8476f]
1599
+ - Updated dependencies [518ca7a]
1600
+ - Updated dependencies [41642b0]
1601
+ - Updated dependencies [4cca74c]
1602
+ - Updated dependencies [88ef03e]
1603
+ - Updated dependencies [9a4932a]
1604
+ - Updated dependencies [3f8817a]
1605
+ - Updated dependencies [a2443e3]
1606
+ - Updated dependencies [e1554b1]
1607
+ - Updated dependencies [9e2caf3]
1608
+ - Updated dependencies [4856789]
1609
+ - Updated dependencies [81ce41a]
1610
+ - Updated dependencies [85e1e4e]
1611
+ - Updated dependencies [c3f4916]
1612
+ - Updated dependencies [55dbbba]
1613
+ - Updated dependencies [33e0385]
1614
+ - Updated dependencies [dac6a08]
1615
+ - Updated dependencies [72c3c86]
1616
+ - Updated dependencies [2d8dba3]
1617
+ - Updated dependencies [7f1a635]
1618
+ - Updated dependencies [2205363]
1619
+ - Updated dependencies [09fe58d]
1620
+ - Updated dependencies [f9fc874]
1621
+ - Updated dependencies [d62f8eb]
1622
+ - Updated dependencies [d0a5ceb]
1623
+ - Updated dependencies [a7586cd]
1624
+ - Updated dependencies [4c5e80e]
1625
+ - Updated dependencies [4b5702a]
1626
+ - Updated dependencies [011b386]
1627
+ - Updated dependencies [e18a162]
1628
+ - Updated dependencies [394b7a1]
1629
+ - Updated dependencies [ce92674]
1630
+ - Updated dependencies [0f2fdcd]
1631
+ - Updated dependencies [d6d1a50]
1632
+ - Updated dependencies [cf2c9b7]
1633
+ - Updated dependencies [8ffa8b9]
1634
+ - Updated dependencies [d127ff0]
1635
+ - Updated dependencies [674ac99]
1636
+ - Updated dependencies [833b512]
1637
+ - Updated dependencies [36d90fc]
1638
+ - Updated dependencies [7777e8f]
1639
+ - Updated dependencies [9b86cf6]
1640
+ - Updated dependencies [d063a96]
1641
+ - Updated dependencies [8825a06]
1642
+ - Updated dependencies [5087ac6]
1643
+ - Updated dependencies [677b591]
1644
+ - Updated dependencies [cf7c694]
1645
+ - Updated dependencies [ddd0f06]
1646
+ - Updated dependencies [d77d1b7]
1647
+ - Updated dependencies [0f9faa2]
1648
+ - Updated dependencies [2d1ddf0]
1649
+ - Updated dependencies [354b00f]
1650
+ - Updated dependencies [3de535b]
1651
+ - Updated dependencies [fe2e15a]
1652
+ - Updated dependencies [5b79a34]
1653
+ - Updated dependencies [502564d]
1654
+ - Updated dependencies [603cab8]
1655
+ - Updated dependencies [c757854]
1656
+ - Updated dependencies [471839d]
1657
+ - Updated dependencies [507b92a]
1658
+ - Updated dependencies [46365ab]
1659
+ - Updated dependencies [b508244]
1660
+ - Updated dependencies [df95346]
1661
+ - Updated dependencies [3dede58]
1662
+ - Updated dependencies [c6b6bb4]
1663
+ - Updated dependencies [594508e]
1664
+ - Updated dependencies [7cf42fe]
1665
+ - Updated dependencies [5966c2a]
1666
+ - Updated dependencies [0045682]
1667
+ - Updated dependencies [7309c81]
1668
+ - Updated dependencies [a8dcc37]
1669
+ - Updated dependencies [040ecd2]
1670
+ - Updated dependencies [932d7e2]
1671
+ - Updated dependencies [2f59da0]
1672
+ - Updated dependencies [d56012f]
1673
+ - Updated dependencies [f78dd83]
1674
+ - Updated dependencies [a2cd18a]
1675
+ - Updated dependencies [9051802]
1676
+ - Updated dependencies [20bc1ec]
1677
+ - Updated dependencies [1c625ca]
1678
+ - Updated dependencies [2f8328c]
1679
+ - Updated dependencies [2a6c279]
1680
+ - Updated dependencies [9319586]
1681
+ - Updated dependencies [8c8f0df]
1682
+ - Updated dependencies [8ad609c]
1683
+ - Updated dependencies [bbee302]
1684
+ - Updated dependencies [90c2b15]
1685
+ - Updated dependencies [4638aaa]
1686
+ - Updated dependencies [0222d3c]
1687
+ - Updated dependencies [08863dd]
1688
+ - Updated dependencies [071d0dc]
1689
+ - Updated dependencies [f293d45]
1690
+ - Updated dependencies [56664f5]
1691
+ - Updated dependencies [71f205d]
1692
+ - Updated dependencies [f067930]
1693
+ - Updated dependencies [414395b]
1694
+ - Updated dependencies [42eeb7d]
1695
+ - Updated dependencies [31cbe90]
1696
+ - Updated dependencies [6b7129a]
1697
+ - Updated dependencies [c5adfe1]
1698
+ - Updated dependencies [97ace2a]
1699
+ - Updated dependencies [26e1029]
1700
+ - Updated dependencies [0a936ea]
1701
+ - Updated dependencies [90bbf25]
1702
+ - Updated dependencies [f1850d8]
1703
+ - Updated dependencies [023c00b]
1704
+ - Updated dependencies [eb91eba]
1705
+ - Updated dependencies [17d0954]
1706
+ - Updated dependencies [f28ef3b]
1707
+ - Updated dependencies [42da73d]
1708
+ - Updated dependencies [01e124d]
1709
+ - Updated dependencies [ef7b5ef]
1710
+ - Updated dependencies [9514767]
1711
+ - Updated dependencies [8f20201]
1712
+ - Updated dependencies [155507e]
1713
+ - Updated dependencies [643b7c7]
1714
+ - Updated dependencies [7bba90b]
1715
+ - Updated dependencies [8813b90]
1716
+ - Updated dependencies [108ba8d]
1717
+ - Updated dependencies [2a5f04a]
1718
+ - Updated dependencies [4f740b0]
1719
+ - Updated dependencies [7ce02eb]
1720
+ - Updated dependencies [b4ad984]
1721
+ - Updated dependencies [a9f32df]
1722
+ - Updated dependencies [aeb9b27]
1723
+ - Updated dependencies [7d27da0]
1724
+ - Updated dependencies [d0d5205]
1725
+ - Updated dependencies [1a15893]
1726
+ - Updated dependencies [b70e534]
1727
+ - Updated dependencies [7e05d8e]
1728
+ - Updated dependencies [8f1851e]
1729
+ - Updated dependencies [61ea810]
1730
+ - Updated dependencies [2233a85]
1731
+ - Updated dependencies [67452d1]
1732
+ - Updated dependencies [089767f]
1733
+ - Updated dependencies [a13827e]
1734
+ - Updated dependencies [66d99ec]
1735
+ - Updated dependencies [cb43296]
1736
+ - Updated dependencies [b61afc1]
1737
+ - Updated dependencies [79021fc]
1738
+ - Updated dependencies [7733604]
1739
+ - Updated dependencies [40e420f]
1740
+ - Updated dependencies [62dd69a]
1741
+ - Updated dependencies [d13004a]
1742
+ - Updated dependencies [be7360c]
1743
+ - Updated dependencies [e15e679]
1744
+ - Updated dependencies [2ab1257]
1745
+ - Updated dependencies [0fc6219]
1746
+ - Updated dependencies [061406d]
1747
+ - Updated dependencies [e4c8b6c]
1748
+ - Updated dependencies [acb10f6]
1749
+ - Updated dependencies [605e190]
1750
+ - Updated dependencies [c6c59f1]
1751
+ - Updated dependencies [b0e78a8]
1752
+ - Updated dependencies [f31cc8d]
1753
+ - Updated dependencies [f343dc4]
1754
+ - Updated dependencies [8269e32]
1755
+ - Updated dependencies [74f7339]
1756
+ - Updated dependencies [a6c35a2]
1757
+ - Updated dependencies [c2f1002]
1758
+ - Updated dependencies [4cc4fb7]
1759
+ - Updated dependencies [97b6658]
1760
+ - Updated dependencies [28d1eb7]
1761
+ - Updated dependencies [06770c0]
1762
+ - Updated dependencies [2c26040]
1763
+ - Updated dependencies [f758cec]
1764
+ - Updated dependencies [5b47ab5]
1765
+ - Updated dependencies [b09d8d9]
1766
+ - Updated dependencies [b09d8d9]
1767
+ - Updated dependencies [8675db6]
1768
+ - Updated dependencies [b09d8d9]
1769
+ - Updated dependencies [27358d5]
1770
+ - Updated dependencies [1c3da1f]
1771
+ - Updated dependencies [c1f344b]
1772
+ - Updated dependencies [3eb1b2b]
1773
+ - Updated dependencies [9c93465]
1774
+ - Updated dependencies [a34fd2e]
1775
+ - Updated dependencies [ebb209c]
1776
+ - Updated dependencies [76bcb83]
1777
+ - Updated dependencies [59b85c0]
1778
+ - Updated dependencies [889ae47]
1779
+ - Updated dependencies [4f4c3fb]
1780
+ - Updated dependencies [78f0be8]
1781
+ - Updated dependencies [6e357ed]
1782
+ - Updated dependencies [d6938bf]
1783
+ - Updated dependencies [35f7fb4]
1784
+ - Updated dependencies [0410522]
1785
+ - Updated dependencies [63b33e6]
1786
+ - Updated dependencies [f163028]
1787
+ - Updated dependencies [814db6d]
1788
+ - Updated dependencies [a5302c7]
1789
+ - Updated dependencies [31e0be9]
1790
+ - Updated dependencies [4bfd455]
1791
+ - Updated dependencies [ffd2ce2]
1792
+ - Updated dependencies [2a44c1d]
1793
+ - Updated dependencies [7084313]
1794
+ - Updated dependencies [f07808c]
1795
+ - Updated dependencies [7ffc3d3]
1796
+ - Updated dependencies [88346ba]
1797
+ - Updated dependencies [4631592]
1798
+ - Updated dependencies [62f8017]
1799
+ - Updated dependencies [32ff033]
1800
+ - Updated dependencies [a831df1]
1801
+ - Updated dependencies [f752ee3]
1802
+ - Updated dependencies [a1b61e0]
1803
+ - Updated dependencies [cd6b9f2]
1804
+ - Updated dependencies [2cb6d3c]
1805
+ - Updated dependencies [af2a095]
1806
+ - Updated dependencies [5ac93d4]
1807
+ - Updated dependencies [695cfbd]
1808
+ - Updated dependencies [0e043d8]
1809
+ - Updated dependencies [93f267f]
1810
+ - Updated dependencies [7445149]
1811
+ - Updated dependencies [ec796d5]
1812
+ - Updated dependencies [071d0dc]
1813
+ - Updated dependencies [0024abf]
1814
+ - Updated dependencies [8dd98bf]
1815
+ - Updated dependencies [e87fea1]
1816
+ - Updated dependencies [c65e529]
1817
+ - Updated dependencies [0848bea]
1818
+ - Updated dependencies [d51bed2]
1819
+ - Updated dependencies [dadd1ad]
1820
+ - Updated dependencies [acbf364]
1821
+ - Updated dependencies [3ca34c1]
1822
+ - Updated dependencies [7adc841]
1823
+ - Updated dependencies [239c3a3]
1824
+ - Updated dependencies [b8b3c64]
1825
+ - Updated dependencies [2f2e63c]
1826
+ - Updated dependencies [4845f85]
1827
+ - Updated dependencies [486d526]
1828
+ - Updated dependencies [94a0bbc]
1829
+ - Updated dependencies [d6bfb3d]
1830
+ - Updated dependencies [8a9c079]
1831
+ - Updated dependencies [7b005b4]
1832
+ - Updated dependencies [cc3555e]
1833
+ - Updated dependencies [a2266a6]
1834
+ - Updated dependencies [d25a0ec]
1835
+ - Updated dependencies [89d7b35]
1836
+ - Updated dependencies [94f7b6a]
1837
+ - Updated dependencies [5c94f83]
1838
+ - Updated dependencies [ea936f3]
1839
+ - Updated dependencies [0c0fbd9]
1840
+ - Updated dependencies [667b83e]
1841
+ - Updated dependencies [f3141d8]
1842
+ - Updated dependencies [7687f7b]
1843
+ - Updated dependencies [5a84d41]
1844
+ - Updated dependencies [fd3013a]
1845
+ - Updated dependencies [85ec26d]
1846
+ - Updated dependencies [73e576f]
1847
+ - Updated dependencies [f6476fc]
1848
+ - Updated dependencies [69ac82c]
1849
+ - Updated dependencies [4ac12ef]
1850
+ - Updated dependencies [833ed84]
1851
+ - Updated dependencies [a18abf3]
1852
+ - Updated dependencies [c6a4eeb]
1853
+ - Updated dependencies [1659072]
1854
+ - Updated dependencies [f450ae7]
1855
+ - Updated dependencies [abceb0d]
1856
+ - Updated dependencies [627b188]
1857
+ - Updated dependencies [8d4eae7]
1858
+ - Updated dependencies [c5a5996]
1859
+ - Updated dependencies [0c302a7]
1860
+ - Updated dependencies [b88f5e8]
1861
+ - Updated dependencies [857a6cf]
1862
+ - Updated dependencies [65a3a84]
1863
+ - Updated dependencies [6633337]
1864
+ - Updated dependencies [21676eb]
1865
+ - Updated dependencies [e9cb9ab]
1866
+ - Updated dependencies [42cc219]
1867
+ - Updated dependencies [d7e0b42]
1868
+ - Updated dependencies [3510e4a]
1869
+ - Updated dependencies [f00d8d4]
1870
+ - Updated dependencies [5326b36]
1871
+ - Updated dependencies [aa4b90d]
1872
+ - Updated dependencies [ccd9397]
1873
+ - Updated dependencies [503be86]
1874
+ - Updated dependencies [54299ca]
1875
+ - Updated dependencies [ae490ef]
1876
+ - Updated dependencies [e124711]
1877
+ - Updated dependencies [dc61def]
1878
+ - Updated dependencies [bca935b]
1879
+ - Updated dependencies [d92c72d]
1880
+ - Updated dependencies [c54c822]
1881
+ - Updated dependencies [8dcc0f5]
1882
+ - Updated dependencies [75b9e51]
1883
+ - Updated dependencies [9c90ea0]
1884
+ - Updated dependencies [f61c8cf]
1885
+ - Updated dependencies [e3ef52b]
1886
+ - Updated dependencies [0a2f233]
1887
+ - Updated dependencies [8621cdd]
1888
+ - Updated dependencies [251e888]
1889
+ - Updated dependencies [07f1822]
1890
+ - Updated dependencies [e336549]
1891
+ - Updated dependencies [3bb9340]
1892
+ - Updated dependencies [1e604c4]
1893
+ - Updated dependencies [04fab5e]
1894
+ - Updated dependencies [183b4c4]
1895
+ - Updated dependencies [7f713b6]
1896
+ - Updated dependencies [d40f43a]
1897
+ - Updated dependencies [2fdb36e]
1898
+ - Updated dependencies [6f23667]
1899
+ - Updated dependencies [cde1975]
1900
+ - Updated dependencies [0bc685a]
1901
+ - Updated dependencies [20526f5]
1902
+ - Updated dependencies [efedd28]
1903
+ - Updated dependencies [5d21a48]
1904
+ - Updated dependencies [5278e11]
1905
+ - Updated dependencies [c5eef1d]
1906
+ - Updated dependencies [e5e7ee0]
1907
+ - Updated dependencies [23dba62]
1908
+ - Updated dependencies [e0f300b]
1909
+ - Updated dependencies [761a0ba]
1910
+ - Updated dependencies [c960170]
1911
+ - Updated dependencies [19365b7]
1912
+ - Updated dependencies [ba98e26]
1913
+ - Updated dependencies [b7ed26d]
1914
+ - Updated dependencies [a2ebea2]
1915
+ - Updated dependencies [800bdb0]
1916
+ - Updated dependencies [9d4dfc4]
1917
+ - Updated dependencies [1059965]
1918
+ - Updated dependencies [def5919]
1919
+ - Updated dependencies [ee264b2]
1920
+ - Updated dependencies [60b672e]
1921
+ - Updated dependencies [6b441a8]
1922
+ - Updated dependencies [ce0cfe9]
1923
+ - Updated dependencies [04f1182]
1924
+ - Updated dependencies [be87153]
1925
+ - Updated dependencies [dd0f681]
1926
+ - Updated dependencies [60f0dd8]
1927
+ - Updated dependencies [a87c5cd]
1928
+ - Updated dependencies [a47f338]
1929
+ - Updated dependencies [b3a3d83]
1930
+ - Updated dependencies [7a55913]
1931
+ - Updated dependencies [35accbf]
1932
+ - Updated dependencies [6038de7]
1933
+ - Updated dependencies [fc5f536]
1934
+ - Updated dependencies [5647006]
1935
+ - Updated dependencies [e654bfd]
1936
+ - Updated dependencies [01a7337]
1937
+ - Updated dependencies [7e4783f]
1938
+ - Updated dependencies [b45c71e]
1939
+ - Updated dependencies [f8cfbb4]
1940
+ - Updated dependencies [6e6c872]
1941
+ - Updated dependencies [2598216]
1942
+ - Updated dependencies [11949fc]
1943
+ - Updated dependencies [2c7e62d]
1944
+ - Updated dependencies [eb95d97]
1945
+ - Updated dependencies [b098b0e]
1946
+ - Updated dependencies [4d00b13]
1947
+ - Updated dependencies [1363084]
1948
+ - Updated dependencies [fa5758e]
1949
+ - Updated dependencies [38f7e4f]
1950
+ - Updated dependencies [eb7613c]
1951
+ - Updated dependencies [c57f3cf]
1952
+ - Updated dependencies [ecc9110]
1953
+ - Updated dependencies [e4c2dc8]
1954
+ - Updated dependencies [97faca3]
1955
+ - Updated dependencies [57bab76]
1956
+ - Updated dependencies [c89d18c]
1957
+ - Updated dependencies [1bd2795]
1958
+ - Updated dependencies [f7bd4e2]
1959
+ - Updated dependencies [361bd5b]
1960
+ - Updated dependencies [aac90a5]
1961
+ - Updated dependencies [3da3da5]
1962
+ - Updated dependencies [1e6ab15]
1963
+ - Updated dependencies [b90086a]
1964
+ - Updated dependencies [8186a70]
1965
+ - Updated dependencies [a329cca]
1966
+ - Updated dependencies [c87ef70]
1967
+ - Updated dependencies [3cb0618]
1968
+ - Updated dependencies [32a0874]
1969
+ - Updated dependencies [6eec18c]
1970
+ - Updated dependencies [4d7bebf]
1971
+ - Updated dependencies [821ac7a]
1972
+ - Updated dependencies [8f81731]
1973
+ - Updated dependencies [7055c22]
1974
+ - Updated dependencies [785a748]
1975
+ - Updated dependencies [3af0354]
1976
+ - Updated dependencies [866ff16]
1977
+ - Updated dependencies [5a85e67]
1978
+ - Updated dependencies [8b50cb3]
1979
+ - Updated dependencies [a0fdc56]
1980
+ - Updated dependencies [b95577a]
1981
+ - Updated dependencies [0dcbc11]
1982
+ - Updated dependencies [d88f3e9]
1983
+ - Updated dependencies [ad5fe25]
1984
+ - Updated dependencies [c183a12]
1985
+ - Updated dependencies [83c161f]
1986
+ - Updated dependencies [d8c4957]
1987
+ - Updated dependencies [b9f930b]
1988
+ - Updated dependencies [f24cb83]
1989
+ - Updated dependencies [5dbbb92]
1990
+ - Updated dependencies [ea90179]
1991
+ - Updated dependencies [1818998]
1992
+ - Updated dependencies [ce92674]
1993
+ - Updated dependencies [5ef0b5b]
1994
+ - Updated dependencies [8c2db68]
1995
+ - Updated dependencies [22b5e54]
1996
+ - Updated dependencies [0166bd5]
1997
+ - Updated dependencies [8064b07]
1998
+ - Updated dependencies [09ee21c]
1999
+ - Updated dependencies [4a56dbd]
2000
+ - Updated dependencies [289d04a]
2001
+ - Updated dependencies [f549a0d]
2002
+ - Updated dependencies [48fbacb]
2003
+ - Updated dependencies [06df4fa]
2004
+ - Updated dependencies [3fc2e48]
2005
+ - Updated dependencies [c9b809f]
2006
+ - Updated dependencies [e8f435c]
2007
+ - Updated dependencies [32386f8]
2008
+ - Updated dependencies [9b702dc]
2009
+ - Updated dependencies [ab16331]
2010
+ - Updated dependencies [41610f6]
2011
+ - Updated dependencies [69f1dfd]
2012
+ - Updated dependencies [06306f1]
2013
+ - Updated dependencies [bbe05de]
2014
+ - Updated dependencies [355e951]
2015
+ - Updated dependencies [a1dd1e4]
2016
+ - Updated dependencies [dadb43f]
2017
+ - Updated dependencies [3556b67]
2018
+ - @objectstack/spec@17.0.0
2019
+ - @objectstack/core@17.0.0
2020
+ - @objectstack/service-messaging@17.0.0
2021
+
3
2022
  ## 17.0.0-rc.6
4
2023
 
5
2024
  ### Patch Changes