@objectstack/plugin-webhooks 17.0.0-rc.0 → 17.0.0-rc.2

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +632 -0
  2. package/dist/{chunk-6EPMRZ7I.js → chunk-3QGZLM3T.js} +8 -3
  3. package/dist/chunk-3QGZLM3T.js.map +1 -0
  4. package/dist/{chunk-QUTQSOQC.cjs → chunk-JQUVS5KK.cjs} +8 -3
  5. package/dist/chunk-JQUVS5KK.cjs.map +1 -0
  6. package/dist/index.cjs +182 -63
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +37 -1
  9. package/dist/index.d.ts +37 -1
  10. package/dist/index.js +138 -19
  11. package/dist/index.js.map +1 -1
  12. package/dist/schema.cjs +2 -2
  13. package/dist/schema.d.cts +721 -548
  14. package/dist/schema.d.ts +721 -548
  15. package/dist/schema.js +1 -1
  16. package/dist/{translations-Y3CXWOTM.js → translations-2VGD4XRF.js} +17 -9
  17. package/dist/translations-2VGD4XRF.js.map +1 -0
  18. package/dist/{translations-CBQRUIS5.cjs → translations-BWS57U2V.cjs} +17 -9
  19. package/dist/translations-BWS57U2V.cjs.map +1 -0
  20. package/package.json +12 -6
  21. package/.turbo/turbo-build.log +0 -36
  22. package/dist/chunk-6EPMRZ7I.js.map +0 -1
  23. package/dist/chunk-QUTQSOQC.cjs.map +0 -1
  24. package/dist/translations-CBQRUIS5.cjs.map +0 -1
  25. package/dist/translations-Y3CXWOTM.js.map +0 -1
  26. package/scripts/i18n-extract.config.ts +0 -34
  27. package/src/auto-enqueuer.test.ts +0 -431
  28. package/src/auto-enqueuer.ts +0 -383
  29. package/src/bootstrap-declared-webhooks.test.ts +0 -283
  30. package/src/bootstrap-declared-webhooks.ts +0 -205
  31. package/src/index.ts +0 -29
  32. package/src/schema.ts +0 -21
  33. package/src/sys-webhook.object.ts +0 -231
  34. package/src/translations/bundle-ownership.test.ts +0 -41
  35. package/src/translations/en.objects.generated.ts +0 -101
  36. package/src/translations/es-ES.objects.generated.ts +0 -101
  37. package/src/translations/index.ts +0 -23
  38. package/src/translations/ja-JP.objects.generated.ts +0 -101
  39. package/src/translations/zh-CN.objects.generated.ts +0 -101
  40. package/src/webhook-outbox-plugin.ts +0 -305
  41. package/src/webhook-provenance.ts +0 -86
  42. package/tsconfig.json +0 -10
  43. package/tsup.config.ts +0 -14
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sys-webhook.object.ts"],"sourcesContent":["// 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 description: 'Unique snake_case name — 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) — full headers/auth/retry/payload config',\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 { fields: ['name'], unique: true },\n { fields: ['object_name'] },\n { fields: ['active', 'object_name'] },\n ],\n});\n"],"mappings":"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","names":[]}
@@ -86,7 +86,7 @@ var SysWebhook = _data.ObjectSchema.create({
86
86
  group: "Definition"
87
87
  }),
