@objectstack/plugin-webhooks 17.0.0 → 17.2.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,431 @@
1
1
  # @objectstack/plugin-webhooks
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cdaa72f: fix(service-messaging,plugin-webhooks): the `update`-op tenant-audit surface on the delivery outboxes is classified — `ack` is a dispatcher sweep, `redeliver` threads the caller's tenant (#10740)
8
+
9
+ **BREAKING** signature change on `IHttpOutbox.redeliver` and
10
+ `MessagingService.redeliverHttp`, shipped as `minor` under the repo's
11
+ launch-window convention for breaking changes.
12
+
13
+ `sys_http_delivery` and `sys_notification_delivery` carry three single-record
14
+ (`multi: false`) writes that the SQL driver audits under the **`update`** op —
15
+ a different op, and a different throttle key, from the `updateMany` half
16
+ classified previously. Their correct classifications are **opposite**, and
17
+ treating them as one sweep is the dangerous reading:
18
+
19
+ | site | reachable from | classification |
20
+ | --- | --- | --- |
21
+ | `SqlNotificationOutbox.ack` | dispatcher tick only | global sweep |
22
+ | `SqlHttpOutbox.ack` | dispatcher tick only | global sweep |
23
+ | `SqlHttpOutbox.redeliver` | `POST /api/v1/webhooks/redeliver` | request-contextual |
24
+
25
+ **The two `ack` sites** are declared global sweeps through a new
26
+ `dispatcherAckOptions()` helper, sibling to `dispatcherSweepOptions()` and
27
+ deliberately not the same function — that one returns `& { multi: true }`, so a
28
+ `multi: false` site cannot borrow it by accident. The warrant was re-derived
29
+ against the current tree rather than inherited: `ack` has exactly two callers,
30
+ both inside `runPartition()` on a `setInterval` tick holding a per-partition
31
+ cluster lock, so no request context exists to thread; and the row being acked
32
+ was claimed by a sweep that crosses organizations by construction
33
+ (`hash(refId | notificationId | digestKey) mod N` is a load-spreading key, and
34
+ one outbox per environment drains the whole queue). Passing the claimed row's
35
+ own `organization_id` is documented at the helper as the tempting wrong answer:
36
+ a predicate read off the row you are about to write matches exactly that row,
37
+ adds no isolation, and silences the audit anyway — the appearance of scoping
38
+ without the substance.
39
+
40
+ **`redeliver` is not that**, and it is the reason this shipped separately. The
41
+ route in front of it is served to any authenticated user, so on a walled
42
+ deployment (`OS_TENANCY_POSTURE=isolated|group`) an unscoped replay is an
43
+ authenticated user writing another organization's delivery row — the case the
44
+ tenant audit exists to catch. It now carries the caller's tenant, applied to
45
+ the rows it reads as well as the row it writes, and it must never be given
46
+ `bypassTenantAudit`: a scoped write and a bypassed write produce the same
47
+ silence in the log, so the flag would convert a detectable hole into an
48
+ undetectable one. The webhook route resolves the session's
49
+ `activeOrganizationId` and threads it.
50
+
51
+ Behaviour change at the endpoint: a delivery row outside the caller's
52
+ organization is now **not found** (`RESOURCE_NOT_FOUND`, HTTP 404) rather than
53
+ replayed. It is deliberately invisible rather than forbidden, so the endpoint
54
+ is not an existence oracle for other tenants' delivery ids. An in-tenant
55
+ redelivery is unchanged.
56
+
57
+ Migrating a caller: `redeliver(id, guard?)` becomes
58
+ `redeliver(id, { tenantId, guard? })`, and `redeliverHttp(id)` becomes
59
+ `redeliverHttp(id, { tenantId })`. `tenantId` is a **required** property typed
60
+ `string | undefined`, so omitting it does not compile — a caller with no tenant
61
+ has to write `tenantId: undefined` and mean it. That is the point of the shape:
62
+ an optional property would let the dangerous case, a request path that simply
63
+ forgot, type-check in silence. Passing `undefined` leaves the write unscoped
64
+ and the audit line still fires, which is the intended reporting behaviour on a
65
+ deployment that cannot resolve an organization for the caller.
66
+
67
+ <!-- adr-0087: not-required (runtime-interface-only packages/services/service-messaging/src/http-outbox.ts#IHttpOutbox, packages/services/service-messaging/src/http-outbox.ts#RedeliverOptions) The surface that changed shape is `IHttpOutbox.redeliver`, plus the new `RedeliverOptions` argument type beside it. Both are TypeScript declarations in a service package with no `packages/spec` schema behind them: no metadata author writes a `redeliver` key, there is no authorable spelling and no `retiredKey()` tombstone, so `os migrate meta` has no stack source to rewrite. The change is a required second argument on an in-process method — a compile error at every call site, which is the notification channel, not a silent runtime gap. `MessagingService.redeliverHttp` moves with it and is deliberately NOT in the list above: this gate refuses that symbol as unresolvable, because `packages/spec/src/api/protocol.zod.ts` mentions the class name in a prose comment about `MessagingService.listInbox` while neither declaring nor importing it. The claim would be true and the gate cannot check it, so it is stated here for a reviewer instead of asserted where it would read as verified. It is a thin delegate to `IHttpOutbox.redeliver` in the same package and carries no schema of its own either. -->
68
+ - e222a53: **BREAKING** (compile-time only): twelve logger sink types that declared an
69
+ optional `error` now declare a **non-optional** `warn`, so a durability report
70
+ always has somewhere to land (#9754, #10556).
71
+
72
+ `minor`, not `major`: during the launch window this stack ships breaking changes
73
+ as `minor` — every publishable package versions in lockstep, so a `major` would
74
+ promote the whole release. `patch` would be wrong in the other direction, because
75
+ this *can* break a consumer's build.
76
+
77
+ `error` stays optional on every one of these types — hosts legitimately inject
78
+ reduced sinks, and requiring `error` was measured and rejected as #9754 option C.
79
+ What changes is that its *absence* now has a declared, guaranteed destination.
80
+ Call sites keep the `logger?.warn?.(…)` spelling as the backstop for hosts the
81
+ type cannot reach, so **no runtime behaviour changes**: nothing that printed
82
+ before stops printing, and nothing silent starts printing.
83
+
84
+ ### Who has to change, and what to do
85
+
86
+ Only a caller that hands one of these sinks an object with **no `warn` method** —
87
+ for example `{ info }` or `{ error }` alone. Add a `warn` member; there is no
88
+ rename, no removal, and no stored value or metadata key to rewrite. Every
89
+ construction site inside this repo already supplied one, so the in-repo cost was
90
+ zero; the compile error is reserved for the callers that were silently discarding
91
+ these reports.
92
+
93
+ The affected types, by package:
94
+
95
+ - `@objectstack/cloud-connection` — the internal `PluginContext['logger']`
96
+ - `@objectstack/metadata-protocol` — `IndexMigrationLogger`
97
+ - `@objectstack/plugin-approvals` — the internal `MinimalLogger` of `lifecycle-hooks`
98
+ - `@objectstack/plugin-audit` — `AuthEventAuditLogger`, `ReadAuditLogger`
99
+ - `@objectstack/plugin-auth` — `ReconcileMembershipDeps['logger']`, the internal
100
+ `LoggerLike` of `member-role-canonical`, and `AuthManagerOptions['logger']`
101
+ - `@objectstack/plugin-email` — `ReclaimLogger`, via `ReclaimAttachmentContentOptions`
102
+ - `@objectstack/plugin-reports` — `ReportServiceOptions['logger']`
103
+ - `@objectstack/plugin-sharing` — the internal `MinimalLogger` of `bulk-recompute`,
104
+ `rule-hooks` and `record-share-cascade`
105
+ - `@objectstack/plugin-webhooks` — `OptionalLogger`, via `AutoEnqueuerOptions`
106
+ - `@objectstack/service-knowledge` — `KnowledgeLogger`
107
+
108
+ `AuthManagerOptions['logger']` is the one most likely to be reached from outside:
109
+ `AuthManager` is public surface, its `logger` option stays optional, and a logger
110
+ that *is* supplied must now carry `warn`. The only non-test construction site in
111
+ this repo passes the kernel `Logger`, whose `warn` is already required.
112
+
113
+ `ReportService` and `AutoEnqueuer` additionally stopped defaulting their logger
114
+ field to `{}`. The field is now honestly optional rather than holding an empty
115
+ object that declared it could report and discarded everything. Behaviour is
116
+ unchanged in both directions.
117
+
118
+ <!-- adr-0087: not-required (runtime-interface-only packages/plugins/plugin-auth/src/auth-manager.ts#AuthManagerOptions, packages/plugins/plugin-auth/src/reconcile-membership.ts#ReconcileMembershipDeps, packages/metadata-protocol/src/migrations/partial-index-probe.ts#IndexMigrationLogger, packages/plugins/plugin-audit/src/auth-event-audit.ts#AuthEventAuditLogger, packages/plugins/plugin-audit/src/read-audit.ts#ReadAuditLogger, packages/plugins/plugin-reports/src/report-service.ts#ReportServiceOptions, packages/services/service-knowledge/src/knowledge-service.ts#KnowledgeLogger) every tightened type is a plain TypeScript logger interface -- no Zod projection, no metadata surface, and none is referenced by one -- so `objectstack migrate meta` has nothing to rewrite. Nothing is removed or renamed and no stored value moves; the only consumer action is adding a `warn` member at a construction site the compiler names. -->
119
+
120
+ ### Patch Changes
121
+
122
+ - 047ac86: Five `Plugin` implementations now release their resources from `destroy()`, the
123
+ only teardown hook the kernel calls (#10772).
124
+
125
+ `Plugin` (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)` and
126
+ `destroy?()`. `ObjectKernel.performShutdown()` and `LiteKernel.destroy()` walk
127
+ the plugins in reverse calling `plugin.destroy()` — and nothing anywhere calls
128
+ `stop()`, `dispose()`, `close()` or `shutdown()` on a plugin. Each of these five
129
+ spelled its teardown with one of those names instead, so what it released was
130
+ still held after `await kernel.shutdown()` had **resolved**:
131
+
132
+ | package | class | was spelled | what outlived shutdown |
133
+ |:--|:--|:--|:--|
134
+ | `@objectstack/metadata` | `MetadataPlugin` | `stop` (arrow property) | artifact watcher, `manager.dispose()`, repository handle |
135
+ | `@objectstack/runtime` | `AppPlugin` | `stop` (arrow property) | the `app:unregistered` catalog event, never emitted |
136
+ | `@objectstack/runtime` | `ExternalValidationPlugin` | `stop` (arrow property) | every armed drift-check `setInterval` |
137
+ | `@objectstack/plugin-email` | `EmailServicePlugin` | `dispose` | two metadata subscriptions, the SMTP transport, an engine binding |
138
+ | `@objectstack/plugin-webhooks` | `WebhookOutboxPlugin` | `dispose` | the auto-enqueuer (2 realtime subscriptions + a refresh interval) and two engine hooks |
139
+
140
+ `ExternalValidationPlugin` is the one with teeth: it is one of only two `Plugin`
141
+ implementations in the tree that own `setInterval` directly, it is mounted on
142
+ the real `os serve` path, and its `stop()`'s only caller anywhere was the class
143
+ itself re-arming. Measured against a real kernel, its drift checker performed
144
+ five further reads in the five intervals after a resolved shutdown — the #9371
145
+ mechanism verbatim. `WebhookOutboxPlugin.dispose()` had **zero** callers in the
146
+ entire repo, so its teardown had never run in any process at all.
147
+
148
+ **Nothing is removed and no signature narrows.** Each old name is retained as a
149
+ delegating alias, because it is public API of an exported class and an embedder
150
+ may have learned to call it directly precisely BECAUSE the kernel never did.
151
+ `stop` stays an arrow property where it was one (so a detached
152
+ `const { stop } = plugin` keeps working) and stays synchronous on
153
+ `ExternalValidationPlugin` (so a non-awaiting call site is unaffected). The two
154
+ `stop(ctx)` aliases widen their parameter to optional.
155
+
156
+ One behavioural note for direct callers, since `destroy()` takes no context:
157
+ `MetadataPlugin.stop(ctx)` and `AppPlugin.stop(ctx)` now use the context
158
+ captured in `init()` and ignore the argument. In a real composition these are
159
+ the same object. The visible difference is confined to a plugin whose `init()`
160
+ never ran — for `MetadataPlugin` a dropped `warn` line, for `AppPlugin` a
161
+ catalog event that is no longer emitted for an app that was never registered.
162
+ - Updated dependencies [6936d07]
163
+ - Updated dependencies [59eb04d]
164
+ - Updated dependencies [9f05b7d]
165
+ - Updated dependencies [3b2af5e]
166
+ - Updated dependencies [7d2d112]
167
+ - Updated dependencies [5fa0d72]
168
+ - Updated dependencies [8cc8401]
169
+ - Updated dependencies [02b3b07]
170
+ - Updated dependencies [8163a1c]
171
+ - Updated dependencies [cdaa72f]
172
+ - Updated dependencies [914c413]
173
+ - Updated dependencies [55809a0]
174
+ - Updated dependencies [ee2ff45]
175
+ - Updated dependencies [47cd3ec]
176
+ - Updated dependencies [52db1d1]
177
+ - Updated dependencies [5649efb]
178
+ - Updated dependencies [9d7d2de]
179
+ - Updated dependencies [c815c50]
180
+ - Updated dependencies [795ea05]
181
+ - Updated dependencies [900e489]
182
+ - Updated dependencies [2306a76]
183
+ - Updated dependencies [e5ea701]
184
+ - Updated dependencies [a40dcc1]
185
+ - Updated dependencies [def0d3e]
186
+ - Updated dependencies [8d0bb79]
187
+ - Updated dependencies [5acb58d]
188
+ - Updated dependencies [2e3cf95]
189
+ - Updated dependencies [4c93387]
190
+ - Updated dependencies [504c8d5]
191
+ - Updated dependencies [a037f7c]
192
+ - Updated dependencies [3ee8ddf]
193
+ - Updated dependencies [16cef97]
194
+ - Updated dependencies [a79bd35]
195
+ - Updated dependencies [6ceaa4b]
196
+ - Updated dependencies [15ea214]
197
+ - Updated dependencies [de19489]
198
+ - Updated dependencies [c684d00]
199
+ - Updated dependencies [923c424]
200
+ - Updated dependencies [1ec36b7]
201
+ - Updated dependencies [5f2e54c]
202
+ - Updated dependencies [189373b]
203
+ - Updated dependencies [35ad101]
204
+ - Updated dependencies [ceb33a9]
205
+ - Updated dependencies [73d9795]
206
+ - Updated dependencies [8012960]
207
+ - Updated dependencies [f34f56b]
208
+ - Updated dependencies [f399618]
209
+ - Updated dependencies [75e9301]
210
+ - Updated dependencies [2810695]
211
+ - @objectstack/spec@17.2.0
212
+ - @objectstack/core@17.2.0
213
+ - @objectstack/service-messaging@17.2.0
214
+
215
+ ## 17.1.0
216
+
217
+ ### Patch Changes
218
+
219
+ - 90417a8: chore(plugin-webhooks): `sys_webhook` declares its data-API exposure explicitly — recording the posture, not narrowing it (#9756)
220
+
221
+ `sys_webhook` shipped with no `enable` block at all, so it kept the full default
222
+ data API. Three cards each noticed and each named the narrowing as the next
223
+ step — #7799 (the signing secret), #7986 (the custom headers), #8025 option 2
224
+ (the URL) — and each assumed a later one would write the line. None did, and the
225
+ last of them closed `completed` with the line still unwritten. The posture was
226
+ never a judgement; it was a default nobody had written down.
227
+
228
+ It is written down now:
229
+
230
+ ```ts
231
+ enable: { apiMethods: ['get', 'list', 'create', 'update', 'delete', 'bulk'] }
232
+ ```
233
+
234
+ **The effective surface is unchanged, and that is the honest headline.** The set
235
+ is derived from a census of who actually reaches the object, taken before
236
+ anything was edited:
237
+
238
+ | consumer | reaches it through | needs |
239
+ |:---|:---|:---|
240
+ | Setup/Studio console — `nav_webhooks`, four list views, `userActions` create/edit/delete | REST `/api/v1/data/sys_webhook` (gated) | `get` `list` `create` `update` `delete` |
241
+ | Operator predicate write — "deactivate every webhook on an object" (#4639) | REST `updateMany`/`deleteMany` (gated on `bulk`) | `bulk` |
242
+ | `AutoEnqueuer`, `bootstrapDeclaredWebhooks`, the provenance stamp, `redeliver-guard`, the secret sweep | `engine.*` and lifecycle hooks — ObjectQL directly, which never consults `enable.apiMethods` | ungated |
243
+
244
+ Every primitive is required by a real consumer, so the set is all six — whose
245
+ operation closure is what the absent block already produced. Nothing that was
246
+ reachable becomes unreachable, and `/me/permissions` reports the identical
247
+ `apiOperations` array. No caller needs to change anything.
248
+
249
+ ⛔ **Do not read this as the read-surface narrowing those three cards asked
250
+ for.** It is not one, and `apiMethods` cannot be one here: `url` (#8025 —
251
+ won't-fix on masking, because the URL is the routing key an operator must be
252
+ able to see, search, sort and edit) and a legacy row's un-migrated
253
+ `definition_json.headers` (#7986 — still read, and warned about, by
254
+ `readLegacyHeaders`) are served by `get`/`list`, which is exactly what the admin
255
+ console requires. Any set that removes them removes the admin surface too. The
256
+ sibling `sys_http_delivery` can hold `['get','list']` because it is engine-owned
257
+ and never authored; `sys_webhook` is a first-class admin authoring surface.
258
+
259
+ The equality above is pinned in `sys-webhook-api-exposure.test.ts` rather than
260
+ left as a claim, so a later change that does move the surface has to say so.
261
+ - b278695: fix(webhooks): refuse a malformed `sys_webhook.headers_secret` at the write door instead of at the next delivery (#8566)
262
+
263
+ <!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is
264
+ renamed, retired or tombstoned. This adds a runtime validation hook on one
265
+ plugin-owned object's existing column; the authoring envelope
266
+ (`webhook.zod.ts`), the field declaration and every stored shape are untouched.
267
+ The accept-set narrows, but only over values that were already unusable at
268
+ delivery time (see below), so there is no configuration for a migration to
269
+ prescribe a rewrite of. -->
270
+
271
+ `sys_webhook.headers_secret` is a `Field.secret()` whose plaintext is **not** an
272
+ opaque blob: it is a serialized header map with a required shape — a flat JSON
273
+ object of string values — and `parseStoredHeaders` is its only reader. Nothing
274
+ validated that shape on the way in. The ordinary data API accepted any string,
275
+ encrypted it like any other secret, minted a real `sys_secret` row, and left the
276
+ column holding a perfectly valid `secret:` ref that read back as the mask with
277
+ `active: true`.
278
+
279
+ Measured on a real engine through `engine.update()` — the ordinary data API, no
280
+ privileged access — every one of these was **accepted** and is a value the
281
+ plugin can never use: `{}`, `[]`, `{"X-Count": 5}`, a nested object, and
282
+ `{X-Team: crm}` (a typo). The field is directly admin-authorable and its own
283
+ description instructs the author to type a JSON object into it, which makes a
284
+ typo the *expected* failure rather than an exotic one.
285
+
286
+ **This is not an exposure fix and must not be read as one.** #8558/#8565 already
287
+ closed the consumer half: a webhook whose stored header map does not come back
288
+ as a flat string map parks the subscription and reports at `error`, rather than
289
+ delivering header-less with a valid signature. Nothing leaks, and nothing is
290
+ silently lost today. What this changes is **when the author finds out** — at the
291
+ write door where they typed it, instead of at the next matching record change,
292
+ an unbounded time later and in a different surface.
293
+
294
+ **What is refused:** a `headers_secret` plaintext that does not parse back as a
295
+ flat JSON object of string values with at least one entry, with a located
296
+ ADR-0112 `VALIDATION_ERROR` / 400 naming `sys_webhook.headers_secret`, quoting
297
+ the shape the field's own description asks for, and diagnosing the specific
298
+ spelling (invalid JSON / an array / an empty object / which key's value is not a
299
+ string). ⛔ The message never echoes the rejected value — this column carries
300
+ credentials, and quoting the input would print an `Authorization: Bearer …` into
301
+ logs and error bodies, re-opening in the diagnostic exactly the exposure #7986
302
+ moved this field onto the encrypted channel to close. It names header *keys* and
303
+ value *types* only.
304
+
305
+ **What stays accepted, byte for byte:** every valid flat string map (as JSON
306
+ text, or as an authored object the engine serializes into the same form); `null`
307
+ to clear; an omitted key to leave the stored value unchanged; and an **echoed
308
+ read-mask**, so the ordinary Setup-form round-trip (GET a row, edit an unrelated
309
+ field, PATCH it back) is untouched. `""` is deliberately passed through to
310
+ #8559's `EmptyCredentialWriteError` rather than re-refused here — one door, one
311
+ owner, one message.
312
+
313
+ **Where it runs, and why that is the whole mechanism:** a `beforeInsert` /
314
+ `beforeUpdate` hook on `sys_webhook`, bound by `WebhookOutboxPlugin` before its
315
+ first seeded write. It has to run *before* the engine's `encryptSecretFields` —
316
+ one step later the plaintext is gone and the column holds an opaque ref, so a
317
+ validator behind it would have nothing left to validate. The suite measures that
318
+ ordering rather than asserting it: every refusal pins that **no `sys_secret`
319
+ cipher row was minted**, which is only true if the gate ran first.
320
+
321
+ A hook rather than checks on the plugin's own write paths
322
+ (`bootstrapDeclaredWebhooks` / `headersPatch` / the migration sweep), because a
323
+ direct `PATCH /api/v1/data/sys_webhook` goes through none of them and that is
324
+ the measured trigger. Those paths inherit the validation through the hook and
325
+ deliberately carry no second check.
326
+
327
+ A general `secret`-channel plaintext validator — letting any `secret`-typed
328
+ field declare its own plaintext shape — is the principled generalization and is
329
+ recorded as the **promotion path**, not built here: it becomes the shape the
330
+ moment a second shaped-plaintext `secret` field exists (maintainer ruling
331
+ 2026-08-13; one consumer does not justify a general capability).
332
+ - Updated dependencies [56656aa]
333
+ - Updated dependencies [07e630e]
334
+ - Updated dependencies [2f65b1b]
335
+ - Updated dependencies [720ee95]
336
+ - Updated dependencies [f287435]
337
+ - Updated dependencies [2782805]
338
+ - Updated dependencies [e43d63a]
339
+ - Updated dependencies [9aa8890]
340
+ - Updated dependencies [7c9c1dd]
341
+ - Updated dependencies [75b7c24]
342
+ - Updated dependencies [d5552ca]
343
+ - Updated dependencies [d9813a9]
344
+ - Updated dependencies [8640fb2]
345
+ - Updated dependencies [2420641]
346
+ - Updated dependencies [2ad91c3]
347
+ - Updated dependencies [f57fb38]
348
+ - Updated dependencies [00777a0]
349
+ - Updated dependencies [d491625]
350
+ - Updated dependencies [420804d]
351
+ - Updated dependencies [716ac9b]
352
+ - Updated dependencies [a38408a]
353
+ - Updated dependencies [62b1427]
354
+ - Updated dependencies [7ea1372]
355
+ - Updated dependencies [23abe27]
356
+ - Updated dependencies [985a9cd]
357
+ - Updated dependencies [5f5e234]
358
+ - Updated dependencies [a8189ae]
359
+ - Updated dependencies [26e70fb]
360
+ - Updated dependencies [42b05af]
361
+ - Updated dependencies [2b292ce]
362
+ - Updated dependencies [abcf853]
363
+ - Updated dependencies [8b9eba5]
364
+ - Updated dependencies [d575779]
365
+ - Updated dependencies [94f7ef8]
366
+ - Updated dependencies [c5ac5e4]
367
+ - Updated dependencies [a777944]
368
+ - Updated dependencies [dd88e1c]
369
+ - Updated dependencies [856527c]
370
+ - Updated dependencies [870f710]
371
+ - Updated dependencies [79c46da]
372
+ - Updated dependencies [7ff3975]
373
+ - Updated dependencies [29d055b]
374
+ - Updated dependencies [65589d6]
375
+ - Updated dependencies [2c86fe3]
376
+ - Updated dependencies [e196c6a]
377
+ - Updated dependencies [24173e9]
378
+ - Updated dependencies [4ab7523]
379
+ - Updated dependencies [19539b4]
380
+ - Updated dependencies [f8eb736]
381
+ - Updated dependencies [11b779e]
382
+ - Updated dependencies [739fe5b]
383
+ - Updated dependencies [4bfe1a5]
384
+ - Updated dependencies [2065e31]
385
+ - Updated dependencies [b69d0f5]
386
+ - Updated dependencies [4d47afe]
387
+ - Updated dependencies [e4e5c6e]
388
+ - Updated dependencies [9a56784]
389
+ - Updated dependencies [d00d2f6]
390
+ - Updated dependencies [df0c12d]
391
+ - Updated dependencies [44738f7]
392
+ - Updated dependencies [d31785f]
393
+ - Updated dependencies [c308a4f]
394
+ - Updated dependencies [e2899f6]
395
+ - Updated dependencies [3851f87]
396
+ - Updated dependencies [2a29caa]
397
+ - Updated dependencies [09a6eee]
398
+ - Updated dependencies [1a7f907]
399
+ - Updated dependencies [cd455c8]
400
+ - Updated dependencies [e1bb0ca]
401
+ - Updated dependencies [30d3752]
402
+ - Updated dependencies [c80e7ae]
403
+ - Updated dependencies [09a9a8a]
404
+ - Updated dependencies [07026cf]
405
+ - Updated dependencies [5d4f3d5]
406
+ - Updated dependencies [4d80e8b]
407
+ - Updated dependencies [30b1c63]
408
+ - Updated dependencies [079b457]
409
+ - Updated dependencies [e43b211]
410
+ - Updated dependencies [890b38f]
411
+ - Updated dependencies [8bee54b]
412
+ - Updated dependencies [7a537ce]
413
+ - Updated dependencies [593c4bf]
414
+ - Updated dependencies [ff08691]
415
+ - Updated dependencies [60e0f90]
416
+ - Updated dependencies [90c5285]
417
+ - Updated dependencies [402c125]
418
+ - Updated dependencies [7901b2d]
419
+ - Updated dependencies [56bca91]
420
+ - Updated dependencies [79394d7]
421
+ - Updated dependencies [730fd9a]
422
+ - Updated dependencies [44bc51d]
423
+ - Updated dependencies [73cfddf]
424
+ - Updated dependencies [d634e66]
425
+ - @objectstack/spec@17.1.0
426
+ - @objectstack/core@17.1.0
427
+ - @objectstack/service-messaging@17.1.0
428
+
3
429
  ## 17.0.0
4
430
 
5
431
  ### Minor Changes
@@ -251,10 +251,69 @@ var SysWebhook = _data.ObjectSchema.create({
251
251
  { fields: ["name"], unique: "organization" },
252
252
  { fields: ["object_name"] },
253
253
  { fields: ["active", "object_name"] }
254
- ]
254
+ ],
255
+ /**
256
+ * [#9756] The data-API exposure of this object, declared EXPLICITLY.
257
+ *
258
+ * ## Why the block exists
259
+ *
260
+ * Three cards observed that `sys_webhook` declared no `enable` block at all
261
+ * and each named narrowing its read surface as the next step — #7799 (the
262
+ * signing secret), #7986 (the custom headers) and #8025 option 2 (the URL) —
263
+ * and each assumed a later one would write the line. None did. The condition
264
+ * held not because anyone judged the full default API correct here, but
265
+ * because the omission was never anybody's deliverable. That is the standard
266
+ * #8025 set and #9756 quotes back: *an omission is not a decision unless
267
+ * someone wrote it down.* This block is that decision, written down.
268
+ *
269
+ * ## The census the set is derived from (#9756, measured before writing)
270
+ *
271
+ * | consumer | reaches this object through | needs |
272
+ * |:---|:---|:---|
273
+ * | Setup/Studio console — `nav_webhooks` (`webhook-outbox-plugin.ts`), the four list views above, `userActions` create/edit/delete | REST `/api/v1/data/sys_webhook` — the gated data API | `get` `list` `create` `update` `delete` |
274
+ * | Operator predicate write — "deactivate every webhook on an object" (#4639, for which `AutoEnqueuer.handleSelfHealEvent` carries a `data.records.*` branch built expressly for this gesture) | REST `updateMany` / `deleteMany`, both gated on the `bulk` primitive | `bulk` |
275
+ * | `AutoEnqueuer` cache refresh, `bootstrapDeclaredWebhooks`, `stampWebhookProvenance`, `redeliver-guard`, `migrateLegacyWebhookSecrets`, the `headers_secret` write gate | `engine.find/findOne/insert/update` and lifecycle hooks — ObjectQL directly, which never consults `enable.apiMethods` | ungated: unaffected by anything declared here |
276
+ *
277
+ * ⇒ every primitive is required by a real, measured consumer, so the set is
278
+ * all six. No consumer outside the admin/operator surface was found.
279
+ *
280
+ * ## ⛔ This narrows NOTHING — do not read it as if it did
281
+ *
282
+ * `resolveEffectiveApiMethods` (`@objectstack/spec/data`) seeds the
283
+ * `unrestricted` branch with the very same `API_PRIMITIVES` set, so the six
284
+ * primitives resolve to the operation closure the *absent* block already
285
+ * produced. The serialized effective set (`/me/permissions`, the 405
286
+ * `allowed` array) is byte-identical, and no route or `callData` action
287
+ * reaches an operation whose answer differs. Only `mode` changes,
288
+ * `unrestricted` → `restricted`.
289
+ *
290
+ * So the presence of this block is NOT evidence that the reachable cleartext
291
+ * on this object was reduced. It was not, and `apiMethods` is the wrong
292
+ * instrument for it: `url` (#8025 — won't-fix on masking, because the URL is
293
+ * the routing key an operator must be able to see, search, sort and edit) and
294
+ * a legacy row's un-migrated `definition_json.headers` (#7986 —
295
+ * `readLegacyHeaders` in `auto-enqueuer.ts` still reads them and warns) are
296
+ * both served by `get`/`list`, which is exactly what the console requires.
297
+ * Any set that removes them removes the admin surface with them. A survey
298
+ * that greps this file for `enable:` and stops is measuring the wrong thing;
299
+ * #9756's report carries the census that says so.
300
+ *
301
+ * Contrast the sibling `sys_http_delivery` (`['get','list']`,
302
+ * `service-messaging`), whose narrowing is real: that table is engine-owned —
303
+ * written only by `SqlHttpOutbox` through context-less raw-engine writes,
304
+ * never authored — so closing its write surface costs nothing. `sys_webhook`
305
+ * is a first-class admin authoring surface. That is the whole difference, and
306
+ * it is why the sibling's shape could not simply be copied here.
307
+ *
308
+ * Pinned — the census, the no-narrowing equality, and the registration-time
309
+ * survival of every write verb — in `sys-webhook-api-exposure.test.ts`.
310
+ */
311
+ enable: {
312
+ apiMethods: ["get", "list", "create", "update", "delete", "bulk"]
313
+ }
255
314
  });
256
315
 
257
316
 
258
317
 
259
318
  exports.SysWebhook = SysWebhook;
260
- //# sourceMappingURL=chunk-Q4FEMGD6.cjs.map
319
+ //# sourceMappingURL=chunk-DRHJ2M45.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/objectstack/objectstack/packages/plugins/plugin-webhooks/dist/chunk-DRHJ2M45.cjs","../src/sys-webhook.object.ts"],"names":[],"mappings":"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","file":"/home/runner/work/objectstack/objectstack/packages/plugins/plugin-webhooks/dist/chunk-DRHJ2M45.cjs","sourcesContent":[null,"// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport { ObjectSchema, Field } from '@objectstack/spec/data';\n\n/**\n * sys_webhook — Outbound HTTP integration configuration (runtime).\n *\n * Persists a single {@link Webhook} envelope per row so administrators\n * can author, enable/disable, and edit webhook subscriptions from the\n * Studio UI without code changes. The canonical Zod schema for the\n * `definition_json` envelope lives at `@objectstack/spec/automation/webhook`.\n *\n * ## Two authoring doors, one row\n * Rows land here two ways, distinguished by the `managed_by` provenance column:\n * - **admin** — created/edited directly through this object's CRUD UI.\n * - **package** — declared in code (`defineStack({ webhooks })` /\n * `defineWebhook()`) and materialized on boot by\n * `bootstrapDeclaredWebhooks` (#3461). Re-seeded every boot, but an admin\n * edit stamps `customized: true` and freezes the row (seed-not-clobber,\n * mirrors `sys_sharing_rule` #2909).\n *\n * One row per `name`. This plugin's {@link AutoEnqueuer} loads active rows on\n * boot + on `sys_webhook:changed` events, and turns matching `data.record.*`\n * events into deliveries on the shared `service-messaging` HTTP outbox\n * (ADR-0018 M3 — `sys_http_delivery`, drained by the messaging dispatcher).\n *\n * Ownership (ADR-0029 K2.a): this object is **owned by\n * `@objectstack/plugin-webhooks`** — the plugin that consumes these rows. It\n * used to live in the `@objectstack/platform-objects` monolith and be imported\n * here; the definition now lives with its owner so the plugin ships both data\n * and behavior as one unit.\n *\n * Platform-wide on purpose: every project (standalone, single-tenant,\n * cloud) can integrate with external systems (Slack, Stripe, internal\n * services) the same way.\n *\n * @namespace sys\n */\nexport const SysWebhook = ObjectSchema.create({\n name: 'sys_webhook',\n label: 'Webhook',\n pluralLabel: 'Webhooks',\n icon: 'webhook',\n isSystem: true,\n managedBy: 'config',\n // Authoring a webhook from the UI requires a structured form for the\n // headers / auth / retry / payload blocks — the generic JSON textarea\n // is acceptable as a v1 until a dedicated builder lands. Re-enable\n // create/edit/delete so admins can at least toggle `active` and edit\n // simple URL/method fields without round-tripping through code.\n userActions: { create: true, edit: true, delete: true, import: false },\n description: 'Outbound HTTP webhook subscription. Declared in code via defineStack({ webhooks }) / defineWebhook() (materialized into rows on boot) or authored directly in the Studio editor; dispatched by the webhook auto-enqueuer onto the shared HTTP outbox.',\n displayNameField: 'name',\n nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)\n titleFormat: '{label}',\n highlightFields: ['name', 'object_name', 'url', 'active', 'updated_at'],\n\n listViews: {\n active: {\n type: 'grid',\n name: 'active',\n label: 'Active',\n data: { provider: 'object', object: 'sys_webhook' },\n columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],\n filter: [{ field: 'active', operator: 'equals', value: true }],\n sort: [{ field: 'label', order: 'asc' }],\n pagination: { pageSize: 50 },\n },\n inactive: {\n type: 'grid',\n name: 'inactive',\n label: 'Inactive',\n data: { provider: 'object', object: 'sys_webhook' },\n columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],\n filter: [{ field: 'active', operator: 'equals', value: false }],\n sort: [{ field: 'label', order: 'asc' }],\n pagination: { pageSize: 50 },\n },\n by_object: {\n type: 'grid',\n name: 'by_object',\n label: 'By Object',\n data: { provider: 'object', object: 'sys_webhook' },\n columns: ['object_name', 'label', 'url', 'active', 'updated_at'],\n sort: [{ field: 'object_name', order: 'asc' }, { field: 'label', order: 'asc' }],\n grouping: { fields: [{ field: 'object_name', order: 'asc', collapsed: false }] },\n pagination: { pageSize: 100 },\n },\n all_webhooks: {\n type: 'grid',\n name: 'all_webhooks',\n label: 'All',\n data: { provider: 'object', object: 'sys_webhook' },\n columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],\n sort: [{ field: 'label', order: 'asc' }],\n pagination: { pageSize: 50 },\n },\n },\n\n fields: {\n id: Field.text({ label: 'Webhook ID', required: true, readonly: true, group: 'System' }),\n\n name: Field.text({\n label: 'Name',\n required: true,\n maxLength: 100,\n // [#8554] \"unique per organization\", not bare \"unique\" — the bare wording\n // described the installation-wide index this card removed.\n description: 'snake_case name, unique per organization — referenced in logs and audit',\n group: 'Definition',\n }),\n\n label: Field.text({\n label: 'Display Label',\n required: false,\n maxLength: 200,\n group: 'Definition',\n }),\n\n object_name: Field.text({\n label: 'Object',\n required: false,\n maxLength: 100,\n // Object picker (same widget as sys_sharing_rule) instead of a free-text\n // machine name. Falls back to a text input when the widget is unavailable.\n widget: 'object-ref',\n description: 'Short object name whose record events (create/update/delete) fire this webhook',\n group: 'Definition',\n }),\n\n triggers: Field.select(\n ['create', 'update', 'delete', 'bulk_update', 'bulk_delete'],\n {\n label: 'Triggers',\n required: false,\n // Multi-select instead of a hand-typed comma-separated string. Stored as\n // an array; the auto-enqueuer parser also tolerates the legacy\n // comma-separated / JSON-string forms so existing rows keep working.\n multiple: true,\n // [#4639] `bulk_*` fire on predicate writes (`multi: true`), whose\n // delivery carries `matched` instead of a record — opt-in precisely\n // because that body is a different shape. Kept in step with\n // `WebhookTriggerType` (`@objectstack/spec/automation`), which is the\n // contract the auto-enqueuer validates against.\n description: 'Record events that fire this webhook (bulk_* deliver a count, not a record)',\n group: 'Definition',\n },\n ),\n\n url: Field.text({\n label: 'Target URL',\n required: true,\n maxLength: 2048,\n description: 'External endpoint that receives the POST',\n group: 'Definition',\n }),\n\n method: Field.select(\n ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],\n {\n label: 'HTTP Method',\n required: true,\n // Select instead of free text. Option values are lowercased by the\n // Field.select helper (get/post/…); the auto-enqueuer upper-cases the\n // resolved method before delivery, so existing 'POST' rows and the\n // lowercase option values both normalise correctly.\n defaultValue: 'post',\n description: 'HTTP method used for the callback request',\n group: 'Definition',\n },\n ),\n\n description: Field.textarea({ label: 'Description', required: false, group: 'Definition' }),\n\n active: Field.boolean({\n label: 'Active',\n required: true,\n defaultValue: true,\n description: 'Inactive webhooks are skipped by the dispatcher',\n group: 'Definition',\n }),\n\n definition_json: Field.textarea({\n label: 'Definition',\n required: true,\n description: 'Serialised Webhook JSON (see @objectstack/spec/automation/webhook) — timeout and the rest of the authored envelope. Credentials are NOT stored here: the signing secret lives in the encrypted `signing_secret` field and the custom headers in the encrypted `headers_secret` field.',\n group: 'Definition',\n }),\n\n /**\n * [#7986] Custom HTTP headers, in the engine's ENCRYPTED credential channel.\n *\n * The sibling passenger #7799 left on the blob it emptied. That card was\n * framed as \"the signing secret is in cleartext\" and was fixed exactly as\n * framed — but the COLUMN was the problem, and `headers` is the ordinary\n * place an `Authorization: Bearer …` goes. `definition_json` is an ordinary\n * textarea on an admin-authorable object with no restrictive\n * `enable.apiMethods` at all, so `GET /api/v1/data/sys_webhook` returned the\n * header map to every persona that can read the object, with none of the\n * retention bound that eventually ages out `sys_http_delivery`'s copies.\n *\n * The WHOLE map moves rather than the credential-looking entries, because\n * only some entries are credentials and the platform cannot tell which:\n * guessing from the header name is fail-OPEN on exactly the custom spellings\n * (`X-Acme-Token`) most likely to be one, and letting the author declare\n * which are sensitive is a change to the authoring envelope\n * (`webhook.zod.ts`) that belongs to the spec seat. See\n * `webhook-headers.ts` for the full comparison.\n *\n * Stored as the SERIALIZED map (the encrypted channel carries a string);\n * the enqueuer recovers and re-parses it server-side through\n * `engine.resolveSecretField()` when it refreshes its subscription cache,\n * on the same refresh that recovers the signing secret.\n *\n * Fail-closed by construction, the same way `signing_secret` is: with no\n * CryptoProvider the engine REFUSES the write rather than falling back to\n * cleartext, and a stored map that cannot be decrypted DROPS the\n * subscription rather than delivering it with its headers silently missing.\n */\n headers_secret: Field.secret({\n label: 'Custom Headers',\n required: false,\n description:\n 'Custom HTTP headers sent with each delivery, as a JSON object ({\"Authorization\": \"Bearer …\"}). '\n + 'Encrypted at rest into sys_secret; reads return a mask, never the headers. Leave the mask '\n + 'untouched to keep the current value.',\n group: 'Definition',\n }),\n\n /**\n * [#7799] HMAC signing key, in the engine's ENCRYPTED credential channel.\n *\n * It used to ride inside `definition_json` as cleartext, because that column\n * is where the seeder parked the whole authored envelope. `definition_json`\n * is an ordinary textarea on an admin-authorable object with no restrictive\n * `enable.apiMethods`, so `GET /api/v1/data/sys_webhook` handed the key back\n * to every persona that can read the object — and that key is the receiver's\n * ONLY proof a delivery came from us. Same exposure class as #7722's\n * per-attempt copies, minus the retention window that eventually aged those\n * out.\n *\n * `type: 'secret'` moves it onto the channel built for this: the engine\n * encrypts on write via the registered `ICryptoProvider`, stores the\n * ciphertext as a `sys_secret` row, keeps only an opaque `secret:<id>` ref\n * on this column, and returns the mask on every read path. The enqueuer\n * recovers the plaintext server-side through `engine.resolveSecretField()`\n * when it refreshes its subscription cache.\n *\n * Fail-closed by construction: with no CryptoProvider wired the engine\n * REFUSES the write rather than falling back to cleartext, so the seeder\n * skips that webhook loudly instead of re-opening the hole in a new column.\n */\n signing_secret: Field.secret({\n label: 'Signing Secret',\n required: false,\n description:\n 'HMAC-SHA256 key used to sign deliveries (X-Objectstack-Signature). Encrypted at rest into '\n + 'sys_secret; reads return a mask, never the key. Leave the mask untouched to keep the current value.',\n group: 'Definition',\n }),\n\n // ── Provenance (#3461 — record-authoritative seed-not-clobber) ──\n // Mirrors sys_sharing_rule (#2909). Both columns are `readonly`: the\n // engine strips them from non-system payloads (forge/clear-proof), while\n // bootstrapDeclaredWebhooks and the provenance stamp hook write with\n // isSystem. Deliberately NOT a write gate: webhooks are a first-class admin\n // authoring/tuning surface — admins may edit or deactivate a package row;\n // the seeder simply stops overwriting it once `customized` is stamped.\n managed_by: Field.select(\n ['platform', 'package', 'admin'],\n {\n label: 'Managed By',\n required: false,\n readonly: true,\n defaultValue: 'admin',\n description:\n 'Record provenance: platform = framework built-in / package = app/package-declared ' +\n '(boot-seeded from defineStack webhooks) / admin = created in Setup.',\n group: 'System',\n },\n ),\n\n customized: Field.boolean({\n label: 'Customized',\n required: false,\n readonly: true,\n defaultValue: false,\n description:\n 'Set when an admin edits a package-declared webhook; boot seeding will no longer ' +\n 'overwrite the row (a deactivated noisy webhook survives redeploys). Meaningless on admin rows.',\n group: 'System',\n }),\n\n created_at: Field.datetime({\n label: 'Created At',\n required: true,\n defaultValue: 'NOW()',\n readonly: true,\n group: 'System',\n }),\n\n updated_at: Field.datetime({ label: 'Updated At', required: false, group: 'System' }),\n },\n\n indexes: [\n // [#8554] Scope spelled EXPLICITLY (ADR-0120 D1). On a DECLARED index bare\n // `unique: true` is the positional spelling of `'global'` — the listed\n // columns verbatim — so this was an installation-wide key on a tenant-scoped\n // object. Measured live before the fix: org_jia 201 / org_yi 409\n // UNIQUE_VIOLATION on the same name / org_yi unused name 201 / org_yi's own\n // GET on the colliding name 0 rows. Webhooks are named by admins from the\n // UI, so two organizations both wanting `order_created_hook` is ordinary.\n { fields: ['name'], unique: 'organization' },\n { fields: ['object_name'] },\n { fields: ['active', 'object_name'] },\n ],\n\n /**\n * [#9756] The data-API exposure of this object, declared EXPLICITLY.\n *\n * ## Why the block exists\n *\n * Three cards observed that `sys_webhook` declared no `enable` block at all\n * and each named narrowing its read surface as the next step — #7799 (the\n * signing secret), #7986 (the custom headers) and #8025 option 2 (the URL) —\n * and each assumed a later one would write the line. None did. The condition\n * held not because anyone judged the full default API correct here, but\n * because the omission was never anybody's deliverable. That is the standard\n * #8025 set and #9756 quotes back: *an omission is not a decision unless\n * someone wrote it down.* This block is that decision, written down.\n *\n * ## The census the set is derived from (#9756, measured before writing)\n *\n * | consumer | reaches this object through | needs |\n * |:---|:---|:---|\n * | Setup/Studio console — `nav_webhooks` (`webhook-outbox-plugin.ts`), the four list views above, `userActions` create/edit/delete | REST `/api/v1/data/sys_webhook` — the gated data API | `get` `list` `create` `update` `delete` |\n * | Operator predicate write — \"deactivate every webhook on an object\" (#4639, for which `AutoEnqueuer.handleSelfHealEvent` carries a `data.records.*` branch built expressly for this gesture) | REST `updateMany` / `deleteMany`, both gated on the `bulk` primitive | `bulk` |\n * | `AutoEnqueuer` cache refresh, `bootstrapDeclaredWebhooks`, `stampWebhookProvenance`, `redeliver-guard`, `migrateLegacyWebhookSecrets`, the `headers_secret` write gate | `engine.find/findOne/insert/update` and lifecycle hooks — ObjectQL directly, which never consults `enable.apiMethods` | ungated: unaffected by anything declared here |\n *\n * ⇒ every primitive is required by a real, measured consumer, so the set is\n * all six. No consumer outside the admin/operator surface was found.\n *\n * ## ⛔ This narrows NOTHING — do not read it as if it did\n *\n * `resolveEffectiveApiMethods` (`@objectstack/spec/data`) seeds the\n * `unrestricted` branch with the very same `API_PRIMITIVES` set, so the six\n * primitives resolve to the operation closure the *absent* block already\n * produced. The serialized effective set (`/me/permissions`, the 405\n * `allowed` array) is byte-identical, and no route or `callData` action\n * reaches an operation whose answer differs. Only `mode` changes,\n * `unrestricted` → `restricted`.\n *\n * So the presence of this block is NOT evidence that the reachable cleartext\n * on this object was reduced. It was not, and `apiMethods` is the wrong\n * instrument for it: `url` (#8025 — won't-fix on masking, because the URL is\n * the routing key an operator must be able to see, search, sort and edit) and\n * a legacy row's un-migrated `definition_json.headers` (#7986 —\n * `readLegacyHeaders` in `auto-enqueuer.ts` still reads them and warns) are\n * both served by `get`/`list`, which is exactly what the console requires.\n * Any set that removes them removes the admin surface with them. A survey\n * that greps this file for `enable:` and stops is measuring the wrong thing;\n * #9756's report carries the census that says so.\n *\n * Contrast the sibling `sys_http_delivery` (`['get','list']`,\n * `service-messaging`), whose narrowing is real: that table is engine-owned —\n * written only by `SqlHttpOutbox` through context-less raw-engine writes,\n * never authored — so closing its write surface costs nothing. `sys_webhook`\n * is a first-class admin authoring surface. That is the whole difference, and\n * it is why the sibling's shape could not simply be copied here.\n *\n * Pinned — the census, the no-narrowing equality, and the registration-time\n * survival of every write verb — in `sys-webhook-api-exposure.test.ts`.\n */\n enable: {\n apiMethods: ['get', 'list', 'create', 'update', 'delete', 'bulk'],\n },\n});\n"]}
@@ -251,10 +251,69 @@ var SysWebhook = ObjectSchema.create({
251
251
  { fields: ["name"], unique: "organization" },
252
252
  { fields: ["object_name"] },
253
253
  { fields: ["active", "object_name"] }
254
- ]
254
+ ],
255
+ /**
256
+ * [#9756] The data-API exposure of this object, declared EXPLICITLY.
257
+ *
258
+ * ## Why the block exists
259
+ *
260
+ * Three cards observed that `sys_webhook` declared no `enable` block at all
261
+ * and each named narrowing its read surface as the next step — #7799 (the
262
+ * signing secret), #7986 (the custom headers) and #8025 option 2 (the URL) —
263
+ * and each assumed a later one would write the line. None did. The condition
264
+ * held not because anyone judged the full default API correct here, but
265
+ * because the omission was never anybody's deliverable. That is the standard
266
+ * #8025 set and #9756 quotes back: *an omission is not a decision unless
267
+ * someone wrote it down.* This block is that decision, written down.
268
+ *
269
+ * ## The census the set is derived from (#9756, measured before writing)
270
+ *
271
+ * | consumer | reaches this object through | needs |
272
+ * |:---|:---|:---|
273
+ * | Setup/Studio console — `nav_webhooks` (`webhook-outbox-plugin.ts`), the four list views above, `userActions` create/edit/delete | REST `/api/v1/data/sys_webhook` — the gated data API | `get` `list` `create` `update` `delete` |
274
+ * | Operator predicate write — "deactivate every webhook on an object" (#4639, for which `AutoEnqueuer.handleSelfHealEvent` carries a `data.records.*` branch built expressly for this gesture) | REST `updateMany` / `deleteMany`, both gated on the `bulk` primitive | `bulk` |
275
+ * | `AutoEnqueuer` cache refresh, `bootstrapDeclaredWebhooks`, `stampWebhookProvenance`, `redeliver-guard`, `migrateLegacyWebhookSecrets`, the `headers_secret` write gate | `engine.find/findOne/insert/update` and lifecycle hooks — ObjectQL directly, which never consults `enable.apiMethods` | ungated: unaffected by anything declared here |
276
+ *
277
+ * ⇒ every primitive is required by a real, measured consumer, so the set is
278
+ * all six. No consumer outside the admin/operator surface was found.
279
+ *
280
+ * ## ⛔ This narrows NOTHING — do not read it as if it did
281
+ *
282
+ * `resolveEffectiveApiMethods` (`@objectstack/spec/data`) seeds the
283
+ * `unrestricted` branch with the very same `API_PRIMITIVES` set, so the six
284
+ * primitives resolve to the operation closure the *absent* block already
285
+ * produced. The serialized effective set (`/me/permissions`, the 405
286
+ * `allowed` array) is byte-identical, and no route or `callData` action
287
+ * reaches an operation whose answer differs. Only `mode` changes,
288
+ * `unrestricted` → `restricted`.
289
+ *
290
+ * So the presence of this block is NOT evidence that the reachable cleartext
291
+ * on this object was reduced. It was not, and `apiMethods` is the wrong
292
+ * instrument for it: `url` (#8025 — won't-fix on masking, because the URL is
293
+ * the routing key an operator must be able to see, search, sort and edit) and
294
+ * a legacy row's un-migrated `definition_json.headers` (#7986 —
295
+ * `readLegacyHeaders` in `auto-enqueuer.ts` still reads them and warns) are
296
+ * both served by `get`/`list`, which is exactly what the console requires.
297
+ * Any set that removes them removes the admin surface with them. A survey
298
+ * that greps this file for `enable:` and stops is measuring the wrong thing;
299
+ * #9756's report carries the census that says so.
300
+ *
301
+ * Contrast the sibling `sys_http_delivery` (`['get','list']`,
302
+ * `service-messaging`), whose narrowing is real: that table is engine-owned —
303
+ * written only by `SqlHttpOutbox` through context-less raw-engine writes,
304
+ * never authored — so closing its write surface costs nothing. `sys_webhook`
305
+ * is a first-class admin authoring surface. That is the whole difference, and
306
+ * it is why the sibling's shape could not simply be copied here.
307
+ *
308
+ * Pinned — the census, the no-narrowing equality, and the registration-time
309
+ * survival of every write verb — in `sys-webhook-api-exposure.test.ts`.
310
+ */
311
+ enable: {
312
+ apiMethods: ["get", "list", "create", "update", "delete", "bulk"]
313
+ }
255
314
  });
256
315
 
257
316
  export {
258
317
  SysWebhook
259
318
  };
260
- //# sourceMappingURL=chunk-GDCWDVDT.js.map
319
+ //# sourceMappingURL=chunk-XERWWQKN.js.map