@objectstack/plugin-webhooks 16.1.0 → 17.0.0-rc.1

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 (41) hide show
  1. package/CHANGELOG.md +626 -0
  2. package/dist/{chunk-RBCYKJVL.js → chunk-6EPMRZ7I.js} +28 -2
  3. package/dist/chunk-6EPMRZ7I.js.map +1 -0
  4. package/dist/{chunk-PTMJ5BLL.cjs → chunk-QUTQSOQC.cjs} +28 -2
  5. package/dist/chunk-QUTQSOQC.cjs.map +1 -0
  6. package/dist/index.cjs +250 -33
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +25 -0
  9. package/dist/index.d.ts +25 -0
  10. package/dist/index.js +231 -14
  11. package/dist/index.js.map +1 -1
  12. package/dist/schema.cjs +2 -2
  13. package/dist/schema.d.cts +515 -304
  14. package/dist/schema.d.ts +515 -304
  15. package/dist/schema.js +1 -1
  16. package/dist/translations-CBQRUIS5.cjs +383 -0
  17. package/dist/translations-CBQRUIS5.cjs.map +1 -0
  18. package/dist/translations-Y3CXWOTM.js +383 -0
  19. package/dist/translations-Y3CXWOTM.js.map +1 -0
  20. package/package.json +11 -5
  21. package/.turbo/turbo-build.log +0 -36
  22. package/dist/chunk-PTMJ5BLL.cjs.map +0 -1
  23. package/dist/chunk-RBCYKJVL.js.map +0 -1
  24. package/dist/translations-EPJYANXJ.js +0 -727
  25. package/dist/translations-EPJYANXJ.js.map +0 -1
  26. package/dist/translations-L2JAQDJB.cjs +0 -727
  27. package/dist/translations-L2JAQDJB.cjs.map +0 -1
  28. package/scripts/i18n-extract.config.ts +0 -32
  29. package/src/auto-enqueuer.test.ts +0 -431
  30. package/src/auto-enqueuer.ts +0 -383
  31. package/src/index.ts +0 -29
  32. package/src/schema.ts +0 -21
  33. package/src/sys-webhook.object.ts +0 -193
  34. package/src/translations/en.objects.generated.ts +0 -187
  35. package/src/translations/es-ES.objects.generated.ts +0 -187
  36. package/src/translations/index.ts +0 -23
  37. package/src/translations/ja-JP.objects.generated.ts +0 -187
  38. package/src/translations/zh-CN.objects.generated.ts +0 -187
  39. package/src/webhook-outbox-plugin.ts +0 -259
  40. package/tsconfig.json +0 -10
  41. 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'],\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 description: 'Record events that fire this webhook',\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":[]}