88
88
  triggers: _data.Field.select(
89
- ["create", "update", "delete"],
89
+ ["create", "update", "delete", "bulk_update", "bulk_delete"],
90
90
  {
91
91
  label: "Triggers",
92
92
  required: false,
@@ -94,7 +94,12 @@ var SysWebhook = _data.ObjectSchema.create({
94
94
  // an array; the auto-enqueuer parser also tolerates the legacy
95
95
  // comma-separated / JSON-string forms so existing rows keep working.
96
96
  multiple: true,
97
- description: "Record events that fire this webhook",
97
+ // [#4639] `bulk_*` fire on predicate writes (`multi: true`), whose
98
+ // delivery carries `matched` instead of a record — opt-in precisely
99
+ // because that body is a different shape. Kept in step with
100
+ // `WebhookTriggerType` (`@objectstack/spec/automation`), which is the
101
+ // contract the auto-enqueuer validates against.
102
+ description: "Record events that fire this webhook (bulk_* deliver a count, not a record)",
98
103
  group: "Definition"
99
104
  }
100
105
  ),
@@ -178,4 +183,4 @@ var SysWebhook = _data.ObjectSchema.create({
178
183
 
179
184
 
180
185
  exports.SysWebhook = SysWebhook;
181
- //# sourceMappingURL=chunk-QUTQSOQC.cjs.map
186
+ //# sourceMappingURL=chunk-JQUVS5KK.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/objectstack/objectstack/packages/plugins/plugin-webhooks/dist/chunk-JQUVS5KK.cjs","../src/sys-webhook.object.ts"],"names":[],"mappings":"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","file":"/home/runner/work/objectstack/objectstack/packages/plugins/plugin-webhooks/dist/chunk-JQUVS5KK.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 description: 'Unique snake_case name — 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) — full headers/auth/retry/payload config',\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 { fields: ['name'], unique: true },\n { fields: ['object_name'] },\n { fields: ['active', 'object_name'] },\n ],\n});\n"]}
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkQUTQSOQCcjs = require('./chunk-QUTQSOQC.cjs');
3
+ var _chunkJQUVS5KKcjs = require('./chunk-JQUVS5KK.cjs');
4
4
 
5
5
  // src/auto-enqueuer.ts
6
6
  var AutoEnqueuer = class {
@@ -112,7 +112,7 @@ var AutoEnqueuer = class {
112
112
  const unknown = normalized.filter((t) => !DISPATCHABLE_WEBHOOK_TRIGGERS.has(t));
113
113
  if (unknown.length > 0) {
114
114
  _optionalChain([this, 'access', _15 => _15.logger, 'access', _16 => _16.warn, 'optionalCall', _17 => _17(
115
- `[webhook-auto-enqueuer] webhook '${_nullishCoalesce(row.name, () => ( row.id))}' declares trigger(s) the engine never emits: ${unknown.join(", ")} \u2014 ignored. Dispatchable triggers: create, update, delete.`,
115
+ `[webhook-auto-enqueuer] webhook '${_nullishCoalesce(row.name, () => ( row.id))}' declares trigger(s) the engine never emits: ${unknown.join(", ")} \u2014 ignored. Dispatchable triggers: ${[...DISPATCHABLE_WEBHOOK_TRIGGERS].join(", ")}.`,
116
116
  { id: row.id, unknown }
117
117
  )]);
118
118
  }
@@ -120,6 +120,10 @@ var AutoEnqueuer = class {
120
120
  normalized.filter((t) => DISPATCHABLE_WEBHOOK_TRIGGERS.has(t))
121
121
  );
122
122
  if (triggers.size === 0) {
123
+ _optionalChain([this, 'access', _18 => _18.logger, 'access', _19 => _19.warn, 'optionalCall', _20 => _20(
124
+ `[webhook-auto-enqueuer] webhook '${_nullishCoalesce(row.name, () => ( row.id))}' has no dispatchable triggers \u2014 it will NEVER fire (rule webhook/without-triggers): there is no manual fire path (#3196), so this row is dead while looking armed in Setup. Declare one of: ${[...DISPATCHABLE_WEBHOOK_TRIGGERS].join(", ")}, or set it inactive if it should be off.`,
125
+ { id: row.id }
126
+ )]);
123
127
  return null;
124
128
  }
125
129
  let defn = {};
@@ -154,9 +158,13 @@ var AutoEnqueuer = class {
154
158
  * webhook persistence.
155
159
  */
156
160
  handleEvent(event) {
157
- if (!_optionalChain([event, 'access', _18 => _18.type, 'optionalAccess', _19 => _19.startsWith, 'call', _20 => _20("data.record.")])) return;
158
161
  if (!event.object) return;
159
162
  if (event.object === this.subscriptionsObject) return;
163
+ if (_optionalChain([event, 'access', _21 => _21.type, 'optionalAccess', _22 => _22.startsWith, 'call', _23 => _23("data.records.")])) {
164
+ this.handleBulkEvent(event);
165
+ return;
166
+ }
167
+ if (!_optionalChain([event, 'access', _24 => _24.type, 'optionalAccess', _25 => _25.startsWith, 'call', _26 => _26("data.record.")])) return;
160
168
  const action = event.type.slice("data.record.".length);
161
169
  const trigger = mapActionToTrigger(action);
162
170
  if (!trigger) return;
@@ -166,7 +174,14 @@ var AutoEnqueuer = class {
166
174
  ];
167
175
  if (subs.length === 0) return;
168
176
  const payload = _nullishCoalesce(event.payload, () => ( {}));
169
- const recordId = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(payload.recordId, () => ( payload.id)), () => ( _optionalChain([payload, 'access', _21 => _21.after, 'optionalAccess', _22 => _22.id]))), () => ( _optionalChain([payload, 'access', _23 => _23.before, 'optionalAccess', _24 => _24.id]))), () => ( "unknown"));
177
+ const recordId = payload.recordId;
178
+ if (typeof recordId !== "string" || recordId === "") {
179
+ _optionalChain([this, 'access', _27 => _27.logger, 'access', _28 => _28.warn, 'optionalCall', _29 => _29(
180
+ "[webhook-auto-enqueuer] dropping off-contract data event: payload is not a DataEvent (no top-level string `recordId`) \u2014 fix the producer",
181
+ { type: event.type, object: event.object }
182
+ )]);
183
+ return;
184
+ }
170
185
  const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;
171
186
  for (const sub of subs) {
172
187
  if (!sub.triggers.has(trigger)) continue;
@@ -180,27 +195,105 @@ var AutoEnqueuer = class {
180
195
  headers: sub.headers,
181
196
  signingSecret: sub.secret,
182
197
  timeoutMs: sub.timeoutMs,
198
+ // [#3946] Envelope keys are written LAST so the event payload
199
+ // cannot rewrite them. Behaviour-neutral for the engine's own
200
+ // publishers — since #4626 a `data.record.*` payload is a
201
+ // `DataEvent` (`id`, `type`, `object`, `recordId`, `changes?`,
202
+ // `after?`, `userId?`, `timestamp`), whose `object` /
203
+ // `recordId` / `timestamp` carry the SAME values written here
204
+ // and whose record fields stay nested under `after`. It is the
205
+ // shape that was wrong: a publisher that flattened record
206
+ // fields into the payload would have silently rewritten the
207
+ // `object` / `action` / `timestamp` a subscriber receives.
183
208
  payload: {
209
+ ...payload,
184
210
  object: event.object,
185
211
  recordId,
186
212
  action,
187
- timestamp: event.timestamp,
188
- ...payload
213
+ timestamp: event.timestamp
214
+ }
215
+ }).catch(
216
+ (err) => _optionalChain([this, 'access', _30 => _30.logger, 'access', _31 => _31.warn, 'optionalCall', _32 => _32("[webhook-auto-enqueuer] enqueue failed", {
217
+ webhook: sub.name,
218
+ eventId,
219
+ err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _33 => _33.message]), () => ( err))
220
+ })])
221
+ );
222
+ }
223
+ }
224
+ /**
225
+ * Handler for aggregate `data.records.*` events — a predicate write
226
+ * (`multi: true`) that the driver reports only as an affected-row count
227
+ * (#4639).
228
+ *
229
+ * Deliberately NOT folded into {@link handleEvent}'s per-record path. The
230
+ * delivered body has no `recordId` and no record fields, so a subscriber
231
+ * to `update` that started receiving these would get a payload missing
232
+ * everything it reads — which is how the pre-#4626 `recordId: ''`
233
+ * fabrication broke consumers, just arriving from the other side. A
234
+ * webhook opts in with `bulk_update` / `bulk_delete`.
235
+ */
236
+ handleBulkEvent(event) {
237
+ const action = event.type.slice("data.records.".length);
238
+ const trigger = mapBulkActionToTrigger(action);
239
+ if (!trigger) return;
240
+ const subs = [
241
+ ..._nullishCoalesce(this.subscriptions.get(event.object), () => ( [])),
242
+ ..._nullishCoalesce(this.subscriptions.get("*"), () => ( []))
243
+ ];
244
+ if (subs.length === 0) return;
245
+ const payload = _nullishCoalesce(event.payload, () => ( {}));
246
+ const matched = payload.matched;
247
+ if (typeof matched !== "number" || !Number.isInteger(matched) || matched < 0) {
248
+ _optionalChain([this, 'access', _34 => _34.logger, 'access', _35 => _35.warn, 'optionalCall', _36 => _36(
249
+ "[webhook-auto-enqueuer] dropping off-contract bulk data event: payload is not a BulkDataEvent (no top-level non-negative integer `matched`) \u2014 fix the producer",
250
+ { type: event.type, object: event.object }
251
+ )]);
252
+ return;
253
+ }
254
+ const eventUuid = payload.id;
255
+ if (typeof eventUuid !== "string" || eventUuid === "") {
256
+ _optionalChain([this, 'access', _37 => _37.logger, 'access', _38 => _38.warn, 'optionalCall', _39 => _39(
257
+ "[webhook-auto-enqueuer] dropping off-contract bulk data event: payload has no top-level string `id` to dedup on \u2014 fix the producer",
258
+ { type: event.type, object: event.object }
259
+ )]);
260
+ return;
261
+ }
262
+ const eventId = `${event.object}:${event.type}:${eventUuid}`;
263
+ for (const sub of subs) {
264
+ if (!sub.triggers.has(trigger)) continue;
265
+ void this.enqueue({
266
+ source: "webhook",
267
+ refId: sub.id,
268
+ dedupKey: `${sub.id}:${eventId}`,
269
+ label: event.type,
270
+ url: sub.url,
271
+ method: sub.method,
272
+ headers: sub.headers,
273
+ signingSecret: sub.secret,
274
+ timeoutMs: sub.timeoutMs,
275
+ // [#3946] Envelope keys last so the payload cannot rewrite them.
276
+ payload: {
277
+ ...payload,
278
+ object: event.object,
279
+ matched,
280
+ action,
281
+ timestamp: event.timestamp
189
282
  }
190
283
  }).catch(
191
- (err) => _optionalChain([this, 'access', _25 => _25.logger, 'access', _26 => _26.warn, 'optionalCall', _27 => _27("[webhook-auto-enqueuer] enqueue failed", {
284
+ (err) => _optionalChain([this, 'access', _40 => _40.logger, 'access', _41 => _41.warn, 'optionalCall', _42 => _42("[webhook-auto-enqueuer] bulk enqueue failed", {
192
285
  webhook: sub.name,
193
286
  eventId,
194
- err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _28 => _28.message]), () => ( err))
287
+ err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _43 => _43.message]), () => ( err))
195
288
  })])
196
289
  );
