@objectstack/plugin-webhooks 15.1.1 → 16.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +20 -20
- package/CHANGELOG.md +51 -0
- package/dist/{chunk-I63ISOGO.cjs → chunk-PTMJ5BLL.cjs} +4 -4
- package/dist/chunk-PTMJ5BLL.cjs.map +1 -0
- package/dist/{chunk-YQG6SUP3.js → chunk-RBCYKJVL.js} +4 -4
- package/dist/chunk-RBCYKJVL.js.map +1 -0
- package/dist/index.cjs +35 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/schema.cjs +2 -2
- package/dist/schema.d.cts +145 -830
- package/dist/schema.d.ts +145 -830
- package/dist/schema.js +1 -1
- package/dist/{translations-AV47AVPV.js → translations-EPJYANXJ.js} +9 -9
- package/dist/translations-EPJYANXJ.js.map +1 -0
- package/dist/{translations-OAKKANSP.cjs → translations-L2JAQDJB.cjs} +9 -9
- package/dist/translations-L2JAQDJB.cjs.map +1 -0
- package/package.json +4 -4
- package/src/auto-enqueuer.test.ts +37 -0
- package/src/auto-enqueuer.ts +24 -10
- package/src/sys-webhook.object.ts +3 -3
- package/src/translations/en.objects.generated.ts +2 -2
- package/src/translations/es-ES.objects.generated.ts +2 -2
- package/src/translations/ja-JP.objects.generated.ts +2 -2
- package/src/translations/zh-CN.objects.generated.ts +2 -2
- package/dist/chunk-I63ISOGO.cjs.map +0 -1
- package/dist/chunk-YQG6SUP3.js.map +0 -1
- package/dist/translations-AV47AVPV.js.map +0 -1
- package/dist/translations-OAKKANSP.cjs.map +0 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-webhooks@
|
|
2
|
+
> @objectstack/plugin-webhooks@16.0.0-rc.0 build /home/runner/work/framework/framework/packages/plugins/plugin-webhooks
|
|
3
3
|
> tsup
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts, src/schema.ts
|
|
@@ -10,27 +10,27 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
+
[32mESM[39m [1mdist/index.js [22m[32m13.97 KB[39m
|
|
13
14
|
[32mESM[39m [1mdist/schema.js [22m[32m111.00 B[39m
|
|
14
|
-
[32mESM[39m [1mdist/chunk-
|
|
15
|
-
[32mESM[39m [1mdist/translations-
|
|
16
|
-
[32mESM[39m [1mdist/index.js
|
|
15
|
+
[32mESM[39m [1mdist/chunk-RBCYKJVL.js [22m[32m5.61 KB[39m
|
|
16
|
+
[32mESM[39m [1mdist/translations-EPJYANXJ.js [22m[32m19.34 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m35.28 KB[39m
|
|
17
18
|
[32mESM[39m [1mdist/schema.js.map [22m[32m71.00 B[39m
|
|
18
|
-
[32mESM[39m [1mdist/chunk-
|
|
19
|
-
[32mESM[39m [1mdist/translations-
|
|
20
|
-
[32mESM[39m
|
|
21
|
-
[
|
|
22
|
-
[32mCJS[39m [1mdist/index.cjs [22m[32m17.15 KB[39m
|
|
19
|
+
[32mESM[39m [1mdist/chunk-RBCYKJVL.js.map [22m[32m10.21 KB[39m
|
|
20
|
+
[32mESM[39m [1mdist/translations-EPJYANXJ.js.map [22m[32m30.50 KB[39m
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 741ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m17.76 KB[39m
|
|
23
23
|
[32mCJS[39m [1mdist/schema.cjs [22m[32m220.00 B[39m
|
|
24
|
-
[32mCJS[39m [1mdist/chunk-
|
|
25
|
-
[32mCJS[39m [1mdist/translations-
|
|
26
|
-
[32mCJS[39m [1mdist/index.cjs.map [22m[
|
|
24
|
+
[32mCJS[39m [1mdist/chunk-PTMJ5BLL.cjs [22m[32m5.76 KB[39m
|
|
25
|
+
[32mCJS[39m [1mdist/translations-L2JAQDJB.cjs [22m[32m19.43 KB[39m
|
|
26
|
+
[32mCJS[39m [1mdist/index.cjs.map [22m[32m32.52 KB[39m
|
|
27
27
|
[32mCJS[39m [1mdist/schema.cjs.map [22m[32m278.00 B[39m
|
|
28
|
-
[32mCJS[39m [1mdist/chunk-
|
|
29
|
-
[32mCJS[39m [1mdist/translations-
|
|
30
|
-
[32mCJS[39m ⚡️ Build success in
|
|
28
|
+
[32mCJS[39m [1mdist/chunk-PTMJ5BLL.cjs.map [22m[32m12.08 KB[39m
|
|
29
|
+
[32mCJS[39m [1mdist/translations-L2JAQDJB.cjs.map [22m[32m34.61 KB[39m
|
|
30
|
+
[32mCJS[39m ⚡️ Build success in 752ms
|
|
31
31
|
[34mDTS[39m Build start
|
|
32
|
-
[32mDTS[39m ⚡️ Build success in
|
|
33
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[32m6.
|
|
34
|
-
[32mDTS[39m [1mdist/schema.d.ts [22m[
|
|
35
|
-
[32mDTS[39m [1mdist/index.d.cts [22m[32m6.
|
|
36
|
-
[32mDTS[39m [1mdist/schema.d.cts [22m[
|
|
32
|
+
[32mDTS[39m ⚡️ Build success in 24563ms
|
|
33
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m6.96 KB[39m
|
|
34
|
+
[32mDTS[39m [1mdist/schema.d.ts [22m[32m107.18 KB[39m
|
|
35
|
+
[32mDTS[39m [1mdist/index.d.cts [22m[32m6.96 KB[39m
|
|
36
|
+
[32mDTS[39m [1mdist/schema.d.cts [22m[32m107.18 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @objectstack/plugin-webhooks
|
|
2
2
|
|
|
3
|
+
## 16.0.0-rc.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4b6fde8: Trim the dead `undelete` and `api` webhook triggers (#3196). `WebhookTriggerType` declared five triggers but only three ever fired:
|
|
8
|
+
|
|
9
|
+
- `undelete` had no event source — the engine has no soft-delete/restore capability (`delete` is a hard delete; no `deleted_at` convention, no restore operation, and `data.record.undeleted` is never emitted). The `undeleted` case in the auto-enqueuer's action mapper was dead code awaiting a producer that doesn't exist.
|
|
10
|
+
- `api` ("manually triggered") had no fire path — the only webhook HTTP surface re-queues already-failed deliveries; nothing originates a manual fire.
|
|
11
|
+
|
|
12
|
+
Both are removed from the enum (contract-first, matching #3184/#3195): authoring a webhook on a removed trigger now fails loudly at `os validate` / registration instead of registering a webhook that silently never fires. No shipped webhook metadata used either. The auto-enqueuer now also warns when a persisted `sys_webhook` row carries a trigger it can't map to an emitted record event (a drift-guard, so a dead trigger can't silently no-op again). Reintroduce `undelete` only alongside a real restore subsystem, and `api` only alongside a real manual-fire endpoint. Updated the `sys_webhook` trigger options, field help (all locales), docs, and reference; added rejection tests.
|
|
13
|
+
|
|
14
|
+
- Updated dependencies [f972574]
|
|
15
|
+
- Updated dependencies [22013aa]
|
|
16
|
+
- Updated dependencies [3ad3dd5]
|
|
17
|
+
- Updated dependencies [3a18b60]
|
|
18
|
+
- Updated dependencies [a8aa34c]
|
|
19
|
+
- Updated dependencies [e057f42]
|
|
20
|
+
- Updated dependencies [a3823b2]
|
|
21
|
+
- Updated dependencies [43a3efb]
|
|
22
|
+
- Updated dependencies [524696a]
|
|
23
|
+
- Updated dependencies [5e3301d]
|
|
24
|
+
- Updated dependencies [dd9f223]
|
|
25
|
+
- Updated dependencies [46e876c]
|
|
26
|
+
- Updated dependencies [5f05de2]
|
|
27
|
+
- Updated dependencies [021ba4c]
|
|
28
|
+
- Updated dependencies [158aa14]
|
|
29
|
+
- Updated dependencies [d2723e2]
|
|
30
|
+
- Updated dependencies [fefcd54]
|
|
31
|
+
- Updated dependencies [beaf2de]
|
|
32
|
+
- Updated dependencies [369eb6e]
|
|
33
|
+
- Updated dependencies [b659111]
|
|
34
|
+
- Updated dependencies [5754a23]
|
|
35
|
+
- Updated dependencies [6c270a6]
|
|
36
|
+
- Updated dependencies [290e2f0]
|
|
37
|
+
- Updated dependencies [668dd17]
|
|
38
|
+
- Updated dependencies [8abf133]
|
|
39
|
+
- Updated dependencies [e0859b1]
|
|
40
|
+
- Updated dependencies [04ecd4e]
|
|
41
|
+
- Updated dependencies [4d5a892]
|
|
42
|
+
- Updated dependencies [16cebeb]
|
|
43
|
+
- Updated dependencies [86d30af]
|
|
44
|
+
- Updated dependencies [8923843]
|
|
45
|
+
- Updated dependencies [a2795f6]
|
|
46
|
+
- Updated dependencies [f16b492]
|
|
47
|
+
- Updated dependencies [4b6fde8]
|
|
48
|
+
- Updated dependencies [2018df9]
|
|
49
|
+
- Updated dependencies [fc5a3a2]
|
|
50
|
+
- @objectstack/spec@16.0.0-rc.0
|
|
51
|
+
- @objectstack/service-messaging@16.0.0-rc.0
|
|
52
|
+
- @objectstack/core@16.0.0-rc.0
|
|
53
|
+
|
|
3
54
|
## 15.1.1
|
|
4
55
|
|
|
5
56
|
### Patch Changes
|
|
@@ -82,11 +82,11 @@ var SysWebhook = _data.ObjectSchema.create({
|
|
|
82
82
|
// Object picker (same widget as sys_sharing_rule) instead of a free-text
|
|
83
83
|
// machine name. Falls back to a text input when the widget is unavailable.
|
|
84
84
|
widget: "object-ref",
|
|
85
|
-
description: "Short object name whose events fire this webhook
|
|
85
|
+
description: "Short object name whose record events (create/update/delete) fire this webhook",
|
|
86
86
|
group: "Definition"
|
|
87
87
|
}),
|
|
88
88
|
triggers: _data.Field.select(
|
|
89
|
-
["create", "update", "delete"
|
|
89
|
+
["create", "update", "delete"],
|
|
90
90
|
{
|
|
91
91
|
label: "Triggers",
|
|
92
92
|
required: false,
|
|
@@ -94,7 +94,7 @@ 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
|
+
description: "Record events that fire this webhook",
|
|
98
98
|
group: "Definition"
|
|
99
99
|
}
|
|
100
100
|
),
|
|
@@ -152,4 +152,4 @@ var SysWebhook = _data.ObjectSchema.create({
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
exports.SysWebhook = SysWebhook;
|
|
155
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-PTMJ5BLL.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/chunk-PTMJ5BLL.cjs","../src/sys-webhook.object.ts"],"names":[],"mappings":"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","file":"/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/chunk-PTMJ5BLL.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 * One row per `name`. The automation runtime\n * (`@objectstack/service-automation`, built-in `http_request` node) loads\n * active rows on boot + on `sys_webhook:changed` events, registers\n * `afterInsert` / `afterUpdate` / `afterDelete` listeners for the\n * targeted object, and dispatches outbound HTTP calls when matching\n * record events fire.\n *\n * Ownership (ADR-0029 K2.a): this object is **owned by\n * `@objectstack/plugin-webhooks`** — the plugin that consumes these rows —\n * alongside its sibling `sys_webhook_delivery`. It used to live in the\n * `@objectstack/platform-objects` monolith and be imported here; the\n * definition now lives with its owner so the plugin ships both data and\n * 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. Authored via defineWebhook() in code or the Studio editor; executed by the HTTP connector plugin.',\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 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"]}
|
|
@@ -82,11 +82,11 @@ var SysWebhook = ObjectSchema.create({
|
|
|
82
82
|
// Object picker (same widget as sys_sharing_rule) instead of a free-text
|
|
83
83
|
// machine name. Falls back to a text input when the widget is unavailable.
|
|
84
84
|
widget: "object-ref",
|
|
85
|
-
description: "Short object name whose events fire this webhook
|
|
85
|
+
description: "Short object name whose record events (create/update/delete) fire this webhook",
|
|
86
86
|
group: "Definition"
|
|
87
87
|
}),
|
|
88
88
|
triggers: Field.select(
|
|
89
|
-
["create", "update", "delete"
|
|
89
|
+
["create", "update", "delete"],
|
|
90
90
|
{
|
|
91
91
|
label: "Triggers",
|
|
92
92
|
required: false,
|
|
@@ -94,7 +94,7 @@ var SysWebhook = 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
|
+
description: "Record events that fire this webhook",
|
|
98
98
|
group: "Definition"
|
|
99
99
|
}
|
|
100
100
|
),
|
|
@@ -152,4 +152,4 @@ var SysWebhook = ObjectSchema.create({
|
|
|
152
152
|
export {
|
|
153
153
|
SysWebhook
|
|
154
154
|
};
|
|
155
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-RBCYKJVL.js.map
|
|
@@ -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 * One row per `name`. The automation runtime\n * (`@objectstack/service-automation`, built-in `http_request` node) loads\n * active rows on boot + on `sys_webhook:changed` events, registers\n * `afterInsert` / `afterUpdate` / `afterDelete` listeners for the\n * targeted object, and dispatches outbound HTTP calls when matching\n * record events fire.\n *\n * Ownership (ADR-0029 K2.a): this object is **owned by\n * `@objectstack/plugin-webhooks`** — the plugin that consumes these rows —\n * alongside its sibling `sys_webhook_delivery`. It used to live in the\n * `@objectstack/platform-objects` monolith and be imported here; the\n * definition now lives with its owner so the plugin ships both data and\n * 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. Authored via defineWebhook() in code or the Studio editor; executed by the HTTP connector plugin.',\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 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":[]}
|
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
|
|
3
|
+
var _chunkPTMJ5BLLcjs = require('./chunk-PTMJ5BLL.cjs');
|
|
4
4
|
|
|
5
5
|
// src/auto-enqueuer.ts
|
|
6
6
|
var AutoEnqueuer = class {
|
|
@@ -108,8 +108,16 @@ var AutoEnqueuer = class {
|
|
|
108
108
|
triggerList = s.split(",");
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
|
+
const normalized = triggerList.map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
112
|
+
const unknown = normalized.filter((t) => !DISPATCHABLE_WEBHOOK_TRIGGERS.has(t));
|
|
113
|
+
if (unknown.length > 0) {
|
|
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.`,
|
|
116
|
+
{ id: row.id, unknown }
|
|
117
|
+
)]);
|
|
118
|
+
}
|
|
111
119
|
const triggers = new Set(
|
|
112
|
-
|
|
120
|
+
normalized.filter((t) => DISPATCHABLE_WEBHOOK_TRIGGERS.has(t))
|
|
113
121
|
);
|
|
114
122
|
if (triggers.size === 0) {
|
|
115
123
|
return null;
|
|
@@ -146,7 +154,7 @@ var AutoEnqueuer = class {
|
|
|
146
154
|
* webhook persistence.
|
|
147
155
|
*/
|
|
148
156
|
handleEvent(event) {
|
|
149
|
-
if (!_optionalChain([event, 'access',
|
|
157
|
+
if (!_optionalChain([event, 'access', _18 => _18.type, 'optionalAccess', _19 => _19.startsWith, 'call', _20 => _20("data.record.")])) return;
|
|
150
158
|
if (!event.object) return;
|
|
151
159
|
if (event.object === this.subscriptionsObject) return;
|
|
152
160
|
const action = event.type.slice("data.record.".length);
|
|
@@ -158,7 +166,7 @@ var AutoEnqueuer = class {
|
|
|
158
166
|
];
|
|
159
167
|
if (subs.length === 0) return;
|
|
160
168
|
const payload = _nullishCoalesce(event.payload, () => ( {}));
|
|
161
|
-
const recordId = _nullishCoalesce(_nullishCoalesce(_nullishCoalesce(_nullishCoalesce(payload.recordId, () => ( payload.id)), () => ( _optionalChain([payload, 'access',
|
|
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"));
|
|
162
170
|
const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;
|
|
163
171
|
for (const sub of subs) {
|
|
164
172
|
if (!sub.triggers.has(trigger)) continue;
|
|
@@ -180,19 +188,19 @@ var AutoEnqueuer = class {
|
|
|
180
188
|
...payload
|
|
181
189
|
}
|
|
182
190
|
}).catch(
|
|
183
|
-
(err) => _optionalChain([this, 'access',
|
|
191
|
+
(err) => _optionalChain([this, 'access', _25 => _25.logger, 'access', _26 => _26.warn, 'optionalCall', _27 => _27("[webhook-auto-enqueuer] enqueue failed", {
|
|
184
192
|
webhook: sub.name,
|
|
185
193
|
eventId,
|
|
186
|
-
err: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
194
|
+
err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _28 => _28.message]), () => ( err))
|
|
187
195
|
})])
|
|
188
196
|
);
|
|
189
197
|
}
|
|
190
198
|
}
|
|
191
199
|
handleSelfHealEvent(event) {
|
|
192
200
|
if (event.object !== this.subscriptionsObject) return;
|
|
193
|
-
if (!_optionalChain([event, 'access',
|
|
201
|
+
if (!_optionalChain([event, 'access', _29 => _29.type, 'optionalAccess', _30 => _30.startsWith, 'call', _31 => _31("data.record.")])) return;
|
|
194
202
|
this.refresh().catch(
|
|
195
|
-
(err) => _optionalChain([this, 'access',
|
|
203
|
+
(err) => _optionalChain([this, 'access', _32 => _32.logger, 'access', _33 => _33.warn, 'optionalCall', _34 => _34("[webhook-auto-enqueuer] self-heal refresh failed", err)])
|
|
196
204
|
);
|
|
197
205
|
}
|
|
198
206
|
/** Test / admin accessor. */
|
|
@@ -208,12 +216,11 @@ function mapActionToTrigger(action) {
|
|
|
208
216
|
return "update";
|
|
209
217
|
case "deleted":
|
|
210
218
|
return "delete";
|
|
211
|
-
case "undeleted":
|
|
212
|
-
return "undelete";
|
|
213
219
|
default:
|
|
214
220
|
return null;
|
|
215
221
|
}
|
|
216
222
|
}
|
|
223
|
+
var DISPATCHABLE_WEBHOOK_TRIGGERS = /* @__PURE__ */ new Set(["create", "update", "delete"]);
|
|
217
224
|
|
|
218
225
|
// src/webhook-outbox-plugin.ts
|
|
219
226
|
var WebhookOutboxPlugin = class {
|
|
@@ -235,7 +242,7 @@ var WebhookOutboxPlugin = class {
|
|
|
235
242
|
scope: "system",
|
|
236
243
|
name: "Webhook Schemas",
|
|
237
244
|
description: "Registers sys_webhook (configuration). Deliveries use messaging's sys_http_delivery outbox.",
|
|
238
|
-
objects: [
|
|
245
|
+
objects: [_chunkPTMJ5BLLcjs.SysWebhook],
|
|
239
246
|
navigationContributions: [
|
|
240
247
|
{
|
|
241
248
|
app: "setup",
|
|
@@ -249,7 +256,7 @@ var WebhookOutboxPlugin = class {
|
|
|
249
256
|
]
|
|
250
257
|
});
|
|
251
258
|
} else {
|
|
252
|
-
_optionalChain([ctx, 'access',
|
|
259
|
+
_optionalChain([ctx, 'access', _35 => _35.logger, 'access', _36 => _36.warn, 'optionalCall', _37 => _37(
|
|
253
260
|
"[webhook-outbox] manifest service unavailable \u2014 sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin."
|
|
254
261
|
)]);
|
|
255
262
|
}
|
|
@@ -258,7 +265,7 @@ var WebhookOutboxPlugin = class {
|
|
|
258
265
|
try {
|
|
259
266
|
const i18n = ctx.getService("i18n");
|
|
260
267
|
if (i18n && typeof i18n.loadTranslations === "function") {
|
|
261
|
-
const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-
|
|
268
|
+
const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-L2JAQDJB.cjs")));
|
|
262
269
|
for (const [locale, data] of Object.entries(WebhooksTranslations)) {
|
|
263
270
|
i18n.loadTranslations(locale, data);
|
|
264
271
|
}
|
|
@@ -274,12 +281,12 @@ var WebhookOutboxPlugin = class {
|
|
|
274
281
|
this.registerAdminRoutes(ctx);
|
|
275
282
|
});
|
|
276
283
|
}
|
|
277
|
-
_optionalChain([ctx, 'access',
|
|
284
|
+
_optionalChain([ctx, 'access', _38 => _38.logger, 'access', _39 => _39.info, 'optionalCall', _40 => _40("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
|
|
278
285
|
autoEnqueue: autoEnqueueOpt !== false
|
|
279
286
|
})]);
|
|
280
287
|
}
|
|
281
288
|
async dispose() {
|
|
282
|
-
await _optionalChain([this, 'access',
|
|
289
|
+
await _optionalChain([this, 'access', _41 => _41.autoEnqueuer, 'optionalAccess', _42 => _42.stop, 'call', _43 => _43()]);
|
|
283
290
|
}
|
|
284
291
|
getMessaging(ctx) {
|
|
285
292
|
const svc = this.tryGetService(ctx, ["messaging"]);
|
|
@@ -291,14 +298,14 @@ var WebhookOutboxPlugin = class {
|
|
|
291
298
|
const realtime = this.tryGetService(ctx, ["realtime"]);
|
|
292
299
|
const messaging = this.getMessaging(ctx);
|
|
293
300
|
if (!engine || !realtime || !messaging) {
|
|
294
|
-
_optionalChain([ctx, 'access',
|
|
301
|
+
_optionalChain([ctx, 'access', _44 => _44.logger, 'access', _45 => _45.warn, 'optionalCall', _46 => _46(
|
|
295
302
|
"[webhook-auto-enqueuer] disabled \u2014 ObjectQL, Realtime, or Messaging service not available",
|
|
296
303
|
{ hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging }
|
|
297
304
|
)]);
|
|
298
305
|
return;
|
|
299
306
|
}
|
|
300
307
|
if (!messaging.isHttpDeliveryReady()) {
|
|
301
|
-
_optionalChain([ctx, 'access',
|
|
308
|
+
_optionalChain([ctx, 'access', _47 => _47.logger, 'access', _48 => _48.warn, 'optionalCall', _49 => _49(
|
|
302
309
|
"[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) \u2014 webhook deliveries will not be durable"
|
|
303
310
|
)]);
|
|
304
311
|
}
|
|
@@ -311,7 +318,7 @@ var WebhookOutboxPlugin = class {
|
|
|
311
318
|
);
|
|
312
319
|
await this.autoEnqueuer.start();
|
|
313
320
|
ctx.registerService("webhook.autoEnqueuer", this.autoEnqueuer);
|
|
314
|
-
_optionalChain([ctx, 'access',
|
|
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)")]);
|
|
315
322
|
}
|
|
316
323
|
tryGetService(ctx, names) {
|
|
317
324
|
for (const n of names) {
|
|
@@ -331,7 +338,7 @@ var WebhookOutboxPlugin = class {
|
|
|
331
338
|
registerAdminRoutes(ctx) {
|
|
332
339
|
const http = this.tryGetService(ctx, ["http-server"]);
|
|
333
340
|
if (!http || typeof http.getRawApp !== "function") {
|
|
334
|
-
_optionalChain([ctx, 'access',
|
|
341
|
+
_optionalChain([ctx, 'access', _53 => _53.logger, 'access', _54 => _54.debug, 'optionalCall', _55 => _55("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
|
|
335
342
|
return;
|
|
336
343
|
}
|
|
337
344
|
const rawApp = http.getRawApp();
|
|
@@ -351,7 +358,7 @@ var WebhookOutboxPlugin = class {
|
|
|
351
358
|
} catch (e5) {
|
|
352
359
|
return c.json({ success: false, error: "invalid_body", message: "Request body must be JSON." }, 400);
|
|
353
360
|
}
|
|
354
|
-
const deliveryId = typeof _optionalChain([body, 'optionalAccess',
|
|
361
|
+
const deliveryId = typeof _optionalChain([body, 'optionalAccess', _56 => _56.deliveryId]) === "string" ? body.deliveryId.trim() : "";
|
|
355
362
|
if (!deliveryId) {
|
|
356
363
|
return c.json(
|
|
357
364
|
{ success: false, error: "missing_delivery_id", message: "Body must include `deliveryId: string`." },
|
|
@@ -360,24 +367,24 @@ var WebhookOutboxPlugin = class {
|
|
|
360
367
|
}
|
|
361
368
|
try {
|
|
362
369
|
const row = await messaging.redeliverHttp(deliveryId);
|
|
363
|
-
_optionalChain([ctx, 'access',
|
|
370
|
+
_optionalChain([ctx, 'access', _57 => _57.logger, 'access', _58 => _58.info, 'optionalCall', _59 => _59("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId })]);
|
|
364
371
|
return c.json({ success: true, data: { id: row.id, status: row.status } });
|
|
365
372
|
} catch (err) {
|
|
366
|
-
const code = _optionalChain([err, 'optionalAccess',
|
|
373
|
+
const code = _optionalChain([err, 'optionalAccess', _60 => _60.code]);
|
|
367
374
|
if (code === "not_found") {
|
|
368
375
|
return c.json({ success: false, error: "not_found", message: err.message }, 404);
|
|
369
376
|
}
|
|
370
377
|
if (code === "not_eligible") {
|
|
371
378
|
return c.json({ success: false, error: "not_eligible", message: err.message }, 409);
|
|
372
379
|
}
|
|
373
|
-
_optionalChain([ctx, 'access',
|
|
380
|
+
_optionalChain([ctx, 'access', _61 => _61.logger, 'access', _62 => _62.error, 'optionalCall', _63 => _63("[webhook-outbox] redeliver failed", err)]);
|
|
374
381
|
return c.json(
|
|
375
|
-
{ success: false, error: "internal_error", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
382
|
+
{ success: false, error: "internal_error", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _64 => _64.message]), () => ( String(err))) },
|
|
376
383
|
500
|
|
377
384
|
);
|
|
378
385
|
}
|
|
379
386
|
});
|
|
380
|
-
_optionalChain([ctx, 'access',
|
|
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")]);
|
|
381
388
|
}
|
|
382
389
|
async resolveSessionUserId(ctx, c) {
|
|
383
390
|
try {
|
|
@@ -387,9 +394,9 @@ var WebhookOutboxPlugin = class {
|
|
|
387
394
|
if (!api && typeof authService.getApi === "function") {
|
|
388
395
|
api = await authService.getApi();
|
|
389
396
|
}
|
|
390
|
-
if (!_optionalChain([api, 'optionalAccess',
|
|
397
|
+
if (!_optionalChain([api, 'optionalAccess', _68 => _68.getSession])) return void 0;
|
|
391
398
|
const session = await api.getSession({ headers: c.req.raw.headers });
|
|
392
|
-
const uid = _optionalChain([session, 'optionalAccess',
|
|
399
|
+
const uid = _optionalChain([session, 'optionalAccess', _69 => _69.user, 'optionalAccess', _70 => _70.id]);
|
|
393
400
|
return typeof uid === "string" && uid.length > 0 ? uid : void 0;
|
|
394
401
|
} catch (e6) {
|
|
395
402
|
return void 0;
|
|
@@ -400,5 +407,5 @@ var WebhookOutboxPlugin = class {
|
|
|
400
407
|
|
|
401
408
|
|
|
402
409
|
|
|
403
|
-
exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook =
|
|
410
|
+
exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkPTMJ5BLLcjs.SysWebhook; exports.WebhookOutboxPlugin = WebhookOutboxPlugin;
|
|
404
411
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/index.cjs","../src/auto-enqueuer.ts","../src/webhook-outbox-plugin.ts"],"names":[],"mappings":"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","file":"/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/index.cjs","sourcesContent":[null,"// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IDataEngine, IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\n\n/**\n * Enqueue callback into the shared `service-messaging` HTTP outbox (ADR-0018 M3).\n * The plugin supplies one bound to `messaging.enqueueHttp(...)`; webhooks no\n * longer own a delivery outbox/dispatcher — they share the generic substrate.\n */\nexport type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;\n\n/**\n * Optional logger interface (subset of console / kernel logger).\n */\ninterface OptionalLogger {\n info?(msg: string, meta?: unknown): void;\n warn?(msg: string, meta?: unknown): void;\n debug?(msg: string, meta?: unknown): void;\n error?(msg: string, err?: unknown, meta?: unknown): void;\n}\n\n/**\n * Per-row subscription cached in memory. Mirrors a subset of the\n * `sys_webhook` object — only what the auto-enqueuer needs to match an\n * event and build an `EnqueueInput`.\n */\ninterface CachedSubscription {\n id: string;\n name: string;\n objectName: string | undefined; // empty = matches all objects (manual-only is filtered out earlier)\n triggers: Set<'create' | 'update' | 'delete' | 'undelete'>;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n secret?: string;\n timeoutMs?: number;\n}\n\nexport interface AutoEnqueuerOptions {\n /**\n * Object name holding webhook subscriptions. Defaults to `sys_webhook`,\n * the platform-objects schema authored in apps.\n */\n subscriptionsObject?: string;\n\n /**\n * Periodic full-cache refresh interval (ms). Belt-and-braces in case\n * the subscription-change event is missed. Default 60s.\n */\n refreshIntervalMs?: number;\n\n logger?: OptionalLogger;\n}\n\n/**\n * Bridge between `IRealtimeService` (`data.record.*` events emitted by\n * the engine) and `IWebhookOutbox` (durable delivery rows the dispatcher\n * picks up).\n *\n * ## Why a separate class\n * Keeps `WebhookOutboxPlugin` lean: the plugin wires services, this\n * class owns the runtime fan-out logic + subscription cache.\n *\n * ## Hot path\n * Every `engine.insert/update/delete` fires a `data.record.*` event.\n * The handler:\n * 1. Looks up matching subscriptions in an in-memory `Map<object, sub[]>`\n * — O(1) per event, no DB hit on the write path.\n * 2. Calls `outbox.enqueue()` fire-and-forget for each match. The\n * enqueue itself is a single INSERT, which runs *after* the user's\n * request has already returned.\n *\n * Net cost on the write path: one synchronous Map lookup (~microseconds).\n *\n * ## Cache freshness\n * The cache is rebuilt:\n * 1. Once on `start()`.\n * 2. On every `data.record.{created,updated,deleted}` event whose\n * object is `sys_webhook` (self-healing — when a user toggles a\n * webhook, the handler refreshes the cache before returning).\n * 3. Periodically (default 60s) as belt-and-braces.\n *\n * For multi-node clusters this is *eventually consistent* — node B may\n * not see node A's edit for up to one cycle. That's acceptable for\n * webhook configuration changes (humans don't expect millisecond\n * propagation) and matches Hasura's behaviour.\n *\n * ## Determinism\n * `eventId` is computed from `${object}:${recordId}:${type}:${timestamp}`\n * so the outbox dedup index catches duplicates that could arise from\n * upstream replay or buggy producers — and is stable across nodes.\n */\nexport class AutoEnqueuer {\n private readonly subscriptions = new Map<string, CachedSubscription[]>();\n private readonly subscriptionsObject: string;\n private readonly refreshIntervalMs: number;\n private readonly logger: OptionalLogger;\n private subId: string | undefined;\n private subIdSelfHeal: string | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n private running = false;\n private refreshing: Promise<void> | undefined;\n\n constructor(\n private readonly engine: IDataEngine,\n private readonly realtime: IRealtimeService,\n private readonly enqueue: HttpEnqueueFn,\n opts: AutoEnqueuerOptions = {},\n ) {\n this.subscriptionsObject = opts.subscriptionsObject ?? 'sys_webhook';\n this.refreshIntervalMs = opts.refreshIntervalMs ?? 60_000;\n this.logger = opts.logger ?? {};\n }\n\n /**\n * Load the subscription cache and start listening for events.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.refresh();\n\n // Main subscription: every data event → match → enqueue.\n this.subId = await this.realtime.subscribe(\n 'webhook-auto-enqueuer',\n (event) => this.handleEvent(event),\n );\n\n // Self-healing: any change to sys_webhook refreshes the cache.\n this.subIdSelfHeal = await this.realtime.subscribe(\n 'webhook-auto-enqueuer-self-heal',\n (event) => this.handleSelfHealEvent(event),\n { object: this.subscriptionsObject },\n );\n\n if (this.refreshIntervalMs > 0) {\n this.refreshTimer = setInterval(() => {\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] periodic refresh failed', err),\n );\n }, this.refreshIntervalMs);\n // Don't keep the process alive solely for this timer.\n this.refreshTimer.unref?.();\n }\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.subId) await this.realtime.unsubscribe(this.subId);\n if (this.subIdSelfHeal) await this.realtime.unsubscribe(this.subIdSelfHeal);\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n this.subId = undefined;\n this.subIdSelfHeal = undefined;\n this.refreshTimer = undefined;\n }\n\n /**\n * Force-refresh the subscription cache from storage. Concurrent\n * callers share a single in-flight refresh.\n */\n async refresh(): Promise<void> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = this.doRefresh().finally(() => {\n this.refreshing = undefined;\n });\n return this.refreshing;\n }\n\n private async doRefresh(): Promise<void> {\n let rows: any[];\n try {\n rows = await this.engine.find(this.subscriptionsObject, {\n where: { active: true },\n });\n } catch (err) {\n this.logger.warn?.(\n `[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,\n err,\n );\n return;\n }\n\n const next = new Map<string, CachedSubscription[]>();\n for (const row of rows) {\n const sub = this.parseRow(row);\n if (!sub) continue;\n // Empty objectName == \"any object\" → indexed under '*'.\n const key = sub.objectName ?? '*';\n const arr = next.get(key) ?? [];\n arr.push(sub);\n next.set(key, arr);\n }\n\n this.subscriptions.clear();\n for (const [k, v] of next) this.subscriptions.set(k, v);\n\n this.logger.debug?.('[webhook-auto-enqueuer] cache refreshed', {\n objects: this.subscriptions.size,\n rows: rows.length,\n });\n }\n\n private parseRow(row: any): CachedSubscription | null {\n if (!row?.id || !row?.url) return null;\n // `triggers` is now authored as a multi-select (stored as an array), but\n // legacy rows stored a comma-separated string (and some drivers hand a\n // JSON-encoded array back as a string). Accept all three shapes so a\n // schema change never silently drops a subscription's events.\n const rawTriggers = row.triggers;\n let triggerList: string[];\n if (Array.isArray(rawTriggers)) {\n triggerList = rawTriggers.map((t) => String(t));\n } else {\n const s = String(rawTriggers ?? '').trim();\n if (s.startsWith('[')) {\n try {\n const parsed = JSON.parse(s);\n triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];\n } catch {\n triggerList = s.split(',');\n }\n } else {\n triggerList = s.split(',');\n }\n }\n const triggers = new Set(\n triggerList\n .map((t) => t.trim().toLowerCase())\n .filter(Boolean) as Array<'create' | 'update' | 'delete' | 'undelete'>,\n );\n if (triggers.size === 0) {\n // Manual-only webhook (no triggers) — skip auto-enqueue.\n return null;\n }\n\n // The \"definition_json\" field carries advanced config (headers,\n // secret, timeout); attempt a best-effort parse. Fall back to\n // top-level fields where present.\n let defn: Record<string, any> = {};\n if (typeof row.definition_json === 'string' && row.definition_json.length > 0) {\n try {\n defn = JSON.parse(row.definition_json) ?? {};\n } catch {\n defn = {};\n }\n }\n\n return {\n id: row.id as string,\n name: (row.name as string) ?? row.id,\n objectName: row.object_name ? String(row.object_name) : undefined,\n triggers,\n url: String(row.url),\n // Method is authored via a select whose option values are lowercased\n // (get/post/…); upper-case here so delivery uses a canonical HTTP\n // method regardless of whether the row was authored before or after\n // the select change (legacy rows stored 'POST').\n method: String(row.method ?? defn.method ?? 'POST').toUpperCase(),\n headers: defn.headers,\n secret: defn.secret,\n timeoutMs: defn.timeoutMs,\n };\n }\n\n /**\n * Handler for the firehose subscription.\n *\n * NOTE: we intentionally `void` the inner enqueue() so the realtime\n * publisher (and therefore the user's request) is never blocked on\n * webhook persistence.\n */\n private handleEvent(event: RealtimeEventPayload): void {\n if (!event.type?.startsWith('data.record.')) return;\n if (!event.object) return;\n if (event.object === this.subscriptionsObject) return; // self-heal handles its own\n\n const action = event.type.slice('data.record.'.length) as\n | 'created' | 'updated' | 'deleted' | 'undeleted' | string;\n const trigger = mapActionToTrigger(action);\n if (!trigger) return;\n\n const subs = [\n ...(this.subscriptions.get(event.object) ?? []),\n ...(this.subscriptions.get('*') ?? []),\n ];\n if (subs.length === 0) return;\n\n const payload = event.payload ?? {};\n const recordId =\n (payload as any).recordId ??\n (payload as any).id ??\n (payload as any).after?.id ??\n (payload as any).before?.id ??\n 'unknown';\n\n // Deterministic eventId — same input on any node → same id.\n // Includes timestamp so two distinct updates to the same record\n // don't accidentally dedup.\n const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;\n\n for (const sub of subs) {\n if (!sub.triggers.has(trigger)) continue;\n\n // Fire-and-forget — never await on the hot path. Map the webhook\n // delivery onto the generic HTTP-outbox shape (ADR-0018 M3):\n // - source 'webhook' + dedupKey '<webhookId>:<eventId>' preserves\n // the old (event_id, webhook_id) at-most-once enqueue;\n // - refId = webhookId keeps per-webhook partition affinity / ordering;\n // - label = event type → X-Objectstack-Event header.\n void this.enqueue({\n source: 'webhook',\n refId: sub.id,\n dedupKey: `${sub.id}:${eventId}`,\n label: event.type,\n url: sub.url,\n method: sub.method,\n headers: sub.headers,\n signingSecret: sub.secret,\n timeoutMs: sub.timeoutMs,\n payload: {\n object: event.object,\n recordId,\n action,\n timestamp: event.timestamp,\n ...payload,\n },\n }).catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] enqueue failed', {\n webhook: sub.name,\n eventId,\n err: (err as Error)?.message ?? err,\n }),\n );\n }\n }\n\n private handleSelfHealEvent(event: RealtimeEventPayload): void {\n if (event.object !== this.subscriptionsObject) return;\n if (!event.type?.startsWith('data.record.')) return;\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] self-heal refresh failed', err),\n );\n }\n\n /** Test / admin accessor. */\n snapshot(): ReadonlyMap<string, ReadonlyArray<CachedSubscription>> {\n return this.subscriptions;\n }\n}\n\nfunction mapActionToTrigger(\n action: string,\n): 'create' | 'update' | 'delete' | 'undelete' | null {\n switch (action) {\n case 'created':\n return 'create';\n case 'updated':\n return 'update';\n case 'deleted':\n return 'delete';\n case 'undeleted':\n return 'undelete';\n default:\n return null;\n }\n}\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\nimport { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';\nimport { SysWebhook } from './sys-webhook.object.js';\n\n/**\n * Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface\n * (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime\n * import on the service. Webhook deliveries are enqueued onto the shared\n * `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.\n */\ninterface MessagingHttpSurface {\n isHttpDeliveryReady(): boolean;\n enqueueHttp(input: EnqueueHttpInput): Promise<string>;\n redeliverHttp(id: string): Promise<{ id: string; status: string }>;\n}\n\nexport interface WebhookOutboxPluginOptions {\n /**\n * Auto-enqueue config. When enabled (default `true` if the realtime + data\n * engine services are available), the plugin subscribes to `data.record.*`\n * events and enqueues a delivery onto the shared messaging HTTP outbox for\n * every matching `sys_webhook` row.\n *\n * Set `false` to disable and enqueue webhooks imperatively elsewhere.\n */\n autoEnqueue?: boolean | AutoEnqueuerOptions;\n}\n\n/**\n * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate\n * (ADR-0018 M3).\n *\n * Webhooks are no longer their own delivery engine: the durable outbox, the\n * cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and\n * the retention sweep all live in `@objectstack/service-messaging`\n * (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the\n * webhook-specific concerns:\n * - the `sys_webhook` configuration object,\n * - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox\n * rows (`source: 'webhook'`), and\n * - the redeliver admin endpoint.\n *\n * End-to-end flow:\n *\n * engine.insert('contact', {...})\n * → engine publishes data.record.created via IRealtimeService\n * → AutoEnqueuer matches active sys_webhook rows in O(1)\n * → messaging.enqueueHttp() runs fire-and-forget (off the write path)\n * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)\n *\n * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),\n * which is a foundational, always-on capability.\n */\nexport class WebhookOutboxPlugin implements Plugin {\n name = 'com.objectstack.plugin-webhook-outbox';\n version = '2.0.0';\n type = 'standard' as const;\n dependencies = ['com.objectstack.service.messaging'];\n\n private autoEnqueuer: AutoEnqueuer | undefined;\n\n constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}\n\n async init(ctx: PluginContext): Promise<void> {\n // Register the webhook config object (ADR-0029 K2.a). The delivery\n // telemetry now lives in messaging's `sys_http_delivery`, so the nav's\n // \"Deliveries\" entry points there (filtered to source=webhook in views).\n const manifest = ctx.getService<{ register(m: any): void }>('manifest');\n if (manifest && typeof manifest.register === 'function') {\n manifest.register({\n id: 'com.objectstack.plugin-webhook-outbox.schema',\n namespace: 'sys',\n version: this.version,\n type: 'plugin',\n scope: 'system',\n name: 'Webhook Schemas',\n description: 'Registers sys_webhook (configuration). Deliveries use messaging\\'s sys_http_delivery outbox.',\n objects: [SysWebhook],\n navigationContributions: [\n {\n app: 'setup',\n group: 'group_integrations',\n priority: 100,\n items: [\n { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },\n { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },\n ],\n },\n ],\n });\n } else {\n ctx.logger.warn?.(\n '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',\n );\n }\n\n // ADR-0029 D8 — contribute object translations once i18n is up.\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n try {\n const i18n = ctx.getService<any>('i18n');\n if (i18n && typeof i18n.loadTranslations === 'function') {\n const { WebhooksTranslations } = await import('./translations/index.js');\n for (const [locale, data] of Object.entries(WebhooksTranslations)) {\n i18n.loadTranslations(locale, data as Record<string, unknown>);\n }\n }\n } catch { /* i18n optional */ }\n });\n }\n\n const autoEnqueueOpt = this.options.autoEnqueue ?? true;\n\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n await this.bootAutoEnqueue(ctx, autoEnqueueOpt);\n this.registerAdminRoutes(ctx);\n });\n }\n\n ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {\n autoEnqueue: autoEnqueueOpt !== false,\n });\n }\n\n async dispose(): Promise<void> {\n await this.autoEnqueuer?.stop();\n }\n\n private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {\n const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);\n return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;\n }\n\n private async bootAutoEnqueue(\n ctx: PluginContext,\n opt: boolean | AutoEnqueuerOptions,\n ): Promise<void> {\n if (opt === false) return;\n const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);\n const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);\n const messaging = this.getMessaging(ctx);\n if (!engine || !realtime || !messaging) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',\n { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },\n );\n return;\n }\n if (!messaging.isHttpDeliveryReady()) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',\n );\n }\n\n const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;\n this.autoEnqueuer = new AutoEnqueuer(\n engine,\n realtime,\n (input) => messaging.enqueueHttp(input),\n { ...enqOpts, logger: ctx.logger },\n );\n await this.autoEnqueuer.start();\n ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);\n ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');\n }\n\n private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {\n for (const n of names) {\n try {\n const svc = ctx.getService<T>(n);\n if (svc) return svc;\n } catch {\n // fall through\n }\n }\n return undefined;\n }\n\n /**\n * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is\n * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the\n * better-auth session cookie — every authenticated user counts.\n */\n private registerAdminRoutes(ctx: PluginContext): void {\n const http = this.tryGetService<any>(ctx, ['http-server']);\n if (!http || typeof http.getRawApp !== 'function') {\n ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');\n return;\n }\n const rawApp = http.getRawApp();\n const messaging = this.getMessaging(ctx);\n if (!rawApp || !messaging) return;\n\n rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {\n const userId = await this.resolveSessionUserId(ctx, c);\n if (!userId) {\n return c.json(\n { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },\n 401,\n );\n }\n let body: any;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);\n }\n const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';\n if (!deliveryId) {\n return c.json(\n { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },\n 400,\n );\n }\n try {\n const row = await messaging.redeliverHttp(deliveryId);\n ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });\n return c.json({ success: true, data: { id: row.id, status: row.status } });\n } catch (err: any) {\n const code = err?.code;\n if (code === 'not_found') {\n return c.json({ success: false, error: 'not_found', message: err.message }, 404);\n }\n if (code === 'not_eligible') {\n return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);\n }\n ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);\n return c.json(\n { success: false, error: 'internal_error', message: err?.message ?? String(err) },\n 500,\n );\n }\n });\n\n ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');\n }\n\n private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {\n try {\n const authService: any = this.tryGetService<any>(ctx, ['auth']);\n if (!authService) return undefined;\n let api: any = authService.api;\n if (!api && typeof authService.getApi === 'function') {\n api = await authService.getApi();\n }\n if (!api?.getSession) return undefined;\n const session = await api.getSession({ headers: c.req.raw.headers });\n const uid = session?.user?.id;\n return typeof uid === 'string' && uid.length > 0 ? uid : undefined;\n } catch {\n return undefined;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/index.cjs","../src/auto-enqueuer.ts","../src/webhook-outbox-plugin.ts"],"names":[],"mappings":"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","file":"/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/index.cjs","sourcesContent":[null,"// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IDataEngine, IRealtimeService, RealtimeEventPayload } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\n\n/**\n * Enqueue callback into the shared `service-messaging` HTTP outbox (ADR-0018 M3).\n * The plugin supplies one bound to `messaging.enqueueHttp(...)`; webhooks no\n * longer own a delivery outbox/dispatcher — they share the generic substrate.\n */\nexport type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;\n\n/**\n * Optional logger interface (subset of console / kernel logger).\n */\ninterface OptionalLogger {\n info?(msg: string, meta?: unknown): void;\n warn?(msg: string, meta?: unknown): void;\n debug?(msg: string, meta?: unknown): void;\n error?(msg: string, err?: unknown, meta?: unknown): void;\n}\n\n/**\n * Per-row subscription cached in memory. Mirrors a subset of the\n * `sys_webhook` object — only what the auto-enqueuer needs to match an\n * event and build an `EnqueueInput`.\n */\ninterface CachedSubscription {\n id: string;\n name: string;\n objectName: string | undefined; // empty = matches all objects\n triggers: Set<'create' | 'update' | 'delete'>;\n url: string;\n method?: string;\n headers?: Record<string, string>;\n secret?: string;\n timeoutMs?: number;\n}\n\nexport interface AutoEnqueuerOptions {\n /**\n * Object name holding webhook subscriptions. Defaults to `sys_webhook`,\n * the platform-objects schema authored in apps.\n */\n subscriptionsObject?: string;\n\n /**\n * Periodic full-cache refresh interval (ms). Belt-and-braces in case\n * the subscription-change event is missed. Default 60s.\n */\n refreshIntervalMs?: number;\n\n logger?: OptionalLogger;\n}\n\n/**\n * Bridge between `IRealtimeService` (`data.record.*` events emitted by\n * the engine) and `IWebhookOutbox` (durable delivery rows the dispatcher\n * picks up).\n *\n * ## Why a separate class\n * Keeps `WebhookOutboxPlugin` lean: the plugin wires services, this\n * class owns the runtime fan-out logic + subscription cache.\n *\n * ## Hot path\n * Every `engine.insert/update/delete` fires a `data.record.*` event.\n * The handler:\n * 1. Looks up matching subscriptions in an in-memory `Map<object, sub[]>`\n * — O(1) per event, no DB hit on the write path.\n * 2. Calls `outbox.enqueue()` fire-and-forget for each match. The\n * enqueue itself is a single INSERT, which runs *after* the user's\n * request has already returned.\n *\n * Net cost on the write path: one synchronous Map lookup (~microseconds).\n *\n * ## Cache freshness\n * The cache is rebuilt:\n * 1. Once on `start()`.\n * 2. On every `data.record.{created,updated,deleted}` event whose\n * object is `sys_webhook` (self-healing — when a user toggles a\n * webhook, the handler refreshes the cache before returning).\n * 3. Periodically (default 60s) as belt-and-braces.\n *\n * For multi-node clusters this is *eventually consistent* — node B may\n * not see node A's edit for up to one cycle. That's acceptable for\n * webhook configuration changes (humans don't expect millisecond\n * propagation) and matches Hasura's behaviour.\n *\n * ## Determinism\n * `eventId` is computed from `${object}:${recordId}:${type}:${timestamp}`\n * so the outbox dedup index catches duplicates that could arise from\n * upstream replay or buggy producers — and is stable across nodes.\n */\nexport class AutoEnqueuer {\n private readonly subscriptions = new Map<string, CachedSubscription[]>();\n private readonly subscriptionsObject: string;\n private readonly refreshIntervalMs: number;\n private readonly logger: OptionalLogger;\n private subId: string | undefined;\n private subIdSelfHeal: string | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n private running = false;\n private refreshing: Promise<void> | undefined;\n\n constructor(\n private readonly engine: IDataEngine,\n private readonly realtime: IRealtimeService,\n private readonly enqueue: HttpEnqueueFn,\n opts: AutoEnqueuerOptions = {},\n ) {\n this.subscriptionsObject = opts.subscriptionsObject ?? 'sys_webhook';\n this.refreshIntervalMs = opts.refreshIntervalMs ?? 60_000;\n this.logger = opts.logger ?? {};\n }\n\n /**\n * Load the subscription cache and start listening for events.\n */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.refresh();\n\n // Main subscription: every data event → match → enqueue.\n this.subId = await this.realtime.subscribe(\n 'webhook-auto-enqueuer',\n (event) => this.handleEvent(event),\n );\n\n // Self-healing: any change to sys_webhook refreshes the cache.\n this.subIdSelfHeal = await this.realtime.subscribe(\n 'webhook-auto-enqueuer-self-heal',\n (event) => this.handleSelfHealEvent(event),\n { object: this.subscriptionsObject },\n );\n\n if (this.refreshIntervalMs > 0) {\n this.refreshTimer = setInterval(() => {\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] periodic refresh failed', err),\n );\n }, this.refreshIntervalMs);\n // Don't keep the process alive solely for this timer.\n this.refreshTimer.unref?.();\n }\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n this.running = false;\n if (this.subId) await this.realtime.unsubscribe(this.subId);\n if (this.subIdSelfHeal) await this.realtime.unsubscribe(this.subIdSelfHeal);\n if (this.refreshTimer) clearInterval(this.refreshTimer);\n this.subId = undefined;\n this.subIdSelfHeal = undefined;\n this.refreshTimer = undefined;\n }\n\n /**\n * Force-refresh the subscription cache from storage. Concurrent\n * callers share a single in-flight refresh.\n */\n async refresh(): Promise<void> {\n if (this.refreshing) return this.refreshing;\n this.refreshing = this.doRefresh().finally(() => {\n this.refreshing = undefined;\n });\n return this.refreshing;\n }\n\n private async doRefresh(): Promise<void> {\n let rows: any[];\n try {\n rows = await this.engine.find(this.subscriptionsObject, {\n where: { active: true },\n });\n } catch (err) {\n this.logger.warn?.(\n `[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,\n err,\n );\n return;\n }\n\n const next = new Map<string, CachedSubscription[]>();\n for (const row of rows) {\n const sub = this.parseRow(row);\n if (!sub) continue;\n // Empty objectName == \"any object\" → indexed under '*'.\n const key = sub.objectName ?? '*';\n const arr = next.get(key) ?? [];\n arr.push(sub);\n next.set(key, arr);\n }\n\n this.subscriptions.clear();\n for (const [k, v] of next) this.subscriptions.set(k, v);\n\n this.logger.debug?.('[webhook-auto-enqueuer] cache refreshed', {\n objects: this.subscriptions.size,\n rows: rows.length,\n });\n }\n\n private parseRow(row: any): CachedSubscription | null {\n if (!row?.id || !row?.url) return null;\n // `triggers` is now authored as a multi-select (stored as an array), but\n // legacy rows stored a comma-separated string (and some drivers hand a\n // JSON-encoded array back as a string). Accept all three shapes so a\n // schema change never silently drops a subscription's events.\n const rawTriggers = row.triggers;\n let triggerList: string[];\n if (Array.isArray(rawTriggers)) {\n triggerList = rawTriggers.map((t) => String(t));\n } else {\n const s = String(rawTriggers ?? '').trim();\n if (s.startsWith('[')) {\n try {\n const parsed = JSON.parse(s);\n triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];\n } catch {\n triggerList = s.split(',');\n }\n } else {\n triggerList = s.split(',');\n }\n }\n const normalized = triggerList.map((t) => t.trim().toLowerCase()).filter(Boolean);\n // [#3196] Drop (and warn about) any trigger the enqueuer can't map to an\n // emitted record event — e.g. a legacy `sys_webhook` row authored with\n // the now-removed `undelete`/`api` values, which would otherwise sit in\n // the cache matching nothing. A loud drift-guard so a dead trigger can't\n // silently no-op again.\n const unknown = normalized.filter((t) => !DISPATCHABLE_WEBHOOK_TRIGGERS.has(t));\n if (unknown.length > 0) {\n this.logger.warn?.(\n `[webhook-auto-enqueuer] webhook '${(row.name as string) ?? row.id}' declares trigger(s) the engine never emits: ` +\n `${unknown.join(', ')} — ignored. Dispatchable triggers: create, update, delete.`,\n { id: row.id, unknown },\n );\n }\n const triggers = new Set(\n normalized.filter((t) => DISPATCHABLE_WEBHOOK_TRIGGERS.has(t)) as Array<'create' | 'update' | 'delete'>,\n );\n if (triggers.size === 0) {\n // No dispatchable triggers (or a manual-only webhook with none) —\n // skip auto-enqueue.\n return null;\n }\n\n // The \"definition_json\" field carries advanced config (headers,\n // secret, timeout); attempt a best-effort parse. Fall back to\n // top-level fields where present.\n let defn: Record<string, any> = {};\n if (typeof row.definition_json === 'string' && row.definition_json.length > 0) {\n try {\n defn = JSON.parse(row.definition_json) ?? {};\n } catch {\n defn = {};\n }\n }\n\n return {\n id: row.id as string,\n name: (row.name as string) ?? row.id,\n objectName: row.object_name ? String(row.object_name) : undefined,\n triggers,\n url: String(row.url),\n // Method is authored via a select whose option values are lowercased\n // (get/post/…); upper-case here so delivery uses a canonical HTTP\n // method regardless of whether the row was authored before or after\n // the select change (legacy rows stored 'POST').\n method: String(row.method ?? defn.method ?? 'POST').toUpperCase(),\n headers: defn.headers,\n secret: defn.secret,\n timeoutMs: defn.timeoutMs,\n };\n }\n\n /**\n * Handler for the firehose subscription.\n *\n * NOTE: we intentionally `void` the inner enqueue() so the realtime\n * publisher (and therefore the user's request) is never blocked on\n * webhook persistence.\n */\n private handleEvent(event: RealtimeEventPayload): void {\n if (!event.type?.startsWith('data.record.')) return;\n if (!event.object) return;\n if (event.object === this.subscriptionsObject) return; // self-heal handles its own\n\n const action = event.type.slice('data.record.'.length) as\n | 'created' | 'updated' | 'deleted' | string;\n const trigger = mapActionToTrigger(action);\n if (!trigger) return;\n\n const subs = [\n ...(this.subscriptions.get(event.object) ?? []),\n ...(this.subscriptions.get('*') ?? []),\n ];\n if (subs.length === 0) return;\n\n const payload = event.payload ?? {};\n const recordId =\n (payload as any).recordId ??\n (payload as any).id ??\n (payload as any).after?.id ??\n (payload as any).before?.id ??\n 'unknown';\n\n // Deterministic eventId — same input on any node → same id.\n // Includes timestamp so two distinct updates to the same record\n // don't accidentally dedup.\n const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;\n\n for (const sub of subs) {\n if (!sub.triggers.has(trigger)) continue;\n\n // Fire-and-forget — never await on the hot path. Map the webhook\n // delivery onto the generic HTTP-outbox shape (ADR-0018 M3):\n // - source 'webhook' + dedupKey '<webhookId>:<eventId>' preserves\n // the old (event_id, webhook_id) at-most-once enqueue;\n // - refId = webhookId keeps per-webhook partition affinity / ordering;\n // - label = event type → X-Objectstack-Event header.\n void this.enqueue({\n source: 'webhook',\n refId: sub.id,\n dedupKey: `${sub.id}:${eventId}`,\n label: event.type,\n url: sub.url,\n method: sub.method,\n headers: sub.headers,\n signingSecret: sub.secret,\n timeoutMs: sub.timeoutMs,\n payload: {\n object: event.object,\n recordId,\n action,\n timestamp: event.timestamp,\n ...payload,\n },\n }).catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] enqueue failed', {\n webhook: sub.name,\n eventId,\n err: (err as Error)?.message ?? err,\n }),\n );\n }\n }\n\n private handleSelfHealEvent(event: RealtimeEventPayload): void {\n if (event.object !== this.subscriptionsObject) return;\n if (!event.type?.startsWith('data.record.')) return;\n this.refresh().catch((err) =>\n this.logger.warn?.('[webhook-auto-enqueuer] self-heal refresh failed', err),\n );\n }\n\n /** Test / admin accessor. */\n snapshot(): ReadonlyMap<string, ReadonlyArray<CachedSubscription>> {\n return this.subscriptions;\n }\n}\n\nfunction mapActionToTrigger(\n action: string,\n): 'create' | 'update' | 'delete' | null {\n switch (action) {\n case 'created':\n return 'create';\n case 'updated':\n return 'update';\n case 'deleted':\n return 'delete';\n default:\n return null;\n }\n}\n\n/** The trigger values the enqueuer can actually map from an emitted record event. */\nconst DISPATCHABLE_WEBHOOK_TRIGGERS: ReadonlySet<string> = new Set(['create', 'update', 'delete']);\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Plugin, PluginContext } from '@objectstack/core';\nimport type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';\nimport type { EnqueueHttpInput } from '@objectstack/service-messaging';\nimport { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';\nimport { SysWebhook } from './sys-webhook.object.js';\n\n/**\n * Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface\n * (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime\n * import on the service. Webhook deliveries are enqueued onto the shared\n * `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.\n */\ninterface MessagingHttpSurface {\n isHttpDeliveryReady(): boolean;\n enqueueHttp(input: EnqueueHttpInput): Promise<string>;\n redeliverHttp(id: string): Promise<{ id: string; status: string }>;\n}\n\nexport interface WebhookOutboxPluginOptions {\n /**\n * Auto-enqueue config. When enabled (default `true` if the realtime + data\n * engine services are available), the plugin subscribes to `data.record.*`\n * events and enqueues a delivery onto the shared messaging HTTP outbox for\n * every matching `sys_webhook` row.\n *\n * Set `false` to disable and enqueue webhooks imperatively elsewhere.\n */\n autoEnqueue?: boolean | AutoEnqueuerOptions;\n}\n\n/**\n * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate\n * (ADR-0018 M3).\n *\n * Webhooks are no longer their own delivery engine: the durable outbox, the\n * cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and\n * the retention sweep all live in `@objectstack/service-messaging`\n * (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the\n * webhook-specific concerns:\n * - the `sys_webhook` configuration object,\n * - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox\n * rows (`source: 'webhook'`), and\n * - the redeliver admin endpoint.\n *\n * End-to-end flow:\n *\n * engine.insert('contact', {...})\n * → engine publishes data.record.created via IRealtimeService\n * → AutoEnqueuer matches active sys_webhook rows in O(1)\n * → messaging.enqueueHttp() runs fire-and-forget (off the write path)\n * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)\n *\n * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),\n * which is a foundational, always-on capability.\n */\nexport class WebhookOutboxPlugin implements Plugin {\n name = 'com.objectstack.plugin-webhook-outbox';\n version = '2.0.0';\n type = 'standard' as const;\n dependencies = ['com.objectstack.service.messaging'];\n\n private autoEnqueuer: AutoEnqueuer | undefined;\n\n constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}\n\n async init(ctx: PluginContext): Promise<void> {\n // Register the webhook config object (ADR-0029 K2.a). The delivery\n // telemetry now lives in messaging's `sys_http_delivery`, so the nav's\n // \"Deliveries\" entry points there (filtered to source=webhook in views).\n const manifest = ctx.getService<{ register(m: any): void }>('manifest');\n if (manifest && typeof manifest.register === 'function') {\n manifest.register({\n id: 'com.objectstack.plugin-webhook-outbox.schema',\n namespace: 'sys',\n version: this.version,\n type: 'plugin',\n scope: 'system',\n name: 'Webhook Schemas',\n description: 'Registers sys_webhook (configuration). Deliveries use messaging\\'s sys_http_delivery outbox.',\n objects: [SysWebhook],\n navigationContributions: [\n {\n app: 'setup',\n group: 'group_integrations',\n priority: 100,\n items: [\n { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },\n { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },\n ],\n },\n ],\n });\n } else {\n ctx.logger.warn?.(\n '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',\n );\n }\n\n // ADR-0029 D8 — contribute object translations once i18n is up.\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n try {\n const i18n = ctx.getService<any>('i18n');\n if (i18n && typeof i18n.loadTranslations === 'function') {\n const { WebhooksTranslations } = await import('./translations/index.js');\n for (const [locale, data] of Object.entries(WebhooksTranslations)) {\n i18n.loadTranslations(locale, data as Record<string, unknown>);\n }\n }\n } catch { /* i18n optional */ }\n });\n }\n\n const autoEnqueueOpt = this.options.autoEnqueue ?? true;\n\n if (typeof (ctx as any).hook === 'function') {\n (ctx as any).hook('kernel:ready', async () => {\n await this.bootAutoEnqueue(ctx, autoEnqueueOpt);\n this.registerAdminRoutes(ctx);\n });\n }\n\n ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {\n autoEnqueue: autoEnqueueOpt !== false,\n });\n }\n\n async dispose(): Promise<void> {\n await this.autoEnqueuer?.stop();\n }\n\n private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {\n const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);\n return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;\n }\n\n private async bootAutoEnqueue(\n ctx: PluginContext,\n opt: boolean | AutoEnqueuerOptions,\n ): Promise<void> {\n if (opt === false) return;\n const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);\n const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);\n const messaging = this.getMessaging(ctx);\n if (!engine || !realtime || !messaging) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',\n { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },\n );\n return;\n }\n if (!messaging.isHttpDeliveryReady()) {\n ctx.logger.warn?.(\n '[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',\n );\n }\n\n const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;\n this.autoEnqueuer = new AutoEnqueuer(\n engine,\n realtime,\n (input) => messaging.enqueueHttp(input),\n { ...enqOpts, logger: ctx.logger },\n );\n await this.autoEnqueuer.start();\n ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);\n ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');\n }\n\n private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {\n for (const n of names) {\n try {\n const svc = ctx.getService<T>(n);\n if (svc) return svc;\n } catch {\n // fall through\n }\n }\n return undefined;\n }\n\n /**\n * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is\n * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the\n * better-auth session cookie — every authenticated user counts.\n */\n private registerAdminRoutes(ctx: PluginContext): void {\n const http = this.tryGetService<any>(ctx, ['http-server']);\n if (!http || typeof http.getRawApp !== 'function') {\n ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');\n return;\n }\n const rawApp = http.getRawApp();\n const messaging = this.getMessaging(ctx);\n if (!rawApp || !messaging) return;\n\n rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {\n const userId = await this.resolveSessionUserId(ctx, c);\n if (!userId) {\n return c.json(\n { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },\n 401,\n );\n }\n let body: any;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);\n }\n const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';\n if (!deliveryId) {\n return c.json(\n { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },\n 400,\n );\n }\n try {\n const row = await messaging.redeliverHttp(deliveryId);\n ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });\n return c.json({ success: true, data: { id: row.id, status: row.status } });\n } catch (err: any) {\n const code = err?.code;\n if (code === 'not_found') {\n return c.json({ success: false, error: 'not_found', message: err.message }, 404);\n }\n if (code === 'not_eligible') {\n return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);\n }\n ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);\n return c.json(\n { success: false, error: 'internal_error', message: err?.message ?? String(err) },\n 500,\n );\n }\n });\n\n ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');\n }\n\n private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {\n try {\n const authService: any = this.tryGetService<any>(ctx, ['auth']);\n if (!authService) return undefined;\n let api: any = authService.api;\n if (!api && typeof authService.getApi === 'function') {\n api = await authService.getApi();\n }\n if (!api?.getSession) return undefined;\n const session = await api.getSession({ headers: c.req.raw.headers });\n const uid = session?.user?.id;\n return typeof uid === 'string' && uid.length > 0 ? uid : undefined;\n } catch {\n return undefined;\n }\n }\n}\n"]}
|