@objectstack/plugin-webhooks 14.6.0 → 14.8.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 +14 -14
- package/CHANGELOG.md +42 -0
- package/dist/{chunk-RA3REIOA.cjs → chunk-I63ISOGO.cjs} +31 -16
- package/dist/chunk-I63ISOGO.cjs.map +1 -0
- package/dist/{chunk-HSSV22TO.js → chunk-YQG6SUP3.js} +31 -16
- package/dist/chunk-YQG6SUP3.js.map +1 -0
- package/dist/index.cjs +31 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +24 -4
- package/dist/index.js.map +1 -1
- package/dist/schema.cjs +2 -2
- package/dist/schema.d.cts +24 -11
- package/dist/schema.d.ts +24 -11
- package/dist/schema.js +1 -1
- package/package.json +4 -4
- package/src/auto-enqueuer.test.ts +36 -0
- package/src/auto-enqueuer.ts +28 -5
- package/src/sys-webhook.object.ts +30 -15
- package/dist/chunk-HSSV22TO.js.map +0 -1
- package/dist/chunk-RA3REIOA.cjs.map +0 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/plugin-webhooks@14.
|
|
2
|
+
> @objectstack/plugin-webhooks@14.8.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[
|
|
14
|
-
[32mESM[39m [1mdist/chunk-HSSV22TO.js [22m[32m4.82 KB[39m
|
|
13
|
+
[32mESM[39m [1mdist/index.js [22m[32m13.47 KB[39m
|
|
15
14
|
[32mESM[39m [1mdist/schema.js [22m[32m111.00 B[39m
|
|
15
|
+
[32mESM[39m [1mdist/chunk-YQG6SUP3.js [22m[32m5.67 KB[39m
|
|
16
16
|
[32mESM[39m [1mdist/translations-AV47AVPV.js [22m[32m19.41 KB[39m
|
|
17
|
-
[32mESM[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mESM[39m [1mdist/chunk-HSSV22TO.js.map [22m[32m9.27 KB[39m
|
|
17
|
+
[32mESM[39m [1mdist/index.js.map [22m[32m34.03 KB[39m
|
|
19
18
|
[32mESM[39m [1mdist/schema.js.map [22m[32m71.00 B[39m
|
|
19
|
+
[32mESM[39m [1mdist/chunk-YQG6SUP3.js.map [22m[32m10.28 KB[39m
|
|
20
20
|
[32mESM[39m [1mdist/translations-AV47AVPV.js.map [22m[32m30.54 KB[39m
|
|
21
|
-
[32mESM[39m ⚡️ Build success in
|
|
22
|
-
[32mCJS[39m [1mdist/index.cjs [22m[
|
|
21
|
+
[32mESM[39m ⚡️ Build success in 617ms
|
|
22
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m17.15 KB[39m
|
|
23
23
|
[32mCJS[39m [1mdist/schema.cjs [22m[32m220.00 B[39m
|
|
24
|
-
[32mCJS[39m [1mdist/chunk-
|
|
24
|
+
[32mCJS[39m [1mdist/chunk-I63ISOGO.cjs [22m[32m5.81 KB[39m
|
|
25
25
|
[32mCJS[39m [1mdist/translations-OAKKANSP.cjs [22m[32m19.50 KB[39m
|
|
26
|
-
[32mCJS[39m [1mdist/index.cjs.map [22m[
|
|
26
|
+
[32mCJS[39m [1mdist/index.cjs.map [22m[32m31.50 KB[39m
|
|
27
27
|
[32mCJS[39m [1mdist/schema.cjs.map [22m[32m278.00 B[39m
|
|
28
|
-
[32mCJS[39m [1mdist/chunk-
|
|
28
|
+
[32mCJS[39m [1mdist/chunk-I63ISOGO.cjs.map [22m[32m12.15 KB[39m
|
|
29
29
|
[32mCJS[39m [1mdist/translations-OAKKANSP.cjs.map [22m[32m34.65 KB[39m
|
|
30
|
-
[32mCJS[39m ⚡️ Build success in
|
|
30
|
+
[32mCJS[39m ⚡️ Build success in 624ms
|
|
31
31
|
[34mDTS[39m Build start
|
|
32
|
-
[32mDTS[39m ⚡️ Build success in
|
|
32
|
+
[32mDTS[39m ⚡️ Build success in 23849ms
|
|
33
33
|
[32mDTS[39m [1mdist/index.d.ts [22m[32m6.97 KB[39m
|
|
34
|
-
[32mDTS[39m [1mdist/schema.d.ts [22m[32m134.
|
|
34
|
+
[32mDTS[39m [1mdist/schema.d.ts [22m[32m134.83 KB[39m
|
|
35
35
|
[32mDTS[39m [1mdist/index.d.cts [22m[32m6.97 KB[39m
|
|
36
|
-
[32mDTS[39m [1mdist/schema.d.cts [22m[32m134.
|
|
36
|
+
[32mDTS[39m [1mdist/schema.d.cts [22m[32m134.83 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @objectstack/plugin-webhooks
|
|
2
2
|
|
|
3
|
+
## 14.8.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [16b4bf6]
|
|
8
|
+
- Updated dependencies [16b4bf6]
|
|
9
|
+
- Updated dependencies [10e8983]
|
|
10
|
+
- Updated dependencies [607aaf4]
|
|
11
|
+
- Updated dependencies [bb71321]
|
|
12
|
+
- @objectstack/spec@14.8.0
|
|
13
|
+
- @objectstack/service-messaging@14.8.0
|
|
14
|
+
- @objectstack/core@14.8.0
|
|
15
|
+
|
|
16
|
+
## 14.7.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- f344ee1: Webhook form: pick, don't type. The `sys_webhook` create/edit form made admins
|
|
21
|
+
hand-type machine data in three fields; they're now proper controls (extends the
|
|
22
|
+
`sys_sharing_rule` pass):
|
|
23
|
+
|
|
24
|
+
- `method` — free text → **select** (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`). Option
|
|
25
|
+
values are lowercased by `Field.select`; the auto-enqueuer now upper-cases the
|
|
26
|
+
resolved method before delivery, so legacy `'POST'` rows and the new lowercase
|
|
27
|
+
values both normalise to a canonical HTTP method.
|
|
28
|
+
- `triggers` — hand-typed comma-separated string → **multi-select**
|
|
29
|
+
(`create`/`update`/`delete`/`undelete`/`api`). Stored as an array; the
|
|
30
|
+
auto-enqueuer's `parseRow` now accepts array, JSON-encoded-array-string, and
|
|
31
|
+
the legacy comma-separated forms, so existing subscriptions keep firing.
|
|
32
|
+
- `object_name` — free text → the **`object-ref`** object picker (same widget as
|
|
33
|
+
`sys_sharing_rule`; degrades to a text input where the widget isn't loaded).
|
|
34
|
+
|
|
35
|
+
Backward compatible: no data migration required. Added tests covering the array
|
|
36
|
+
and JSON-string trigger shapes.
|
|
37
|
+
|
|
38
|
+
### Patch Changes
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [d6a72eb]
|
|
41
|
+
- @objectstack/spec@14.7.0
|
|
42
|
+
- @objectstack/core@14.7.0
|
|
43
|
+
- @objectstack/service-messaging@14.7.0
|
|
44
|
+
|
|
3
45
|
## 14.6.0
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
|
@@ -79,16 +79,25 @@ var SysWebhook = _data.ObjectSchema.create({
|
|
|
79
79
|
label: "Object",
|
|
80
80
|
required: false,
|
|
81
81
|
maxLength: 100,
|
|
82
|
+
// Object picker (same widget as sys_sharing_rule) instead of a free-text
|
|
83
|
+
// machine name. Falls back to a text input when the widget is unavailable.
|
|
84
|
+
widget: "object-ref",
|
|
82
85
|
description: "Short object name whose events fire this webhook (blank = manual / API-triggered)",
|
|
83
86
|
group: "Definition"
|
|
84
87
|
}),
|
|
85
|
-
triggers: _data.Field.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
triggers: _data.Field.select(
|
|
89
|
+
["create", "update", "delete", "undelete", "api"],
|
|
90
|
+
{
|
|
91
|
+
label: "Triggers",
|
|
92
|
+
required: false,
|
|
93
|
+
// Multi-select instead of a hand-typed comma-separated string. Stored as
|
|
94
|
+
// an array; the auto-enqueuer parser also tolerates the legacy
|
|
95
|
+
// comma-separated / JSON-string forms so existing rows keep working.
|
|
96
|
+
multiple: true,
|
|
97
|
+
description: "Record events that fire this webhook (empty = manual / API-triggered)",
|
|
98
|
+
group: "Definition"
|
|
99
|
+
}
|
|
100
|
+
),
|
|
92
101
|
url: _data.Field.text({
|
|
93
102
|
label: "Target URL",
|
|
94
103
|
required: true,
|
|
@@ -96,14 +105,20 @@ var SysWebhook = _data.ObjectSchema.create({
|
|
|
96
105
|
description: "External endpoint that receives the POST",
|
|
97
106
|
group: "Definition"
|
|
98
107
|
}),
|
|
99
|
-
method: _data.Field.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
method: _data.Field.select(
|
|
109
|
+
["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
110
|
+
{
|
|
111
|
+
label: "HTTP Method",
|
|
112
|
+
required: true,
|
|
113
|
+
// Select instead of free text. Option values are lowercased by the
|
|
114
|
+
// Field.select helper (get/post/…); the auto-enqueuer upper-cases the
|
|
115
|
+
// resolved method before delivery, so existing 'POST' rows and the
|
|
116
|
+
// lowercase option values both normalise correctly.
|
|
117
|
+
defaultValue: "post",
|
|
118
|
+
description: "HTTP method used for the callback request",
|
|
119
|
+
group: "Definition"
|
|
120
|
+
}
|
|
121
|
+
),
|
|
107
122
|
description: _data.Field.textarea({ label: "Description", required: false, group: "Definition" }),
|
|
108
123
|
active: _data.Field.boolean({
|
|
109
124
|
label: "Active",
|
|
@@ -137,4 +152,4 @@ var SysWebhook = _data.ObjectSchema.create({
|
|
|
137
152
|
|
|
138
153
|
|
|
139
154
|
exports.SysWebhook = SysWebhook;
|
|
140
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-I63ISOGO.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/chunk-I63ISOGO.cjs","../src/sys-webhook.object.ts"],"names":[],"mappings":"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","file":"/home/runner/work/framework/framework/packages/plugins/plugin-webhooks/dist/chunk-I63ISOGO.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 events fire this webhook (blank = manual / API-triggered)',\n group: 'Definition',\n }),\n\n triggers: Field.select(\n ['create', 'update', 'delete', 'undelete', 'api'],\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 (empty = manual / API-triggered)',\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"]}
|
|
@@ -79,16 +79,25 @@ var SysWebhook = ObjectSchema.create({
|
|
|
79
79
|
label: "Object",
|
|
80
80
|
required: false,
|
|
81
81
|
maxLength: 100,
|
|
82
|
+
// Object picker (same widget as sys_sharing_rule) instead of a free-text
|
|
83
|
+
// machine name. Falls back to a text input when the widget is unavailable.
|
|
84
|
+
widget: "object-ref",
|
|
82
85
|
description: "Short object name whose events fire this webhook (blank = manual / API-triggered)",
|
|
83
86
|
group: "Definition"
|
|
84
87
|
}),
|
|
85
|
-
triggers: Field.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
triggers: Field.select(
|
|
89
|
+
["create", "update", "delete", "undelete", "api"],
|
|
90
|
+
{
|
|
91
|
+
label: "Triggers",
|
|
92
|
+
required: false,
|
|
93
|
+
// Multi-select instead of a hand-typed comma-separated string. Stored as
|
|
94
|
+
// an array; the auto-enqueuer parser also tolerates the legacy
|
|
95
|
+
// comma-separated / JSON-string forms so existing rows keep working.
|
|
96
|
+
multiple: true,
|
|
97
|
+
description: "Record events that fire this webhook (empty = manual / API-triggered)",
|
|
98
|
+
group: "Definition"
|
|
99
|
+
}
|
|
100
|
+
),
|
|
92
101
|
url: Field.text({
|
|
93
102
|
label: "Target URL",
|
|
94
103
|
required: true,
|
|
@@ -96,14 +105,20 @@ var SysWebhook = ObjectSchema.create({
|
|
|
96
105
|
description: "External endpoint that receives the POST",
|
|
97
106
|
group: "Definition"
|
|
98
107
|
}),
|
|
99
|
-
method: Field.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
method: Field.select(
|
|
109
|
+
["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
110
|
+
{
|
|
111
|
+
label: "HTTP Method",
|
|
112
|
+
required: true,
|
|
113
|
+
// Select instead of free text. Option values are lowercased by the
|
|
114
|
+
// Field.select helper (get/post/…); the auto-enqueuer upper-cases the
|
|
115
|
+
// resolved method before delivery, so existing 'POST' rows and the
|
|
116
|
+
// lowercase option values both normalise correctly.
|
|
117
|
+
defaultValue: "post",
|
|
118
|
+
description: "HTTP method used for the callback request",
|
|
119
|
+
group: "Definition"
|
|
120
|
+
}
|
|
121
|
+
),
|
|
107
122
|
description: Field.textarea({ label: "Description", required: false, group: "Definition" }),
|
|
108
123
|
active: Field.boolean({
|
|
109
124
|
label: "Active",
|
|
@@ -137,4 +152,4 @@ var SysWebhook = ObjectSchema.create({
|
|
|
137
152
|
export {
|
|
138
153
|
SysWebhook
|
|
139
154
|
};
|
|
140
|
-
//# sourceMappingURL=chunk-
|
|
155
|
+
//# sourceMappingURL=chunk-YQG6SUP3.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 events fire this webhook (blank = manual / API-triggered)',\n group: 'Definition',\n }),\n\n triggers: Field.select(\n ['create', 'update', 'delete', 'undelete', 'api'],\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 (empty = manual / API-triggered)',\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 _chunkI63ISOGOcjs = require('./chunk-I63ISOGO.cjs');
|
|
4
4
|
|
|
5
5
|
// src/auto-enqueuer.ts
|
|
6
6
|
var AutoEnqueuer = class {
|
|
@@ -91,9 +91,25 @@ var AutoEnqueuer = class {
|
|
|
91
91
|
}
|
|
92
92
|
parseRow(row) {
|
|
93
93
|
if (!_optionalChain([row, 'optionalAccess', _13 => _13.id]) || !_optionalChain([row, 'optionalAccess', _14 => _14.url])) return null;
|
|
94
|
-
const
|
|
94
|
+
const rawTriggers = row.triggers;
|
|
95
|
+
let triggerList;
|
|
96
|
+
if (Array.isArray(rawTriggers)) {
|
|
97
|
+
triggerList = rawTriggers.map((t) => String(t));
|
|
98
|
+
} else {
|
|
99
|
+
const s = String(_nullishCoalesce(rawTriggers, () => ( ""))).trim();
|
|
100
|
+
if (s.startsWith("[")) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(s);
|
|
103
|
+
triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];
|
|
104
|
+
} catch (e) {
|
|
105
|
+
triggerList = s.split(",");
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
triggerList = s.split(",");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
95
111
|
const triggers = new Set(
|
|
96
|
-
|
|
112
|
+
triggerList.map((t) => t.trim().toLowerCase()).filter(Boolean)
|
|
97
113
|
);
|
|
98
114
|
if (triggers.size === 0) {
|
|
99
115
|
return null;
|
|
@@ -102,7 +118,7 @@ var AutoEnqueuer = class {
|
|
|
102
118
|
if (typeof row.definition_json === "string" && row.definition_json.length > 0) {
|
|
103
119
|
try {
|
|
104
120
|
defn = _nullishCoalesce(JSON.parse(row.definition_json), () => ( {}));
|
|
105
|
-
} catch (
|
|
121
|
+
} catch (e2) {
|
|
106
122
|
defn = {};
|
|
107
123
|
}
|
|
108
124
|
}
|
|
@@ -112,7 +128,11 @@ var AutoEnqueuer = class {
|
|
|
112
128
|
objectName: row.object_name ? String(row.object_name) : void 0,
|
|
113
129
|
triggers,
|
|
114
130
|
url: String(row.url),
|
|
115
|
-
|
|
131
|
+
// Method is authored via a select whose option values are lowercased
|
|
132
|
+
// (get/post/…); upper-case here so delivery uses a canonical HTTP
|
|
133
|
+
// method regardless of whether the row was authored before or after
|
|
134
|
+
// the select change (legacy rows stored 'POST').
|
|
135
|
+
method: String(_nullishCoalesce(_nullishCoalesce(row.method, () => ( defn.method)), () => ( "POST"))).toUpperCase(),
|
|
116
136
|
headers: defn.headers,
|
|
117
137
|
secret: defn.secret,
|
|
118
138
|
timeoutMs: defn.timeoutMs
|
|
@@ -215,7 +235,7 @@ var WebhookOutboxPlugin = class {
|
|
|
215
235
|
scope: "system",
|
|
216
236
|
name: "Webhook Schemas",
|
|
217
237
|
description: "Registers sys_webhook (configuration). Deliveries use messaging's sys_http_delivery outbox.",
|
|
218
|
-
objects: [
|
|
238
|
+
objects: [_chunkI63ISOGOcjs.SysWebhook],
|
|
219
239
|
navigationContributions: [
|
|
220
240
|
{
|
|
221
241
|
app: "setup",
|
|
@@ -243,7 +263,7 @@ var WebhookOutboxPlugin = class {
|
|
|
243
263
|
i18n.loadTranslations(locale, data);
|
|
244
264
|
}
|
|
245
265
|
}
|
|
246
|
-
} catch (
|
|
266
|
+
} catch (e3) {
|
|
247
267
|
}
|
|
248
268
|
});
|
|
249
269
|
}
|
|
@@ -298,7 +318,7 @@ var WebhookOutboxPlugin = class {
|
|
|
298
318
|
try {
|
|
299
319
|
const svc = ctx.getService(n);
|
|
300
320
|
if (svc) return svc;
|
|
301
|
-
} catch (
|
|
321
|
+
} catch (e4) {
|
|
302
322
|
}
|
|
303
323
|
}
|
|
304
324
|
return void 0;
|
|
@@ -328,7 +348,7 @@ var WebhookOutboxPlugin = class {
|
|
|
328
348
|
let body;
|
|
329
349
|
try {
|
|
330
350
|
body = await c.req.json();
|
|
331
|
-
} catch (
|
|
351
|
+
} catch (e5) {
|
|
332
352
|
return c.json({ success: false, error: "invalid_body", message: "Request body must be JSON." }, 400);
|
|
333
353
|
}
|
|
334
354
|
const deliveryId = typeof _optionalChain([body, 'optionalAccess', _53 => _53.deliveryId]) === "string" ? body.deliveryId.trim() : "";
|
|
@@ -371,7 +391,7 @@ var WebhookOutboxPlugin = class {
|
|
|
371
391
|
const session = await api.getSession({ headers: c.req.raw.headers });
|
|
372
392
|
const uid = _optionalChain([session, 'optionalAccess', _66 => _66.user, 'optionalAccess', _67 => _67.id]);
|
|
373
393
|
return typeof uid === "string" && uid.length > 0 ? uid : void 0;
|
|
374
|
-
} catch (
|
|
394
|
+
} catch (e6) {
|
|
375
395
|
return void 0;
|
|
376
396
|
}
|
|
377
397
|
}
|
|
@@ -380,5 +400,5 @@ var WebhookOutboxPlugin = class {
|
|
|
380
400
|
|
|
381
401
|
|
|
382
402
|
|
|
383
|
-
exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook =
|
|
403
|
+
exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkI63ISOGOcjs.SysWebhook; exports.WebhookOutboxPlugin = WebhookOutboxPlugin;
|
|
384
404
|
//# 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 const triggersField = (row.triggers ?? '') as string;\n const triggers = new Set(\n triggersField\n .split(',')\n .map((s: string) => s.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: row.method ?? defn.method ?? 'POST',\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 (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"]}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SysWebhook
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-YQG6SUP3.js";
|
|
4
4
|
|
|
5
5
|
// src/auto-enqueuer.ts
|
|
6
6
|
var AutoEnqueuer = class {
|
|
@@ -91,9 +91,25 @@ var AutoEnqueuer = class {
|
|
|
91
91
|
}
|
|
92
92
|
parseRow(row) {
|
|
93
93
|
if (!row?.id || !row?.url) return null;
|
|
94
|
-
const
|
|
94
|
+
const rawTriggers = row.triggers;
|
|
95
|
+
let triggerList;
|
|
96
|
+
if (Array.isArray(rawTriggers)) {
|
|
97
|
+
triggerList = rawTriggers.map((t) => String(t));
|
|
98
|
+
} else {
|
|
99
|
+
const s = String(rawTriggers ?? "").trim();
|
|
100
|
+
if (s.startsWith("[")) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(s);
|
|
103
|
+
triggerList = Array.isArray(parsed) ? parsed.map((t) => String(t)) : [s];
|
|
104
|
+
} catch {
|
|
105
|
+
triggerList = s.split(",");
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
triggerList = s.split(",");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
95
111
|
const triggers = new Set(
|
|
96
|
-
|
|
112
|
+
triggerList.map((t) => t.trim().toLowerCase()).filter(Boolean)
|
|
97
113
|
);
|
|
98
114
|
if (triggers.size === 0) {
|
|
99
115
|
return null;
|
|
@@ -112,7 +128,11 @@ var AutoEnqueuer = class {
|
|
|
112
128
|
objectName: row.object_name ? String(row.object_name) : void 0,
|
|
113
129
|
triggers,
|
|
114
130
|
url: String(row.url),
|
|
115
|
-
|
|
131
|
+
// Method is authored via a select whose option values are lowercased
|
|
132
|
+
// (get/post/…); upper-case here so delivery uses a canonical HTTP
|
|
133
|
+
// method regardless of whether the row was authored before or after
|
|
134
|
+
// the select change (legacy rows stored 'POST').
|
|
135
|
+
method: String(row.method ?? defn.method ?? "POST").toUpperCase(),
|
|
116
136
|
headers: defn.headers,
|
|
117
137
|
secret: defn.secret,
|
|
118
138
|
timeoutMs: defn.timeoutMs
|