197
290
  }
198
291
  }
199
292
  handleSelfHealEvent(event) {
200
293
  if (event.object !== this.subscriptionsObject) return;
201
- if (!_optionalChain([event, 'access', _29 => _29.type, 'optionalAccess', _30 => _30.startsWith, 'call', _31 => _31("data.record.")])) return;
294
+ if (!_optionalChain([event, 'access', _44 => _44.type, 'optionalAccess', _45 => _45.startsWith, 'call', _46 => _46("data.record.")]) && !_optionalChain([event, 'access', _47 => _47.type, 'optionalAccess', _48 => _48.startsWith, 'call', _49 => _49("data.records.")])) return;
202
295
  this.refresh().catch(
203
- (err) => _optionalChain([this, 'access', _32 => _32.logger, 'access', _33 => _33.warn, 'optionalCall', _34 => _34("[webhook-auto-enqueuer] self-heal refresh failed", err)])
296
+ (err) => _optionalChain([this, 'access', _50 => _50.logger, 'access', _51 => _51.warn, 'optionalCall', _52 => _52("[webhook-auto-enqueuer] self-heal refresh failed", err)])
204
297
  );
205
298
  }
206
299
  /** Test / admin accessor. */
@@ -220,29 +313,45 @@ function mapActionToTrigger(action) {
220
313
  return null;
221
314
  }