@@ -13,7 +13,7 @@ var SysWebhook = _data.ObjectSchema.create({
13
13
  // create/edit/delete so admins can at least toggle `active` and edit
14
14
  // simple URL/method fields without round-tripping through code.
15
15
  userActions: { create: true, edit: true, delete: true, import: false },
16
- description: "Outbound HTTP webhook subscription. Authored via defineWebhook() in code or the Studio editor; executed by the HTTP connector plugin.",
16
+ 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.",
17
17
  displayNameField: "name",
18
18
  nameField: "name",
19
19
  // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
@@ -133,6 +133,32 @@ var SysWebhook = _data.ObjectSchema.create({
133
133
  description: "Serialised Webhook JSON (see @objectstack/spec/automation/webhook) \u2014 full headers/auth/retry/payload config",
134
134
  group: "Definition"
135
135
  }),
136
+ // ── Provenance (#3461 — record-authoritative seed-not-clobber) ──
137
+ // Mirrors sys_sharing_rule (#2909). Both columns are `readonly`: the
138
+ // engine strips them from non-system payloads (forge/clear-proof), while
139
+ // bootstrapDeclaredWebhooks and the provenance stamp hook write with
140
+ // isSystem. Deliberately NOT a write gate: webhooks are a first-class admin
141
+ // authoring/tuning surface — admins may edit or deactivate a package row;
142
+ // the seeder simply stops overwriting it once `customized` is stamped.
143
+ managed_by: _data.Field.select(
144
+ ["platform", "package", "admin"],
145
+ {
146
+ label: "Managed By",
147
+ required: false,
148
+ readonly: true,
149
+ defaultValue: "admin",
150
+ description: "Record provenance: platform = framework built-in / package = app/package-declared (boot-seeded from defineStack webhooks) / admin = created in Setup.",
151
+ group: "System"
152
+ }
153
+ ),
154
+ customized: _data.Field.boolean({
155
+ label: "Customized",
156
+ required: false,
157
+ readonly: true,
158
+ defaultValue: false,
159
+ description: "Set when an admin edits a package-declared webhook; boot seeding will no longer overwrite the row (a deactivated noisy webhook survives redeploys). Meaningless on admin rows.",
160
+ group: "System"
161
+ }),
136
162
  created_at: _data.Field.datetime({
137
163
  label: "Created At",
138
164
  required: true,
@@ -152,4 +178,4 @@ var SysWebhook = _data.ObjectSchema.create({
152
178
 
153
179
 
154
180
  exports.SysWebhook = SysWebhook;
155
- //# sourceMappingURL=chunk-PTMJ5BLL.cjs.map
181
+ //# sourceMappingURL=chunk-QUTQSOQC.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/objectstack/objectstack/packages/plugins/plugin-webhooks/dist/chunk-QUTQSOQC.cjs","../src/sys-webhook.object.ts"],"names":[],"mappings":"AAAA;ACEA,8CAAoC;AAoC7B,IAAM,WAAA,EAAa,kBAAA,CAAa,MAAA,CAAO;AAAA,EAC5C,IAAA,EAAM,aAAA;AAAA,EACN,KAAA,EAAO,SAAA;AAAA,EACP,WAAA,EAAa,UAAA;AAAA,EACb,IAAA,EAAM,SAAA;AAAA,EACN,QAAA,EAAU,IAAA;AAAA,EACV,SAAA,EAAW,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMX,WAAA,EAAa,EAAE,MAAA,EAAQ,IAAA,EAAM,IAAA,EAAM,IAAA,EAAM,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAQ,MAAM,CAAA;AAAA,EACrE,WAAA,EAAa,uPAAA;AAAA,EACb,gBAAA,EAAkB,MAAA;AAAA,EAClB,SAAA,EAAW,MAAA;AAAA;AAAA,EACX,WAAA,EAAa,SAAA;AAAA,EACb,eAAA,EAAiB,CAAC,MAAA,EAAQ,aAAA,EAAe,KAAA,EAAO,QAAA,EAAU,YAAY,CAAA;AAAA,EAEtE,SAAA,EAAW;AAAA,IACT,MAAA,EAAQ;AAAA,MACN,IAAA,EAAM,MAAA;AAAA,MACN,IAAA,EAAM,QAAA;AAAA,MACN,KAAA,EAAO,QAAA;AAAA,MACP,IAAA,EAAM,EAAE,QAAA,EAAU,QAAA,EAAU,MAAA,EAAQ,cAAc,CAAA;AAAA,MAClD,OAAA,EAAS,CAAC,OAAA,EAAS,aAAA,EAAe,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,YAAY,CAAA;AAAA,MACzE,MAAA,EAAQ,CAAC,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,QAAA,EAAU,KAAA,EAAO,KAAK,CAAC,CAAA;AAAA,MAC7D,IAAA,EAAM,CAAC,EAAE,KAAA,EAAO,OAAA,EAAS,KAAA,EAAO,MAAM,CAAC,CAAA;AAAA,MACvC,UAAA,EAAY,EAAE,QAAA,EAAU,GAAG;AAAA,IAC7B,CAAA;AAAA,IACA,QAAA,EAAU;AAAA,MACR,IAAA,EAAM,MAAA;AAAA,MACN,IAAA,EAAM,UAAA;AAAA,MACN,KAAA,EAAO,UAAA;AAAA,MACP,IAAA,EAAM,EAAE,QAAA,EAAU,QAAA,EAAU,MAAA,EAAQ,cAAc,CAAA;AAAA,MAClD,OAAA,EAAS,CAAC,OAAA,EAAS,aAAA,EAAe,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,YAAY,CAAA;AAAA,MACzE,MAAA,EAAQ,CAAC,EAAE,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,QAAA,EAAU,KAAA,EAAO,MAAM,CAAC,CAAA;AAAA,MAC9D,IAAA,EAAM,CAAC,EAAE,KAAA,EAAO,OAAA,EAAS,KAAA,EAAO,MAAM,CAAC,CAAA;AAAA,MACvC,UAAA,EAAY,EAAE,QAAA,EAAU,GAAG;AAAA,IAC7B,CAAA;AAAA,IACA,SAAA,EAAW;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,IAAA,EAAM,WAAA;AAAA,MACN,KAAA,EAAO,WAAA;AAAA,MACP,IAAA,EAAM,EAAE,QAAA,EAAU,QAAA,EAAU,MAAA,EAAQ,cAAc,CAAA;AAAA,MAClD,OAAA,EAAS,CAAC,aAAA,EAAe,OAAA,EAAS,KAAA,EAAO,QAAA,EAAU,YAAY,CAAA;AAAA,MAC/D,IAAA,EAAM,CAAC,EAAE,KAAA,EAAO,aAAA,EAAe,KAAA,EAAO,MAAM,CAAA,EAAG,EAAE,KAAA,EAAO,OAAA,EAAS,KAAA,EAAO,MAAM,CAAC,CAAA;AAAA,MAC/E,QAAA,EAAU,EAAE,MAAA,EAAQ,CAAC,EAAE,KAAA,EAAO,aAAA,EAAe,KAAA,EAAO,KAAA,EAAO,SAAA,EAAW,MAAM,CAAC,EAAE,CAAA;AAAA,MAC/E,UAAA,EAAY,EAAE,QAAA,EAAU,IAAI;AAAA,IAC9B,CAAA;AAAA,IACA,YAAA,EAAc;AAAA,MACZ,IAAA,EAAM,MAAA;AAAA,MACN,IAAA,EAAM,cAAA;AAAA,MACN,KAAA,EAAO,KAAA;AAAA,MACP,IAAA,EAAM,EAAE,QAAA,EAAU,QAAA,EAAU,MAAA,EAAQ,cAAc,CAAA;AAAA,MAClD,OAAA,EAAS,CAAC,OAAA,EAAS,aAAA,EAAe,KAAA,EAAO,QAAA,EAAU,QAAA,EAAU,YAAY,CAAA;AAAA,MACzE,IAAA,EAAM,CAAC,EAAE,KAAA,EAAO,OAAA,EAAS,KAAA,EAAO,MAAM,CAAC,CAAA;AAAA,MACvC,UAAA,EAAY,EAAE,QAAA,EAAU,GAAG;AAAA,IAC7B;AAAA,EACF,CAAA;AAAA,EAEA,MAAA,EAAQ;AAAA,IACN,EAAA,EAAI,WAAA,CAAM,IAAA,CAAK,EAAE,KAAA,EAAO,YAAA,EAAc,QAAA,EAAU,IAAA,EAAM,QAAA,EAAU,IAAA,EAAM,KAAA,EAAO,SAAS,CAAC,CAAA;AAAA,IAEvF,IAAA,EAAM,WAAA,CAAM,IAAA,CAAK;AAAA,MACf,KAAA,EAAO,MAAA;AAAA,MACP,QAAA,EAAU,IAAA;AAAA,MACV,SAAA,EAAW,GAAA;AAAA,MACX,WAAA,EAAa,4DAAA;AAAA,MACb,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,IAED,KAAA,EAAO,WAAA,CAAM,IAAA,CAAK;AAAA,MAChB,KAAA,EAAO,eAAA;AAAA,MACP,QAAA,EAAU,KAAA;AAAA,MACV,SAAA,EAAW,GAAA;AAAA,MACX,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,IAED,WAAA,EAAa,WAAA,CAAM,IAAA,CAAK;AAAA,MACtB,KAAA,EAAO,QAAA;AAAA,MACP,QAAA,EAAU,KAAA;AAAA,MACV,SAAA,EAAW,GAAA;AAAA;AAAA;AAAA,MAGX,MAAA,EAAQ,YAAA;AAAA,MACR,WAAA,EAAa,gFAAA;AAAA,MACb,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,IAED,QAAA,EAAU,WAAA,CAAM,MAAA;AAAA,MACd,CAAC,QAAA,EAAU,QAAA,EAAU,QAAQ,CAAA;AAAA,MAC7B;AAAA,QACE,KAAA,EAAO,UAAA;AAAA,QACP,QAAA,EAAU,KAAA;AAAA;AAAA;AAAA;AAAA,QAIV,QAAA,EAAU,IAAA;AAAA,QACV,WAAA,EAAa,sCAAA;AAAA,QACb,KAAA,EAAO;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IAEA,GAAA,EAAK,WAAA,CAAM,IAAA,CAAK;AAAA,MACd,KAAA,EAAO,YAAA;AAAA,MACP,QAAA,EAAU,IAAA;AAAA,MACV,SAAA,EAAW,IAAA;AAAA,MACX,WAAA,EAAa,0CAAA;AAAA,MACb,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,IAED,MAAA,EAAQ,WAAA,CAAM,MAAA;AAAA,MACZ,CAAC,KAAA,EAAO,MAAA,EAAQ,KAAA,EAAO,OAAA,EAAS,QAAQ,CAAA;AAAA,MACxC;AAAA,QACE,KAAA,EAAO,aAAA;AAAA,QACP,QAAA,EAAU,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAKV,YAAA,EAAc,MAAA;AAAA,QACd,WAAA,EAAa,2CAAA;AAAA,QACb,KAAA,EAAO;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IAEA,WAAA,EAAa,WAAA,CAAM,QAAA,CAAS,EAAE,KAAA,EAAO,aAAA,EAAe,QAAA,EAAU,KAAA,EAAO,KAAA,EAAO,aAAa,CAAC,CAAA;AAAA,IAE1F,MAAA,EAAQ,WAAA,CAAM,OAAA,CAAQ;AAAA,MACpB,KAAA,EAAO,QAAA;AAAA,MACP,QAAA,EAAU,IAAA;AAAA,MACV,YAAA,EAAc,IAAA;AAAA,MACd,WAAA,EAAa,iDAAA;AAAA,MACb,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,IAED,eAAA,EAAiB,WAAA,CAAM,QAAA,CAAS;AAAA,MAC9B,KAAA,EAAO,YAAA;AAAA,MACP,QAAA,EAAU,IAAA;AAAA,MACV,WAAA,EAAa,kHAAA;AAAA,MACb,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASD,UAAA,EAAY,WAAA,CAAM,MAAA;AAAA,MAChB,CAAC,UAAA,EAAY,SAAA,EAAW,OAAO,CAAA;AAAA,MAC/B;AAAA,QACE,KAAA,EAAO,YAAA;AAAA,QACP,QAAA,EAAU,KAAA;AAAA,QACV,QAAA,EAAU,IAAA;AAAA,QACV,YAAA,EAAc,OAAA;AAAA,QACd,WAAA,EACE,uJAAA;AAAA,QAEF,KAAA,EAAO;AAAA,MACT;AAAA,IACF,CAAA;AAAA,IAEA,UAAA,EAAY,WAAA,CAAM,OAAA,CAAQ;AAAA,MACxB,KAAA,EAAO,YAAA;AAAA,MACP,QAAA,EAAU,KAAA;AAAA,MACV,QAAA,EAAU,IAAA;AAAA,MACV,YAAA,EAAc,KAAA;AAAA,MACd,WAAA,EACE,gLAAA;AAAA,MAEF,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,IAED,UAAA,EAAY,WAAA,CAAM,QAAA,CAAS;AAAA,MACzB,KAAA,EAAO,YAAA;AAAA,MACP,QAAA,EAAU,IAAA;AAAA,MACV,YAAA,EAAc,OAAA;AAAA,MACd,QAAA,EAAU,IAAA;AAAA,MACV,KAAA,EAAO;AAAA,IACT,CAAC,CAAA;AAAA,IAED,UAAA,EAAY,WAAA,CAAM,QAAA,CAAS,EAAE,KAAA,EAAO,YAAA,EAAc,QAAA,EAAU,KAAA,EAAO,KAAA,EAAO,SAAS,CAAC;AAAA,EACtF,CAAA;AAAA,EAEA,OAAA,EAAS;AAAA,IACP,EAAE,MAAA,EAAQ,CAAC,MAAM,CAAA,EAAG,MAAA,EAAQ,KAAK,CAAA;AAAA,IACjC,EAAE,MAAA,EAAQ,CAAC,aAAa,EAAE,CAAA;AAAA,IAC1B,EAAE,MAAA,EAAQ,CAAC,QAAA,EAAU,aAAa,EAAE;AAAA,EACtC;AACF,CAAC,CAAA;ADtDD;AACA;AACE;AACF,gCAAC","file":"/home/runner/work/objectstack/objectstack/packages/plugins/plugin-webhooks/dist/chunk-QUTQSOQC.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'],\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 description: 'Record events that fire this webhook',\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 _chunkPTMJ5BLLcjs = require('./chunk-PTMJ5BLL.cjs');
3
+ var _chunkQUTQSOQCcjs = require('./chunk-QUTQSOQC.cjs');
4
4
 
5
5
  // src/auto-enqueuer.ts
6
6
  var AutoEnqueuer = class {
@@ -180,12 +180,21 @@ var AutoEnqueuer = class {
180
180
  headers: sub.headers,
181
181
  signingSecret: sub.secret,
182
182
  timeoutMs: sub.timeoutMs,
183
+ // [#3946] Envelope keys are written LAST so the event payload
184
+ // cannot rewrite them. Behaviour-neutral for the engine's own
185
+ // publishers — `data.record.*` payloads are
186
+ // `{ recordId, after, changes }`, with record fields nested
187
+ // under `after`, so none of these four keys collide today. It
188
+ // is the shape that was wrong: a publisher that flattened
189
+ // record fields into the payload (the `payload.id` fallback
190
+ // above suggests some do) would have silently rewritten the
191
+ // `object` / `action` / `timestamp` a subscriber receives.
183
192
  payload: {
193
+ ...payload,
184
194
  object: event.object,
185
195
  recordId,
186
196
  action,
187
- timestamp: event.timestamp,
188
- ...payload
197
+ timestamp: event.timestamp
189
198
  }
190
199
  }).catch(
191
200
  (err) => _optionalChain([this, 'access', _25 => _25.logger, 'access', _26 => _26.warn, 'optionalCall', _27 => _27("[webhook-auto-enqueuer] enqueue failed", {
@@ -222,6 +231,163 @@ function mapActionToTrigger(action) {
222
231
  }
223
232
  var DISPATCHABLE_WEBHOOK_TRIGGERS = /* @__PURE__ */ new Set(["create", "update", "delete"]);
224
233
 
234
+ // src/bootstrap-declared-webhooks.ts
235
+ var _automation = require('@objectstack/spec/automation');
236
+ var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
237
+ function uid(prefix) {
238
+ const g = globalThis;
239
+ if (_optionalChain([g, 'access', _35 => _35.crypto, 'optionalAccess', _36 => _36.randomUUID])) return `${prefix}_${g.crypto.randomUUID()}`;
240
+ return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
241
+ }
242
+ function readDeclared(engine, metadataService, type) {
243
+ try {
244
+ const reg = _optionalChain([engine, 'optionalAccess', _37 => _37._registry]);
245
+ if (_optionalChain([reg, 'optionalAccess', _38 => _38.listItems])) {
246
+ const items = (_nullishCoalesce(reg.listItems(type), () => ( []))).map((i) => _nullishCoalesce(_optionalChain([i, 'optionalAccess', _39 => _39.content]), () => ( i))).filter(Boolean);
247
+ if (items.length > 0) return items;
248
+ }
249
+ } catch (e3) {
250
+ }
251
+ try {
252
+ const listed = _optionalChain([metadataService, 'optionalAccess', _40 => _40.list, 'optionalCall', _41 => _41(type)]);
253
+ const arr = typeof _optionalChain([listed, 'optionalAccess', _42 => _42.then]) === "function" ? [] : _nullishCoalesce(listed, () => ( []));
254
+ return Array.isArray(arr) ? arr.map((i) => _nullishCoalesce(_optionalChain([i, 'optionalAccess', _43 => _43.content]), () => ( i))).filter(Boolean) : [];
255
+ } catch (e4) {
256
+ return [];
257
+ }
258
+ }
259
+ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscriptionsObject = "sys_webhook") {
260
+ const declared = readDeclared(engine, metadataService, "webhook");
261
+ if (declared.length === 0) return { seeded: 0, skipped: 0 };
262
+ const now = (/* @__PURE__ */ new Date()).toISOString();
263
+ let seeded = 0;
264
+ let skipped = 0;
265
+ for (const raw of declared) {
266
+ let wh;
267
+ try {
268
+ wh = _automation.WebhookSchema.parse(raw);
269
+ } catch (err) {
270
+ _optionalChain([logger, 'optionalAccess', _44 => _44.warn, 'optionalCall', _45 => _45("[webhook] declared webhook failed validation \u2014 skipped", {
271
+ name: _optionalChain([raw, 'optionalAccess', _46 => _46.name]),
272
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _47 => _47.message]), () => ( String(err)))
273
+ })]);
274
+ skipped += 1;
275
+ continue;
276
+ }
277
+ try {
278
+ const existing = await engine.find(subscriptionsObject, {
279
+ where: { name: wh.name },
280
+ limit: 1,
281
+ context: SYSTEM_CTX
282
+ });
283
+ const row = Array.isArray(existing) ? existing[0] : void 0;
284
+ if (row) {
285
+ if (row.managed_by === "admin") {
286
+ _optionalChain([logger, 'optionalAccess', _48 => _48.warn, 'optionalCall', _49 => _49("[webhook] declared name collides with an admin-authored row \u2014 seed skipped", {
287
+ name: wh.name
288
+ })]);
289
+ skipped += 1;
290
+ continue;
291
+ }
292
+ if (row.customized === true) {
293
+ skipped += 1;
294
+ continue;
295
+ }
296
+ const patch = {
297
+ id: row.id,
298
+ ...mapWebhookToRow(wh),
299
+ // Adopt pristine/legacy (pre-provenance) rows so future boots
300
+ // recognize them as package-managed.
301
+ managed_by: "package",
302
+ updated_at: now
303
+ };
304
+ await engine.update(subscriptionsObject, patch, { context: SYSTEM_CTX });
305
+ seeded += 1;
306
+ continue;
307
+ }
308
+ const newRow = {
309
+ id: uid("whk"),
310
+ ...mapWebhookToRow(wh),
311
+ managed_by: "package",
312
+ customized: false,
313
+ created_at: now,
314
+ updated_at: now
315
+ };
316
+ await engine.insert(subscriptionsObject, newRow, { context: SYSTEM_CTX });
317
+ seeded += 1;
318
+ } catch (err) {
319
+ _optionalChain([logger, 'optionalAccess', _50 => _50.warn, 'optionalCall', _51 => _51("[webhook] declared webhook seed failed", {
320
+ name: wh.name,
321
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _52 => _52.message]), () => ( String(err)))
322
+ })]);
323
+ skipped += 1;
324
+ }
325
+ }
326
+ _optionalChain([logger, 'optionalAccess', _53 => _53.info, 'optionalCall', _54 => _54("[webhook] declared webhooks materialized into sys_webhook", {
327
+ seeded,
328
+ skipped,
329
+ total: declared.length
330
+ })]);
331
+ return { seeded, skipped };
332
+ }
333
+ function mapWebhookToRow(wh) {
334
+ return {
335
+ name: wh.name,
336
+ label: _nullishCoalesce(wh.label, () => ( wh.name)),
337
+ object_name: _nullishCoalesce(wh.object, () => ( null)),
338
+ triggers: _nullishCoalesce(wh.triggers, () => ( [])),
339
+ url: wh.url,
340
+ // Store lowercase to match the object's Field.select option values
341
+ // (get/post/…); the enqueuer upper-cases before delivery either way.
342
+ method: String(_nullishCoalesce(wh.method, () => ( "POST"))).toLowerCase(),
343
+ description: _nullishCoalesce(wh.description, () => ( null)),
344
+ active: wh.isActive !== false,
345
+ definition_json: JSON.stringify(wh)
346
+ };
347
+ }
348
+
349
+ // src/webhook-provenance.ts
350
+ var WEBHOOK_PROVENANCE_PACKAGE = "plugin-webhooks:provenance";
351
+ var SYSTEM_CTX2 = { isSystem: true, positions: [], permissions: [] };
352
+ function bindWebhookProvenanceStamp(engine, logger) {
353
+ if (typeof _optionalChain([engine, 'optionalAccess', _55 => _55.registerHook]) !== "function") return;
354
+ engine.registerHook(
355
+ "beforeUpdate",
356
+ async (ctx) => {
357
+ if (_optionalChain([ctx, 'optionalAccess', _56 => _56.session, 'optionalAccess', _57 => _57.isSystem])) return;
358
+ 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])));
359
+ if (!id) return;
360
+ const data = _optionalChain([ctx, 'optionalAccess', _63 => _63.input, 'optionalAccess', _64 => _64.data]);
361
+ if (!data || typeof data !== "object") return;
362
+ try {
363
+ const rows = await engine.find("sys_webhook", {
364
+ where: { id },
365
+ fields: ["id", "managed_by", "customized"],
366
+ limit: 1,
367
+ context: SYSTEM_CTX2
368
+ });
369
+ const row = Array.isArray(rows) ? rows[0] : void 0;
370
+ if (!row) return;
371
+ if ((row.managed_by === "package" || row.managed_by === "platform") && row.customized !== true) {
372
+ data.customized = true;
373
+ }
374
+ } catch (err) {
375
+ _optionalChain([logger, 'optionalAccess', _65 => _65.warn, 'optionalCall', _66 => _66("[webhook] provenance stamp failed (edit proceeds unstamped)", {
376
+ id,
377
+ error: _optionalChain([err, 'optionalAccess', _67 => _67.message])
378
+ })]);
379
+ }
380
+ },
381
+ { object: "sys_webhook", packageId: WEBHOOK_PROVENANCE_PACKAGE, priority: 150 }
382
+ );
383
+ _optionalChain([logger, 'optionalAccess', _68 => _68.info, 'optionalCall', _69 => _69("[webhook] provenance stamp hook bound")]);
384
+ }
385
+ function unbindWebhookProvenanceStamp(engine) {
386
+ if (typeof _optionalChain([engine, 'optionalAccess', _70 => _70.unregisterHooksByPackage]) === "function") {
387
+ engine.unregisterHooksByPackage(WEBHOOK_PROVENANCE_PACKAGE);
388
+ }
389
+ }
390
+
225
391
  // src/webhook-outbox-plugin.ts
226
392
  var WebhookOutboxPlugin = class {
227
393
  constructor(options = {}) {
@@ -230,6 +396,16 @@ var WebhookOutboxPlugin = class {
230
396
  this.version = "2.0.0";
231
397
  this.type = "standard";
232
398
  this.dependencies = ["com.objectstack.service.messaging"];
399
+ /**
400
+ * `init()` registers this plugin's schema through `manifest` with no
401
+ * fallback. Until #4187 that was safe only TRANSITIVELY — messaging happens
402
+ * to depend on ObjectQL, which provides `manifest` — so the guarantee would
403
+ * have evaporated silently the day messaging stopped depending on the
404
+ * engine, and the failure would have surfaced as an unrelated plugin's
405
+ * init crash. Declaring the requirement directly makes the kernel check it
406
+ * regardless of what messaging depends on.
407
+ */
408
+ this.requiresServices = ["manifest"];
233
409
  }
234
410
  async init(ctx) {
235
411
  const manifest = ctx.getService("manifest");
@@ -242,7 +418,7 @@ var WebhookOutboxPlugin = class {
242
418
  scope: "system",
243
419
  name: "Webhook Schemas",
244
420
  description: "Registers sys_webhook (configuration). Deliveries use messaging's sys_http_delivery outbox.",
245
- objects: [_chunkPTMJ5BLLcjs.SysWebhook],
421
+ objects: [_chunkQUTQSOQCcjs.SysWebhook],
246
422
  navigationContributions: [
247
423
  {
248
424
  app: "setup",
@@ -256,7 +432,7 @@ var WebhookOutboxPlugin = class {
256
432
  ]
257
433
  });
258
434
  } else {
259
- _optionalChain([ctx, 'access', _35 => _35.logger, 'access', _36 => _36.warn, 'optionalCall', _37 => _37(
435
+ _optionalChain([ctx, 'access', _71 => _71.logger, 'access', _72 => _72.warn, 'optionalCall', _73 => _73(
260
436
  "[webhook-outbox] manifest service unavailable \u2014 sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin."
261
437
  )]);
262
438
  }
@@ -265,47 +441,88 @@ var WebhookOutboxPlugin = class {
265
441
  try {
266
442
  const i18n = ctx.getService("i18n");
267
443
  if (i18n && typeof i18n.loadTranslations === "function") {
268
- const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-L2JAQDJB.cjs")));
444
+ const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-CBQRUIS5.cjs")));
269
445
  for (const [locale, data] of Object.entries(WebhooksTranslations)) {
270
446
  i18n.loadTranslations(locale, data);
271
447
  }
272
448
  }
273
- } catch (e3) {
449
+ } catch (e5) {
274
450
  }
275
451
  });
276
452
  }
277
453
  const autoEnqueueOpt = _nullishCoalesce(this.options.autoEnqueue, () => ( true));
278
454
  if (typeof ctx.hook === "function") {
279
455
  ctx.hook("kernel:ready", async () => {
456
+ await this.bootDeclaredWebhooks(ctx);
280
457
  await this.bootAutoEnqueue(ctx, autoEnqueueOpt);
281
458
  this.registerAdminRoutes(ctx);
282
459
  });
283
460
  }
284
- _optionalChain([ctx, 'access', _38 => _38.logger, 'access', _39 => _39.info, 'optionalCall', _40 => _40("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
461
+ _optionalChain([ctx, 'access', _74 => _74.logger, 'access', _75 => _75.info, 'optionalCall', _76 => _76("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
285
462
  autoEnqueue: autoEnqueueOpt !== false
286
463
  })]);
287
464
  }
288
465
  async dispose() {
289
- await _optionalChain([this, 'access', _41 => _41.autoEnqueuer, 'optionalAccess', _42 => _42.stop, 'call', _43 => _43()]);
466
+ await _optionalChain([this, 'access', _77 => _77.autoEnqueuer, 'optionalAccess', _78 => _78.stop, 'call', _79 => _79()]);
467
+ if (this.boundEngine) {
468
+ try {
469
+ unbindWebhookProvenanceStamp(this.boundEngine);
470
+ } catch (e6) {
471
+ }
472
+ this.boundEngine = void 0;
473
+ }
290
474
  }
291
475
  getMessaging(ctx) {
292
476
  const svc = this.tryGetService(ctx, ["messaging"]);
293
477
  return svc && typeof svc.enqueueHttp === "function" ? svc : void 0;
294
478
  }
479
+ /**
480
+ * [#3461] Bridge the declarative authoring surface to the dispatcher:
481
+ * materialize stack/connector-declared `webhook` metadata into `sys_webhook`
482
+ * rows so the auto-enqueuer (and the Studio UI) can see them.
483
+ *
484
+ * Gated on the DATA ENGINE alone — deliberately independent of the
485
+ * auto-enqueue dispatch prerequisites (realtime + messaging). Materializing
486
+ * rows is a pure write; a deployment that mounts the webhook plugin without
487
+ * realtime must still get its declared webhooks into the table (and the
488
+ * Setup UI), even if nothing dispatches them yet. Runs before
489
+ * {@link bootAutoEnqueue} so the enqueuer's first cache refresh sees the rows.
490
+ */
491
+ async bootDeclaredWebhooks(ctx) {
492
+ const engine = this.tryGetService(ctx, ["objectql", "data"]);
493
+ if (!engine) {
494
+ _optionalChain([ctx, 'access', _80 => _80.logger, 'access', _81 => _81.warn, 'optionalCall', _82 => _82("[webhook] declared-webhook bootstrap skipped \u2014 no data engine available")]);
495
+ return;
496
+ }
497
+ this.boundEngine = engine;
498
+ bindWebhookProvenanceStamp(engine, ctx.logger);
499
+ let metadataService;
500
+ try {
501
+ metadataService = ctx.getService("metadata");
502
+ } catch (e7) {
503
+ }
504
+ try {
505
+ await bootstrapDeclaredWebhooks(engine, metadataService, ctx.logger);
506
+ } catch (err) {
507
+ _optionalChain([ctx, 'access', _83 => _83.logger, 'access', _84 => _84.warn, 'optionalCall', _85 => _85("[webhook] declared-webhook bootstrap failed (dispatcher still serves admin rows)", {
508
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _86 => _86.message]), () => ( String(err)))
509
+ })]);
510
+ }
511
+ }
295
512
  async bootAutoEnqueue(ctx, opt) {
296
513
  if (opt === false) return;
297
514
  const engine = this.tryGetService(ctx, ["objectql", "data"]);
298
515
  const realtime = this.tryGetService(ctx, ["realtime"]);
299
516
  const messaging = this.getMessaging(ctx);
300
517
  if (!engine || !realtime || !messaging) {
301
- _optionalChain([ctx, 'access', _44 => _44.logger, 'access', _45 => _45.warn, 'optionalCall', _46 => _46(
518
+ _optionalChain([ctx, 'access', _87 => _87.logger, 'access', _88 => _88.warn, 'optionalCall', _89 => _89(
302
519
  "[webhook-auto-enqueuer] disabled \u2014 ObjectQL, Realtime, or Messaging service not available",
303
520
  { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging }
304
521
  )]);
305
522
  return;
306
523
  }
307
524
  if (!messaging.isHttpDeliveryReady()) {
308
- _optionalChain([ctx, 'access', _47 => _47.logger, 'access', _48 => _48.warn, 'optionalCall', _49 => _49(
525
+ _optionalChain([ctx, 'access', _90 => _90.logger, 'access', _91 => _91.warn, 'optionalCall', _92 => _92(
309
526
  "[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) \u2014 webhook deliveries will not be durable"
310
527
  )]);
311
528
  }
@@ -318,14 +535,14 @@ var WebhookOutboxPlugin = class {
318
535
  );
319
536
  await this.autoEnqueuer.start();
320
537
  ctx.registerService("webhook.autoEnqueuer", this.autoEnqueuer);
321
- _optionalChain([ctx, 'access', _50 => _50.logger, 'access', _51 => _51.info, 'optionalCall', _52 => _52("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
538
+ _optionalChain([ctx, 'access', _93 => _93.logger, 'access', _94 => _94.info, 'optionalCall', _95 => _95("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
322
539
  }
323
540
  tryGetService(ctx, names) {
324
541
  for (const n of names) {
325
542
  try {
326
543
  const svc = ctx.getService(n);
327
544
  if (svc) return svc;
328
- } catch (e4) {
545
+ } catch (e8) {
329
546
  }
330
547
  }
331
548
  return void 0;
@@ -338,7 +555,7 @@ var WebhookOutboxPlugin = class {
338
555
  registerAdminRoutes(ctx) {
339
556
  const http = this.tryGetService(ctx, ["http-server"]);
340
557
  if (!http || typeof http.getRawApp !== "function") {
341
- _optionalChain([ctx, 'access', _53 => _53.logger, 'access', _54 => _54.debug, 'optionalCall', _55 => _55("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
558
+ _optionalChain([ctx, 'access', _96 => _96.logger, 'access', _97 => _97.debug, 'optionalCall', _98 => _98("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
342
559
  return;
343
560
  }
344
561
  const rawApp = http.getRawApp();
@@ -348,43 +565,43 @@ var WebhookOutboxPlugin = class {
348
565
  const userId = await this.resolveSessionUserId(ctx, c);
349
566
  if (!userId) {
350
567
  return c.json(
351
- { success: false, error: "unauthenticated", message: "Sign in to redeliver webhook deliveries." },
568
+ { success: false, error: { code: "UNAUTHENTICATED", message: "Sign in to redeliver webhook deliveries." } },
352
569
  401
353
570
  );
354
571
  }
355
572
  let body;
356
573
  try {
357
574
  body = await c.req.json();
358
- } catch (e5) {
359
- return c.json({ success: false, error: "invalid_body", message: "Request body must be JSON." }, 400);
575
+ } catch (e9) {
576
+ return c.json({ success: false, error: { code: "INVALID_REQUEST", message: "Request body must be JSON." } }, 400);
360
577
  }
361
- const deliveryId = typeof _optionalChain([body, 'optionalAccess', _56 => _56.deliveryId]) === "string" ? body.deliveryId.trim() : "";
578
+ const deliveryId = typeof _optionalChain([body, 'optionalAccess', _99 => _99.deliveryId]) === "string" ? body.deliveryId.trim() : "";
362
579
  if (!deliveryId) {
363
580
  return c.json(
364
- { success: false, error: "missing_delivery_id", message: "Body must include `deliveryId: string`." },
581
+ { success: false, error: { code: "MISSING_REQUIRED_FIELD", message: "Body must include `deliveryId: string`." } },
365
582
  400
366
583
  );
367
584
  }
368
585
  try {
369
586
  const row = await messaging.redeliverHttp(deliveryId);
370
- _optionalChain([ctx, 'access', _57 => _57.logger, 'access', _58 => _58.info, 'optionalCall', _59 => _59("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId })]);
587
+ _optionalChain([ctx, 'access', _100 => _100.logger, 'access', _101 => _101.info, 'optionalCall', _102 => _102("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId })]);
371
588
  return c.json({ success: true, data: { id: row.id, status: row.status } });
372
589
  } catch (err) {
373
- const code = _optionalChain([err, 'optionalAccess', _60 => _60.code]);
374
- if (code === "not_found") {
375
- return c.json({ success: false, error: "not_found", message: err.message }, 404);
590
+ const code = _optionalChain([err, 'optionalAccess', _103 => _103.code]);
591
+ if (code === "RESOURCE_NOT_FOUND") {
592
+ return c.json({ success: false, error: { code, message: err.message } }, 404);
376
593
  }
377
- if (code === "not_eligible") {
378
- return c.json({ success: false, error: "not_eligible", message: err.message }, 409);
594
+ if (code === "DELIVERY_NOT_ELIGIBLE") {
595
+ return c.json({ success: false, error: { code, message: err.message } }, 409);
379
596
  }
380
- _optionalChain([ctx, 'access', _61 => _61.logger, 'access', _62 => _62.error, 'optionalCall', _63 => _63("[webhook-outbox] redeliver failed", err)]);
597
+ _optionalChain([ctx, 'access', _104 => _104.logger, 'access', _105 => _105.error, 'optionalCall', _106 => _106("[webhook-outbox] redeliver failed", err)]);
381
598
  return c.json(
382
- { success: false, error: "internal_error", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _64 => _64.message]), () => ( String(err))) },
599
+ { success: false, error: { code: "INTERNAL_ERROR", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _107 => _107.message]), () => ( String(err))) } },
383
600
  500
384
601
  );
385
602
  }
386
603
  });
387
- _optionalChain([ctx, 'access', _65 => _65.logger, 'access', _66 => _66.info, 'optionalCall', _67 => _67("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
604
+ _optionalChain([ctx, 'access', _108 => _108.logger, 'access', _109 => _109.info, 'optionalCall', _110 => _110("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
388
605
  }
389
606
  async resolveSessionUserId(ctx, c) {
390
607
  try {
@@ -394,11 +611,11 @@ var WebhookOutboxPlugin = class {
394
611
  if (!api && typeof authService.getApi === "function") {
395
612
  api = await authService.getApi();
396
613
  }
397
- if (!_optionalChain([api, 'optionalAccess', _68 => _68.getSession])) return void 0;
614
+ if (!_optionalChain([api, 'optionalAccess', _111 => _111.getSession])) return void 0;
398
615
  const session = await api.getSession({ headers: c.req.raw.headers });
399
- const uid = _optionalChain([session, 'optionalAccess', _69 => _69.user, 'optionalAccess', _70 => _70.id]);
400
- return typeof uid === "string" && uid.length > 0 ? uid : void 0;
401
- } catch (e6) {
616
+ const uid2 = _optionalChain([session, 'optionalAccess', _112 => _112.user, 'optionalAccess', _113 => _113.id]);
617
+ return typeof uid2 === "string" && uid2.length > 0 ? uid2 : void 0;
618
+ } catch (e10) {
402
619
  return void 0;
403
620
  }
404
621
  }
@@ -407,5 +624,5 @@ var WebhookOutboxPlugin = class {
407
624
 
408
625
 
409
626
 
410
- exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkPTMJ5BLLcjs.SysWebhook; exports.WebhookOutboxPlugin = WebhookOutboxPlugin;
627
+ exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkQUTQSOQCcjs.SysWebhook; exports.WebhookOutboxPlugin = WebhookOutboxPlugin;
411
628
  //# sourceMappingURL=index.cjs.map