222
315
  }
223
- var DISPATCHABLE_WEBHOOK_TRIGGERS = /* @__PURE__ */ new Set(["create", "update", "delete"]);
316
+ function mapBulkActionToTrigger(action) {
317
+ switch (action) {
318
+ case "updated":
319
+ return "bulk_update";
320
+ case "deleted":
321
+ return "bulk_delete";
322
+ default:
323
+ return null;
324
+ }
325
+ }
326
+ var DISPATCHABLE_WEBHOOK_TRIGGERS = /* @__PURE__ */ new Set([
327
+ "create",
328
+ "update",
329
+ "delete",
330
+ "bulk_update",
331
+ "bulk_delete"
332
+ ]);
224
333
 
225
334
  // src/bootstrap-declared-webhooks.ts
226
335
  var _automation = require('@objectstack/spec/automation');
227
336
  var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
228
337
  function uid(prefix) {
229
338
  const g = globalThis;
230
- if (_optionalChain([g, 'access', _35 => _35.crypto, 'optionalAccess', _36 => _36.randomUUID])) return `${prefix}_${g.crypto.randomUUID()}`;
339
+ if (_optionalChain([g, 'access', _53 => _53.crypto, 'optionalAccess', _54 => _54.randomUUID])) return `${prefix}_${g.crypto.randomUUID()}`;
231
340
  return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
232
341
  }
233
342
  function readDeclared(engine, metadataService, type) {
234
343
  try {
235
- const reg = _optionalChain([engine, 'optionalAccess', _37 => _37._registry]);
236
- if (_optionalChain([reg, 'optionalAccess', _38 => _38.listItems])) {
237
- const items = (_nullishCoalesce(reg.listItems(type), () => ( []))).map((i) => _nullishCoalesce(_optionalChain([i, 'optionalAccess', _39 => _39.content]), () => ( i))).filter(Boolean);
344
+ const reg = _optionalChain([engine, 'optionalAccess', _55 => _55._registry]);
345
+ if (_optionalChain([reg, 'optionalAccess', _56 => _56.listItems])) {
346
+ const items = (_nullishCoalesce(reg.listItems(type), () => ( []))).map((i) => _nullishCoalesce(_optionalChain([i, 'optionalAccess', _57 => _57.content]), () => ( i))).filter(Boolean);
238
347
  if (items.length > 0) return items;
239
348
  }
240
349
  } catch (e3) {
241
350
  }
242
351
  try {
243
- const listed = _optionalChain([metadataService, 'optionalAccess', _40 => _40.list, 'optionalCall', _41 => _41(type)]);
244
- const arr = typeof _optionalChain([listed, 'optionalAccess', _42 => _42.then]) === "function" ? [] : _nullishCoalesce(listed, () => ( []));
245
- return Array.isArray(arr) ? arr.map((i) => _nullishCoalesce(_optionalChain([i, 'optionalAccess', _43 => _43.content]), () => ( i))).filter(Boolean) : [];
352
+ const listed = _optionalChain([metadataService, 'optionalAccess', _58 => _58.list, 'optionalCall', _59 => _59(type)]);
353
+ const arr = typeof _optionalChain([listed, 'optionalAccess', _60 => _60.then]) === "function" ? [] : _nullishCoalesce(listed, () => ( []));
354
+ return Array.isArray(arr) ? arr.map((i) => _nullishCoalesce(_optionalChain([i, 'optionalAccess', _61 => _61.content]), () => ( i))).filter(Boolean) : [];
246
355
  } catch (e4) {
247
356
  return [];
248
357
  }
@@ -258,23 +367,23 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
258
367
  try {
259
368
  wh = _automation.WebhookSchema.parse(raw);
260
369
  } catch (err) {
261
- _optionalChain([logger, 'optionalAccess', _44 => _44.warn, 'optionalCall', _45 => _45("[webhook] declared webhook failed validation \u2014 skipped", {
262
- name: _optionalChain([raw, 'optionalAccess', _46 => _46.name]),
263
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _47 => _47.message]), () => ( String(err)))
370
+ _optionalChain([logger, 'optionalAccess', _62 => _62.warn, 'optionalCall', _63 => _63("[webhook] declared webhook failed validation \u2014 skipped", {
371
+ name: _optionalChain([raw, 'optionalAccess', _64 => _64.name]),
372
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _65 => _65.message]), () => ( String(err)))
264
373
  })]);
265
374
  skipped += 1;
266
375
  continue;
267
376
  }
268
377
  try {
269
378
  const existing = await engine.find(subscriptionsObject, {
270
- filter: { name: wh.name },
379
+ where: { name: wh.name },
271
380
  limit: 1,
272
381
  context: SYSTEM_CTX
273
382
  });
274
383
  const row = Array.isArray(existing) ? existing[0] : void 0;
275
384
  if (row) {
276
385
  if (row.managed_by === "admin") {
277
- _optionalChain([logger, 'optionalAccess', _48 => _48.warn, 'optionalCall', _49 => _49("[webhook] declared name collides with an admin-authored row \u2014 seed skipped", {
386
+ _optionalChain([logger, 'optionalAccess', _66 => _66.warn, 'optionalCall', _67 => _67("[webhook] declared name collides with an admin-authored row \u2014 seed skipped", {
278
387
  name: wh.name
279
388
  })]);
280
389
  skipped += 1;
@@ -307,14 +416,14 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
307
416
  await engine.insert(subscriptionsObject, newRow, { context: SYSTEM_CTX });
308
417
  seeded += 1;
309
418
  } catch (err) {
310
- _optionalChain([logger, 'optionalAccess', _50 => _50.warn, 'optionalCall', _51 => _51("[webhook] declared webhook seed failed", {
419
+ _optionalChain([logger, 'optionalAccess', _68 => _68.warn, 'optionalCall', _69 => _69("[webhook] declared webhook seed failed", {
311
420
  name: wh.name,
312
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _52 => _52.message]), () => ( String(err)))
421
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _70 => _70.message]), () => ( String(err)))
313
422
  })]);
314
423
  skipped += 1;
315
424
  }
316
425
  }
317
- _optionalChain([logger, 'optionalAccess', _53 => _53.info, 'optionalCall', _54 => _54("[webhook] declared webhooks materialized into sys_webhook", {
426
+ _optionalChain([logger, 'optionalAccess', _71 => _71.info, 'optionalCall', _72 => _72("[webhook] declared webhooks materialized into sys_webhook", {
318
427
  seeded,
319
428
  skipped,
320
429
  total: declared.length
@@ -341,18 +450,18 @@ function mapWebhookToRow(wh) {
341
450
  var WEBHOOK_PROVENANCE_PACKAGE = "plugin-webhooks:provenance";
342
451
  var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
343
452
  function bindWebhookProvenanceStamp(engine, logger) {
344
- if (typeof _optionalChain([engine, 'optionalAccess', _55 => _55.registerHook]) !== "function") return;
453
+ if (typeof _optionalChain([engine, 'optionalAccess', _73 => _73.registerHook]) !== "function") return;
345
454
  engine.registerHook(
346
455
  "beforeUpdate",
347
456
  async (ctx) => {
348
- if (_optionalChain([ctx, 'optionalAccess', _56 => _56.session, 'optionalAccess', _57 => _57.isSystem])) return;
349
- const id = _nullishCoalesce(_optionalChain([ctx, 'optionalAccess', _58 => _58.input, 'optionalAccess', _59 => _59.id]), () => ( _optionalChain([ctx, 'optionalAccess', _60 => _60.input, 'optionalAccess', _61 => _61.data, 'optionalAccess', _62 => _62.id])));
457
+ if (_optionalChain([ctx, 'optionalAccess', _74 => _74.session, 'optionalAccess', _75 => _75.isSystem])) return;
458
+ const id = _nullishCoalesce(_optionalChain([ctx, 'optionalAccess', _76 => _76.input, 'optionalAccess', _77 => _77.id]), () => ( _optionalChain([ctx, 'optionalAccess', _78 => _78.input, 'optionalAccess', _79 => _79.data, 'optionalAccess', _80 => _80.id])));
350
459
  if (!id) return;
351
- const data = _optionalChain([ctx, 'optionalAccess', _63 => _63.input, 'optionalAccess', _64 => _64.data]);
460
+ const data = _optionalChain([ctx, 'optionalAccess', _81 => _81.input, 'optionalAccess', _82 => _82.data]);
352
461
  if (!data || typeof data !== "object") return;
353
462
  try {
354
463
  const rows = await engine.find("sys_webhook", {
355
- filter: { id },
464
+ where: { id },
356
465
  fields: ["id", "managed_by", "customized"],
357
466
  limit: 1,
358
467
  context: SYSTEM_CTX2
@@ -363,18 +472,18 @@ function bindWebhookProvenanceStamp(engine, logger) {
363
472
  data.customized = true;
364
473
  }
365
474
  } catch (err) {
366
- _optionalChain([logger, 'optionalAccess', _65 => _65.warn, 'optionalCall', _66 => _66("[webhook] provenance stamp failed (edit proceeds unstamped)", {
475
+ _optionalChain([logger, 'optionalAccess', _83 => _83.warn, 'optionalCall', _84 => _84("[webhook] provenance stamp failed (edit proceeds unstamped)", {
367
476
  id,
368
- error: _optionalChain([err, 'optionalAccess', _67 => _67.message])
477
+ error: _optionalChain([err, 'optionalAccess', _85 => _85.message])
369
478
  })]);
370
479
  }
371
480
  },
372
481
  { object: "sys_webhook", packageId: WEBHOOK_PROVENANCE_PACKAGE, priority: 150 }
373
482
  );
374
- _optionalChain([logger, 'optionalAccess', _68 => _68.info, 'optionalCall', _69 => _69("[webhook] provenance stamp hook bound")]);
483
+ _optionalChain([logger, 'optionalAccess', _86 => _86.info, 'optionalCall', _87 => _87("[webhook] provenance stamp hook bound")]);
375
484
  }
376
485
  function unbindWebhookProvenanceStamp(engine) {
377
- if (typeof _optionalChain([engine, 'optionalAccess', _70 => _70.unregisterHooksByPackage]) === "function") {
486
+ if (typeof _optionalChain([engine, 'optionalAccess', _88 => _88.unregisterHooksByPackage]) === "function") {
378
487
  engine.unregisterHooksByPackage(WEBHOOK_PROVENANCE_PACKAGE);
379
488
  }
380
489
  }
@@ -387,6 +496,16 @@ var WebhookOutboxPlugin = class {
387
496
  this.version = "2.0.0";
388
497
  this.type = "standard";
389
498
  this.dependencies = ["com.objectstack.service.messaging"];
499
+ /**
500
+ * `init()` registers this plugin's schema through `manifest` with no
501
+ * fallback. Until #4187 that was safe only TRANSITIVELY — messaging happens
502
+ * to depend on ObjectQL, which provides `manifest` — so the guarantee would
503
+ * have evaporated silently the day messaging stopped depending on the
504
+ * engine, and the failure would have surfaced as an unrelated plugin's
505
+ * init crash. Declaring the requirement directly makes the kernel check it
506
+ * regardless of what messaging depends on.
507
+ */
508
+ this.requiresServices = ["manifest"];
390
509
  }
391
510
  async init(ctx) {
392
511
  const manifest = ctx.getService("manifest");
@@ -399,7 +518,7 @@ var WebhookOutboxPlugin = class {
399
518
  scope: "system",
400
519
  name: "Webhook Schemas",
401
520
  description: "Registers sys_webhook (configuration). Deliveries use messaging's sys_http_delivery outbox.",
402
- objects: [_chunkQUTQSOQCcjs.SysWebhook],
521
+ objects: [_chunkJQUVS5KKcjs.SysWebhook],
403
522
  navigationContributions: [
404
523
  {
405
524
  app: "setup",
@@ -413,7 +532,7 @@ var WebhookOutboxPlugin = class {
413
532
  ]
414
533
  });
415
534
  } else {
416
- _optionalChain([ctx, 'access', _71 => _71.logger, 'access', _72 => _72.warn, 'optionalCall', _73 => _73(
535
+ _optionalChain([ctx, 'access', _89 => _89.logger, 'access', _90 => _90.warn, 'optionalCall', _91 => _91(
417
536
  "[webhook-outbox] manifest service unavailable \u2014 sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin."
418
537
  )]);
419
538
  }
@@ -422,7 +541,7 @@ var WebhookOutboxPlugin = class {
422
541
  try {
423
542
  const i18n = ctx.getService("i18n");
424
543
  if (i18n && typeof i18n.loadTranslations === "function") {
425
- const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-CBQRUIS5.cjs")));
544
+ const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-BWS57U2V.cjs")));
426
545
  for (const [locale, data] of Object.entries(WebhooksTranslations)) {
427
546
  i18n.loadTranslations(locale, data);
428
547
  }
@@ -439,12 +558,12 @@ var WebhookOutboxPlugin = class {
439
558
  this.registerAdminRoutes(ctx);
440
559
  });
441
560
  }
442
- _optionalChain([ctx, 'access', _74 => _74.logger, 'access', _75 => _75.info, 'optionalCall', _76 => _76("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
561
+ _optionalChain([ctx, 'access', _92 => _92.logger, 'access', _93 => _93.info, 'optionalCall', _94 => _94("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
443
562
  autoEnqueue: autoEnqueueOpt !== false
444
563
  })]);
445
564
  }
446
565
  async dispose() {
447
- await _optionalChain([this, 'access', _77 => _77.autoEnqueuer, 'optionalAccess', _78 => _78.stop, 'call', _79 => _79()]);
566
+ await _optionalChain([this, 'access', _95 => _95.autoEnqueuer, 'optionalAccess', _96 => _96.stop, 'call', _97 => _97()]);
448
567
  if (this.boundEngine) {
449
568
  try {
450
569
  unbindWebhookProvenanceStamp(this.boundEngine);
@@ -472,7 +591,7 @@ var WebhookOutboxPlugin = class {
472
591
  async bootDeclaredWebhooks(ctx) {
473
592
  const engine = this.tryGetService(ctx, ["objectql", "data"]);
474
593
  if (!engine) {
475
- _optionalChain([ctx, 'access', _80 => _80.logger, 'access', _81 => _81.warn, 'optionalCall', _82 => _82("[webhook] declared-webhook bootstrap skipped \u2014 no data engine available")]);
594
+ _optionalChain([ctx, 'access', _98 => _98.logger, 'access', _99 => _99.warn, 'optionalCall', _100 => _100("[webhook] declared-webhook bootstrap skipped \u2014 no data engine available")]);
476
595
  return;
477
596
  }
478
597
  this.boundEngine = engine;
@@ -485,8 +604,8 @@ var WebhookOutboxPlugin = class {
485
604
  try {
486
605
  await bootstrapDeclaredWebhooks(engine, metadataService, ctx.logger);
487
606
  } catch (err) {
488
- _optionalChain([ctx, 'access', _83 => _83.logger, 'access', _84 => _84.warn, 'optionalCall', _85 => _85("[webhook] declared-webhook bootstrap failed (dispatcher still serves admin rows)", {
489
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _86 => _86.message]), () => ( String(err)))
607
+ _optionalChain([ctx, 'access', _101 => _101.logger, 'access', _102 => _102.warn, 'optionalCall', _103 => _103("[webhook] declared-webhook bootstrap failed (dispatcher still serves admin rows)", {
608
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _104 => _104.message]), () => ( String(err)))
490
609
  })]);
491
610
  }
492
611
  }
@@ -496,14 +615,14 @@ var WebhookOutboxPlugin = class {
496
615
  const realtime = this.tryGetService(ctx, ["realtime"]);
497
616
  const messaging = this.getMessaging(ctx);
498
617
  if (!engine || !realtime || !messaging) {
499
- _optionalChain([ctx, 'access', _87 => _87.logger, 'access', _88 => _88.warn, 'optionalCall', _89 => _89(
618
+ _optionalChain([ctx, 'access', _105 => _105.logger, 'access', _106 => _106.warn, 'optionalCall', _107 => _107(
500
619
  "[webhook-auto-enqueuer] disabled \u2014 ObjectQL, Realtime, or Messaging service not available",
501
620
  { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging }
502
621
  )]);
503
622
  return;
504
623
  }
505
624
  if (!messaging.isHttpDeliveryReady()) {
506
- _optionalChain([ctx, 'access', _90 => _90.logger, 'access', _91 => _91.warn, 'optionalCall', _92 => _92(
625
+ _optionalChain([ctx, 'access', _108 => _108.logger, 'access', _109 => _109.warn, 'optionalCall', _110 => _110(
507
626
  "[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) \u2014 webhook deliveries will not be durable"
508
627
  )]);
509
628
  }
@@ -516,7 +635,7 @@ var WebhookOutboxPlugin = class {
516
635
  );
517
636
  await this.autoEnqueuer.start();
518
637
  ctx.registerService("webhook.autoEnqueuer", this.autoEnqueuer);
519
- _optionalChain([ctx, 'access', _93 => _93.logger, 'access', _94 => _94.info, 'optionalCall', _95 => _95("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
638
+ _optionalChain([ctx, 'access', _111 => _111.logger, 'access', _112 => _112.info, 'optionalCall', _113 => _113("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
520
639
  }
521
640
  tryGetService(ctx, names) {
522
641
  for (const n of names) {
@@ -536,7 +655,7 @@ var WebhookOutboxPlugin = class {
536
655
  registerAdminRoutes(ctx) {
537
656
  const http = this.tryGetService(ctx, ["http-server"]);
538
657
  if (!http || typeof http.getRawApp !== "function") {
539
- _optionalChain([ctx, 'access', _96 => _96.logger, 'access', _97 => _97.debug, 'optionalCall', _98 => _98("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
658
+ _optionalChain([ctx, 'access', _114 => _114.logger, 'access', _115 => _115.debug, 'optionalCall', _116 => _116("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
540
659
  return;
541
660
  }
542
661
  const rawApp = http.getRawApp();
@@ -546,7 +665,7 @@ var WebhookOutboxPlugin = class {
546
665
  const userId = await this.resolveSessionUserId(ctx, c);
547
666
  if (!userId) {
548
667
  return c.json(
549
- { success: false, error: "unauthenticated", message: "Sign in to redeliver webhook deliveries." },
668
+ { success: false, error: { code: "UNAUTHENTICATED", message: "Sign in to redeliver webhook deliveries." } },
550
669
  401
551
670
  );
552
671
  }
@@ -554,35 +673,35 @@ var WebhookOutboxPlugin = class {
554
673
  try {
555
674
  body = await c.req.json();
556
675
  } catch (e9) {
557
- return c.json({ success: false, error: "invalid_body", message: "Request body must be JSON." }, 400);
676
+ return c.json({ success: false, error: { code: "INVALID_REQUEST", message: "Request body must be JSON." } }, 400);
558
677
  }
559
- const deliveryId = typeof _optionalChain([body, 'optionalAccess', _99 => _99.deliveryId]) === "string" ? body.deliveryId.trim() : "";
678
+ const deliveryId = typeof _optionalChain([body, 'optionalAccess', _117 => _117.deliveryId]) === "string" ? body.deliveryId.trim() : "";
560
679
  if (!deliveryId) {
561
680
  return c.json(
562
- { success: false, error: "missing_delivery_id", message: "Body must include `deliveryId: string`." },
681
+ { success: false, error: { code: "MISSING_REQUIRED_FIELD", message: "Body must include `deliveryId: string`." } },
563
682
  400
564
683
  );
565
684
  }
566
685
  try {
567
686
  const row = await messaging.redeliverHttp(deliveryId);
568
- _optionalChain([ctx, 'access', _100 => _100.logger, 'access', _101 => _101.info, 'optionalCall', _102 => _102("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId })]);
687
+ _optionalChain([ctx, 'access', _118 => _118.logger, 'access', _119 => _119.info, 'optionalCall', _120 => _120("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId })]);
569
688
  return c.json({ success: true, data: { id: row.id, status: row.status } });
570
689
  } catch (err) {
571
- const code = _optionalChain([err, 'optionalAccess', _103 => _103.code]);
572
- if (code === "not_found") {
573
- return c.json({ success: false, error: "not_found", message: err.message }, 404);
690
+ const code = _optionalChain([err, 'optionalAccess', _121 => _121.code]);
691
+ if (code === "RESOURCE_NOT_FOUND") {
692
+ return c.json({ success: false, error: { code, message: err.message } }, 404);
574
693
  }
575
- if (code === "not_eligible") {
576
- return c.json({ success: false, error: "not_eligible", message: err.message }, 409);
694
+ if (code === "DELIVERY_NOT_ELIGIBLE") {
695
+ return c.json({ success: false, error: { code, message: err.message } }, 409);
577
696
  }
578
- _optionalChain([ctx, 'access', _104 => _104.logger, 'access', _105 => _105.error, 'optionalCall', _106 => _106("[webhook-outbox] redeliver failed", err)]);
697
+ _optionalChain([ctx, 'access', _122 => _122.logger, 'access', _123 => _123.error, 'optionalCall', _124 => _124("[webhook-outbox] redeliver failed", err)]);
579
698
  return c.json(
580
- { success: false, error: "internal_error", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _107 => _107.message]), () => ( String(err))) },
699
+ { success: false, error: { code: "INTERNAL_ERROR", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _125 => _125.message]), () => ( String(err))) } },
581
700
  500
582
701
  );
583
702
  }
584
703
  });
585
- _optionalChain([ctx, 'access', _108 => _108.logger, 'access', _109 => _109.info, 'optionalCall', _110 => _110("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
704
+ _optionalChain([ctx, 'access', _126 => _126.logger, 'access', _127 => _127.info, 'optionalCall', _128 => _128("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
586
705
  }
587
706
  async resolveSessionUserId(ctx, c) {
588
707
  try {
@@ -592,9 +711,9 @@ var WebhookOutboxPlugin = class {
592
711
  if (!api && typeof authService.getApi === "function") {
593
712
  api = await authService.getApi();
594
713
  }
595
- if (!_optionalChain([api, 'optionalAccess', _111 => _111.getSession])) return void 0;
714
+ if (!_optionalChain([api, 'optionalAccess', _129 => _129.getSession])) return void 0;
596
715
  const session = await api.getSession({ headers: c.req.raw.headers });
597
- const uid2 = _optionalChain([session, 'optionalAccess', _112 => _112.user, 'optionalAccess', _113 => _113.id]);
716
+ const uid2 = _optionalChain([session, 'optionalAccess', _130 => _130.user, 'optionalAccess', _131 => _131.id]);
598
717
  return typeof uid2 === "string" && uid2.length > 0 ? uid2 : void 0;
599
718
  } catch (e10) {
600
719
  return void 0;
@@ -605,5 +724,5 @@ var WebhookOutboxPlugin = class {
605
724
 
606
725
 
607
726
 
608
- exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkQUTQSOQCcjs.SysWebhook; exports.WebhookOutboxPlugin = WebhookOutboxPlugin;
727
+ exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkJQUVS5KKcjs.SysWebhook; exports.WebhookOutboxPlugin = WebhookOutboxPlugin;
609
728
  //# sourceMappingURL=index.cjs.map