@objectstack/plugin-webhooks 17.0.0-rc.0 → 17.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +632 -0
- package/dist/{chunk-6EPMRZ7I.js → chunk-3QGZLM3T.js} +8 -3
- package/dist/chunk-3QGZLM3T.js.map +1 -0
- package/dist/{chunk-QUTQSOQC.cjs → chunk-JQUVS5KK.cjs} +8 -3
- package/dist/chunk-JQUVS5KK.cjs.map +1 -0
- package/dist/index.cjs +182 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +138 -19
- package/dist/index.js.map +1 -1
- package/dist/schema.cjs +2 -2
- package/dist/schema.d.cts +721 -548
- package/dist/schema.d.ts +721 -548
- package/dist/schema.js +1 -1
- package/dist/{translations-Y3CXWOTM.js → translations-2VGD4XRF.js} +17 -9
- package/dist/translations-2VGD4XRF.js.map +1 -0
- package/dist/{translations-CBQRUIS5.cjs → translations-BWS57U2V.cjs} +17 -9
- package/dist/translations-BWS57U2V.cjs.map +1 -0
- package/package.json +12 -6
- package/.turbo/turbo-build.log +0 -36
- package/dist/chunk-6EPMRZ7I.js.map +0 -1
- package/dist/chunk-QUTQSOQC.cjs.map +0 -1
- package/dist/translations-CBQRUIS5.cjs.map +0 -1
- package/dist/translations-Y3CXWOTM.js.map +0 -1
- package/scripts/i18n-extract.config.ts +0 -34
- package/src/auto-enqueuer.test.ts +0 -431
- package/src/auto-enqueuer.ts +0 -383
- package/src/bootstrap-declared-webhooks.test.ts +0 -283
- package/src/bootstrap-declared-webhooks.ts +0 -205
- package/src/index.ts +0 -29
- package/src/schema.ts +0 -21
- package/src/sys-webhook.object.ts +0 -231
- package/src/translations/bundle-ownership.test.ts +0 -41
- package/src/translations/en.objects.generated.ts +0 -101
- package/src/translations/es-ES.objects.generated.ts +0 -101
- package/src/translations/index.ts +0 -23
- package/src/translations/ja-JP.objects.generated.ts +0 -101
- package/src/translations/zh-CN.objects.generated.ts +0 -101
- package/src/webhook-outbox-plugin.ts +0 -305
- package/src/webhook-provenance.ts +0 -86
- package/tsconfig.json +0 -10
- package/tsup.config.ts +0 -14
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Auto-generated by 'os i18n extract' for locale 'es-ES'.
|
|
5
|
-
* Edit translations in place; re-run extract (with --merge) to fill new gaps.
|
|
6
|
-
* Do not hand-edit the structure — only the leaf string values.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { TranslationData } from '@objectstack/spec/system';
|
|
10
|
-
|
|
11
|
-
export const esESObjects: NonNullable<TranslationData['objects']> = {
|
|
12
|
-
sys_webhook: {
|
|
13
|
-
label: "Webhook",
|
|
14
|
-
pluralLabel: "Webhooks",
|
|
15
|
-
description: "Suscripción saliente de Webhook HTTP. Se crea mediante defineWebhook() en código o con el editor de Studio; la ejecuta el plugin del conector HTTP.",
|
|
16
|
-
fields: {
|
|
17
|
-
id: {
|
|
18
|
-
label: "ID de webhook"
|
|
19
|
-
},
|
|
20
|
-
name: {
|
|
21
|
-
label: "Nombre",
|
|
22
|
-
help: "Nombre snake_case único; se usa en los registros y en la auditoría."
|
|
23
|
-
},
|
|
24
|
-
label: {
|
|
25
|
-
label: "Nombre visible"
|
|
26
|
-
},
|
|
27
|
-
object_name: {
|
|
28
|
-
label: "Objeto",
|
|
29
|
-
help: "Nombre corto del objeto cuyos eventos de registro (create/update/delete) activan este Webhook."
|
|
30
|
-
},
|
|
31
|
-
triggers: {
|
|
32
|
-
label: "Desencadenantes",
|
|
33
|
-
help: "Lista de eventos separada por comas: create,update,delete.",
|
|
34
|
-
options: {
|
|
35
|
-
create: "Crear",
|
|
36
|
-
update: "Actualizar",
|
|
37
|
-
delete: "Eliminar"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
url: {
|
|
41
|
-
label: "URL de destino",
|
|
42
|
-
help: "Endpoint externo que recibe el POST."
|
|
43
|
-
},
|
|
44
|
-
method: {
|
|
45
|
-
label: "Método HTTP",
|
|
46
|
-
help: "GET / POST / PUT / PATCH / DELETE",
|
|
47
|
-
options: {
|
|
48
|
-
get: "GET",
|
|
49
|
-
post: "POST",
|
|
50
|
-
put: "PUT",
|
|
51
|
-
patch: "PATCH",
|
|
52
|
-
delete: "DELETE"
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
description: {
|
|
56
|
-
label: "Descripción"
|
|
57
|
-
},
|
|
58
|
-
active: {
|
|
59
|
-
label: "Activo",
|
|
60
|
-
help: "Los Webhooks inactivos se omiten en el despachador."
|
|
61
|
-
},
|
|
62
|
-
definition_json: {
|
|
63
|
-
label: "Definición",
|
|
64
|
-
help: "JSON serializado de Webhook (consulte @objectstack/spec/automation/webhook): configuración completa de cabeceras/auth/reintentos/payload."
|
|
65
|
-
},
|
|
66
|
-
managed_by: {
|
|
67
|
-
label: "Gestionado por",
|
|
68
|
-
help: "Procedencia del registro: platform = integrado en el framework / package = declarado por app o paquete (sembrado al arrancar desde los webhooks de defineStack) / admin = creado en Setup.",
|
|
69
|
-
options: {
|
|
70
|
-
platform: "Plataforma",
|
|
71
|
-
package: "Paquete",
|
|
72
|
-
admin: "Administrador"
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
customized: {
|
|
76
|
-
label: "Personalizado",
|
|
77
|
-
help: "Se establece cuando un administrador edita un webhook declarado por un paquete; a partir de entonces el sembrado de arranque deja de sobrescribir la fila (un webhook ruidoso desactivado sobrevive a los redespliegues). Sin sentido en filas admin."
|
|
78
|
-
},
|
|
79
|
-
created_at: {
|
|
80
|
-
label: "Creado el"
|
|
81
|
-
},
|
|
82
|
-
updated_at: {
|
|
83
|
-
label: "Actualizado el"
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
_views: {
|
|
87
|
-
active: {
|
|
88
|
-
label: "Activo"
|
|
89
|
-
},
|
|
90
|
-
inactive: {
|
|
91
|
-
label: "Inactivo"
|
|
92
|
-
},
|
|
93
|
-
by_object: {
|
|
94
|
-
label: "Por objeto"
|
|
95
|
-
},
|
|
96
|
-
all_webhooks: {
|
|
97
|
-
label: "Todos"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* WebhooksTranslations — i18n bundle owned by this plugin (ADR-0029 D8).
|
|
5
|
-
*
|
|
6
|
-
* Object label/field/view/action translations for the sys_* objects this
|
|
7
|
-
* plugin owns. Loaded at runtime via the plugin's `kernel:ready` hook
|
|
8
|
-
* (`i18n.loadTranslations`). Regenerate with `os i18n extract` against
|
|
9
|
-
* `scripts/i18n-extract.config.ts`.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { TranslationBundle } from '@objectstack/spec/system';
|
|
13
|
-
import { enObjects } from './en.objects.generated.js';
|
|
14
|
-
import { zhCNObjects } from './zh-CN.objects.generated.js';
|
|
15
|
-
import { jaJPObjects } from './ja-JP.objects.generated.js';
|
|
16
|
-
import { esESObjects } from './es-ES.objects.generated.js';
|
|
17
|
-
|
|
18
|
-
export const WebhooksTranslations: TranslationBundle = {
|
|
19
|
-
en: { objects: enObjects },
|
|
20
|
-
'zh-CN': { objects: zhCNObjects },
|
|
21
|
-
'ja-JP': { objects: jaJPObjects },
|
|
22
|
-
'es-ES': { objects: esESObjects },
|
|
23
|
-
};
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Auto-generated by 'os i18n extract' for locale 'ja-JP'.
|
|
5
|
-
* Edit translations in place; re-run extract (with --merge) to fill new gaps.
|
|
6
|
-
* Do not hand-edit the structure — only the leaf string values.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { TranslationData } from '@objectstack/spec/system';
|
|
10
|
-
|
|
11
|
-
export const jaJPObjects: NonNullable<TranslationData['objects']> = {
|
|
12
|
-
sys_webhook: {
|
|
13
|
-
label: "Webhook",
|
|
14
|
-
pluralLabel: "Webhook",
|
|
15
|
-
description: "送信 HTTP Webhook サブスクリプション。defineWebhook() またはスタジオエディタで作成し、HTTP コネクタプラグインが実行します。",
|
|
16
|
-
fields: {
|
|
17
|
-
id: {
|
|
18
|
-
label: "Webhook ID"
|
|
19
|
-
},
|
|
20
|
-
name: {
|
|
21
|
-
label: "名前",
|
|
22
|
-
help: "一意の snake_case 名 — ログおよび監査で参照"
|
|
23
|
-
},
|
|
24
|
-
label: {
|
|
25
|
-
label: "表示名"
|
|
26
|
-
},
|
|
27
|
-
object_name: {
|
|
28
|
-
label: "オブジェクト",
|
|
29
|
-
help: "このウェブフックを発火するレコードイベント(create/update/delete)のオブジェクト短縮名"
|
|
30
|
-
},
|
|
31
|
-
triggers: {
|
|
32
|
-
label: "トリガー",
|
|
33
|
-
help: "カンマ区切りのイベントリスト: create,update,delete",
|
|
34
|
-
options: {
|
|
35
|
-
create: "作成",
|
|
36
|
-
update: "更新",
|
|
37
|
-
delete: "削除"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
url: {
|
|
41
|
-
label: "ターゲット URL",
|
|
42
|
-
help: "POST を受信する外部エンドポイント"
|
|
43
|
-
},
|
|
44
|
-
method: {
|
|
45
|
-
label: "HTTP メソッド",
|
|
46
|
-
help: "GET / POST / PUT / PATCH / DELETE",
|
|
47
|
-
options: {
|
|
48
|
-
get: "GET",
|
|
49
|
-
post: "POST",
|
|
50
|
-
put: "PUT",
|
|
51
|
-
patch: "PATCH",
|
|
52
|
-
delete: "DELETE"
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
description: {
|
|
56
|
-
label: "説明"
|
|
57
|
-
},
|
|
58
|
-
active: {
|
|
59
|
-
label: "有効",
|
|
60
|
-
help: "無効にするとディスパッチャにスキップされます"
|
|
61
|
-
},
|
|
62
|
-
definition_json: {
|
|
63
|
-
label: "定義",
|
|
64
|
-
help: "シリアライズされた Webhook JSON(@objectstack/spec/automation/webhook 参照)— ヘッダー/認証/リトライ/ペイロード設定を含む"
|
|
65
|
-
},
|
|
66
|
-
managed_by: {
|
|
67
|
-
label: "管理元",
|
|
68
|
-
help: "レコードの出所: platform = フレームワーク組み込み / package = アプリ・パッケージ宣言(defineStack の webhooks から起動時シード)/ admin = Setup で作成。",
|
|
69
|
-
options: {
|
|
70
|
-
platform: "プラットフォーム",
|
|
71
|
-
package: "パッケージ",
|
|
72
|
-
admin: "管理者"
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
customized: {
|
|
76
|
-
label: "カスタマイズ済み",
|
|
77
|
-
help: "管理者がパッケージ宣言の webhook を編集すると設定されます。以後、起動時シードはこの行を上書きしません(無効化したノイジーな webhook は再デプロイ後も保持)。admin 行では意味を持ちません。"
|
|
78
|
-
},
|
|
79
|
-
created_at: {
|
|
80
|
-
label: "作成日時"
|
|
81
|
-
},
|
|
82
|
-
updated_at: {
|
|
83
|
-
label: "更新日時"
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
_views: {
|
|
87
|
-
active: {
|
|
88
|
-
label: "有効"
|
|
89
|
-
},
|
|
90
|
-
inactive: {
|
|
91
|
-
label: "無効"
|
|
92
|
-
},
|
|
93
|
-
by_object: {
|
|
94
|
-
label: "オブジェクト別"
|
|
95
|
-
},
|
|
96
|
-
all_webhooks: {
|
|
97
|
-
label: "すべて"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Auto-generated by 'os i18n extract' for locale 'zh-CN'.
|
|
5
|
-
* Edit translations in place; re-run extract (with --merge) to fill new gaps.
|
|
6
|
-
* Do not hand-edit the structure — only the leaf string values.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { TranslationData } from '@objectstack/spec/system';
|
|
10
|
-
|
|
11
|
-
export const zhCNObjects: NonNullable<TranslationData['objects']> = {
|
|
12
|
-
sys_webhook: {
|
|
13
|
-
label: "Webhook",
|
|
14
|
-
pluralLabel: "Webhook",
|
|
15
|
-
description: "外发 HTTP Webhook 订阅。可在代码中通过 defineWebhook() 编写,或在 Studio 编辑器中维护;由 HTTP 连接器插件执行。",
|
|
16
|
-
fields: {
|
|
17
|
-
id: {
|
|
18
|
-
label: "Webhook ID"
|
|
19
|
-
},
|
|
20
|
-
name: {
|
|
21
|
-
label: "名称",
|
|
22
|
-
help: "唯一的 snake_case 名称——用于日志和审计引用"
|
|
23
|
-
},
|
|
24
|
-
label: {
|
|
25
|
-
label: "显示标签"
|
|
26
|
-
},
|
|
27
|
-
object_name: {
|
|
28
|
-
label: "对象",
|
|
29
|
-
help: "触发该 Webhook 的记录事件(create/update/delete)的短对象名"
|
|
30
|
-
},
|
|
31
|
-
triggers: {
|
|
32
|
-
label: "触发器",
|
|
33
|
-
help: "以逗号分隔的事件列表:create,update,delete",
|
|
34
|
-
options: {
|
|
35
|
-
create: "创建",
|
|
36
|
-
update: "更新",
|
|
37
|
-
delete: "删除"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
url: {
|
|
41
|
-
label: "目标 URL",
|
|
42
|
-
help: "接收 POST 请求的外部端点"
|
|
43
|
-
},
|
|
44
|
-
method: {
|
|
45
|
-
label: "HTTP 方法",
|
|
46
|
-
help: "GET / POST / PUT / PATCH / DELETE",
|
|
47
|
-
options: {
|
|
48
|
-
get: "GET",
|
|
49
|
-
post: "POST",
|
|
50
|
-
put: "PUT",
|
|
51
|
-
patch: "PATCH",
|
|
52
|
-
delete: "DELETE"
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
description: {
|
|
56
|
-
label: "描述"
|
|
57
|
-
},
|
|
58
|
-
active: {
|
|
59
|
-
label: "启用",
|
|
60
|
-
help: "停用的 Webhook 会被调度器跳过"
|
|
61
|
-
},
|
|
62
|
-
definition_json: {
|
|
63
|
-
label: "定义",
|
|
64
|
-
help: "序列化的 Webhook JSON(参见 @objectstack/spec/automation/webhook)——包含完整的 headers/auth/retry/payload 配置"
|
|
65
|
-
},
|
|
66
|
-
managed_by: {
|
|
67
|
-
label: "管理来源",
|
|
68
|
-
help: "记录来源:platform = 框架内置 / package = 应用包声明(由 defineStack 的 webhooks 在启动时种入)/ admin = 在 Setup 中创建。",
|
|
69
|
-
options: {
|
|
70
|
-
platform: "平台",
|
|
71
|
-
package: "包",
|
|
72
|
-
admin: "管理员"
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
customized: {
|
|
76
|
-
label: "已定制",
|
|
77
|
-
help: "当管理员编辑应用包声明的 webhook 时置位;此后启动种入不再覆盖该行(被停用的嘈杂 webhook 可跨重新部署保留)。对 admin 来源的记录无意义。"
|
|
78
|
-
},
|
|
79
|
-
created_at: {
|
|
80
|
-
label: "创建时间"
|
|
81
|
-
},
|
|
82
|
-
updated_at: {
|
|
83
|
-
label: "更新时间"
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
_views: {
|
|
87
|
-
active: {
|
|
88
|
-
label: "启用"
|
|
89
|
-
},
|
|
90
|
-
inactive: {
|
|
91
|
-
label: "停用"
|
|
92
|
-
},
|
|
93
|
-
by_object: {
|
|
94
|
-
label: "按对象"
|
|
95
|
-
},
|
|
96
|
-
all_webhooks: {
|
|
97
|
-
label: "全部"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import type { Plugin, PluginContext } from '@objectstack/core';
|
|
4
|
-
import type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';
|
|
5
|
-
import type { EnqueueHttpInput } from '@objectstack/service-messaging';
|
|
6
|
-
import { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';
|
|
7
|
-
import { SysWebhook } from './sys-webhook.object.js';
|
|
8
|
-
import { bootstrapDeclaredWebhooks } from './bootstrap-declared-webhooks.js';
|
|
9
|
-
import { bindWebhookProvenanceStamp, unbindWebhookProvenanceStamp } from './webhook-provenance.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface
|
|
13
|
-
* (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime
|
|
14
|
-
* import on the service. Webhook deliveries are enqueued onto the shared
|
|
15
|
-
* `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.
|
|
16
|
-
*/
|
|
17
|
-
interface MessagingHttpSurface {
|
|
18
|
-
isHttpDeliveryReady(): boolean;
|
|
19
|
-
enqueueHttp(input: EnqueueHttpInput): Promise<string>;
|
|
20
|
-
redeliverHttp(id: string): Promise<{ id: string; status: string }>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface WebhookOutboxPluginOptions {
|
|
24
|
-
/**
|
|
25
|
-
* Auto-enqueue config. When enabled (default `true` if the realtime + data
|
|
26
|
-
* engine services are available), the plugin subscribes to `data.record.*`
|
|
27
|
-
* events and enqueues a delivery onto the shared messaging HTTP outbox for
|
|
28
|
-
* every matching `sys_webhook` row.
|
|
29
|
-
*
|
|
30
|
-
* Set `false` to disable and enqueue webhooks imperatively elsewhere.
|
|
31
|
-
*/
|
|
32
|
-
autoEnqueue?: boolean | AutoEnqueuerOptions;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate
|
|
37
|
-
* (ADR-0018 M3).
|
|
38
|
-
*
|
|
39
|
-
* Webhooks are no longer their own delivery engine: the durable outbox, the
|
|
40
|
-
* cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and
|
|
41
|
-
* the retention sweep all live in `@objectstack/service-messaging`
|
|
42
|
-
* (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the
|
|
43
|
-
* webhook-specific concerns:
|
|
44
|
-
* - the `sys_webhook` configuration object,
|
|
45
|
-
* - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox
|
|
46
|
-
* rows (`source: 'webhook'`), and
|
|
47
|
-
* - the redeliver admin endpoint.
|
|
48
|
-
*
|
|
49
|
-
* End-to-end flow:
|
|
50
|
-
*
|
|
51
|
-
* engine.insert('contact', {...})
|
|
52
|
-
* → engine publishes data.record.created via IRealtimeService
|
|
53
|
-
* → AutoEnqueuer matches active sys_webhook rows in O(1)
|
|
54
|
-
* → messaging.enqueueHttp() runs fire-and-forget (off the write path)
|
|
55
|
-
* → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)
|
|
56
|
-
*
|
|
57
|
-
* **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),
|
|
58
|
-
* which is a foundational, always-on capability.
|
|
59
|
-
*/
|
|
60
|
-
export class WebhookOutboxPlugin implements Plugin {
|
|
61
|
-
name = 'com.objectstack.plugin-webhook-outbox';
|
|
62
|
-
version = '2.0.0';
|
|
63
|
-
type = 'standard' as const;
|
|
64
|
-
dependencies = ['com.objectstack.service.messaging'];
|
|
65
|
-
|
|
66
|
-
private autoEnqueuer: AutoEnqueuer | undefined;
|
|
67
|
-
/** Engine the provenance hook was bound to, so `dispose()` can unbind it. */
|
|
68
|
-
private boundEngine: any;
|
|
69
|
-
|
|
70
|
-
constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}
|
|
71
|
-
|
|
72
|
-
async init(ctx: PluginContext): Promise<void> {
|
|
73
|
-
// Register the webhook config object (ADR-0029 K2.a). The delivery
|
|
74
|
-
// telemetry now lives in messaging's `sys_http_delivery`, so the nav's
|
|
75
|
-
// "Deliveries" entry points there (filtered to source=webhook in views).
|
|
76
|
-
const manifest = ctx.getService<{ register(m: any): void }>('manifest');
|
|
77
|
-
if (manifest && typeof manifest.register === 'function') {
|
|
78
|
-
manifest.register({
|
|
79
|
-
id: 'com.objectstack.plugin-webhook-outbox.schema',
|
|
80
|
-
namespace: 'sys',
|
|
81
|
-
version: this.version,
|
|
82
|
-
type: 'plugin',
|
|
83
|
-
scope: 'system',
|
|
84
|
-
name: 'Webhook Schemas',
|
|
85
|
-
description: 'Registers sys_webhook (configuration). Deliveries use messaging\'s sys_http_delivery outbox.',
|
|
86
|
-
objects: [SysWebhook],
|
|
87
|
-
navigationContributions: [
|
|
88
|
-
{
|
|
89
|
-
app: 'setup',
|
|
90
|
-
group: 'group_integrations',
|
|
91
|
-
priority: 100,
|
|
92
|
-
items: [
|
|
93
|
-
{ id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },
|
|
94
|
-
{ id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },
|
|
95
|
-
],
|
|
96
|
-
},
|
|
97
|
-
],
|
|
98
|
-
});
|
|
99
|
-
} else {
|
|
100
|
-
ctx.logger.warn?.(
|
|
101
|
-
'[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ADR-0029 D8 — contribute object translations once i18n is up.
|
|
106
|
-
if (typeof (ctx as any).hook === 'function') {
|
|
107
|
-
(ctx as any).hook('kernel:ready', async () => {
|
|
108
|
-
try {
|
|
109
|
-
const i18n = ctx.getService<any>('i18n');
|
|
110
|
-
if (i18n && typeof i18n.loadTranslations === 'function') {
|
|
111
|
-
const { WebhooksTranslations } = await import('./translations/index.js');
|
|
112
|
-
for (const [locale, data] of Object.entries(WebhooksTranslations)) {
|
|
113
|
-
i18n.loadTranslations(locale, data as Record<string, unknown>);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
} catch { /* i18n optional */ }
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const autoEnqueueOpt = this.options.autoEnqueue ?? true;
|
|
121
|
-
|
|
122
|
-
if (typeof (ctx as any).hook === 'function') {
|
|
123
|
-
(ctx as any).hook('kernel:ready', async () => {
|
|
124
|
-
// Materialize declared webhooks FIRST — this only needs the data
|
|
125
|
-
// engine, so it must not be gated behind the auto-enqueue
|
|
126
|
-
// dispatch prerequisites (realtime + messaging). Otherwise a
|
|
127
|
-
// deployment without realtime would silently fail to materialize
|
|
128
|
-
// declared webhooks — the very no-op this bridge closes (#3461).
|
|
129
|
-
await this.bootDeclaredWebhooks(ctx);
|
|
130
|
-
await this.bootAutoEnqueue(ctx, autoEnqueueOpt);
|
|
131
|
-
this.registerAdminRoutes(ctx);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {
|
|
136
|
-
autoEnqueue: autoEnqueueOpt !== false,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async dispose(): Promise<void> {
|
|
141
|
-
await this.autoEnqueuer?.stop();
|
|
142
|
-
if (this.boundEngine) {
|
|
143
|
-
try { unbindWebhookProvenanceStamp(this.boundEngine); } catch { /* best effort */ }
|
|
144
|
-
this.boundEngine = undefined;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {
|
|
149
|
-
const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);
|
|
150
|
-
return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* [#3461] Bridge the declarative authoring surface to the dispatcher:
|
|
155
|
-
* materialize stack/connector-declared `webhook` metadata into `sys_webhook`
|
|
156
|
-
* rows so the auto-enqueuer (and the Studio UI) can see them.
|
|
157
|
-
*
|
|
158
|
-
* Gated on the DATA ENGINE alone — deliberately independent of the
|
|
159
|
-
* auto-enqueue dispatch prerequisites (realtime + messaging). Materializing
|
|
160
|
-
* rows is a pure write; a deployment that mounts the webhook plugin without
|
|
161
|
-
* realtime must still get its declared webhooks into the table (and the
|
|
162
|
-
* Setup UI), even if nothing dispatches them yet. Runs before
|
|
163
|
-
* {@link bootAutoEnqueue} so the enqueuer's first cache refresh sees the rows.
|
|
164
|
-
*/
|
|
165
|
-
private async bootDeclaredWebhooks(ctx: PluginContext): Promise<void> {
|
|
166
|
-
const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);
|
|
167
|
-
if (!engine) {
|
|
168
|
-
ctx.logger.warn?.('[webhook] declared-webhook bootstrap skipped — no data engine available');
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
// Bind the provenance stamp so an admin edit freezes a seeded row.
|
|
172
|
-
this.boundEngine = engine;
|
|
173
|
-
bindWebhookProvenanceStamp(engine as any, ctx.logger as any);
|
|
174
|
-
let metadataService: any;
|
|
175
|
-
try { metadataService = ctx.getService('metadata'); } catch { /* optional */ }
|
|
176
|
-
try {
|
|
177
|
-
await bootstrapDeclaredWebhooks(engine, metadataService, ctx.logger as any);
|
|
178
|
-
} catch (err: any) {
|
|
179
|
-
ctx.logger.warn?.('[webhook] declared-webhook bootstrap failed (dispatcher still serves admin rows)', {
|
|
180
|
-
error: err?.message ?? String(err),
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private async bootAutoEnqueue(
|
|
186
|
-
ctx: PluginContext,
|
|
187
|
-
opt: boolean | AutoEnqueuerOptions,
|
|
188
|
-
): Promise<void> {
|
|
189
|
-
if (opt === false) return;
|
|
190
|
-
const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);
|
|
191
|
-
const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);
|
|
192
|
-
const messaging = this.getMessaging(ctx);
|
|
193
|
-
if (!engine || !realtime || !messaging) {
|
|
194
|
-
ctx.logger.warn?.(
|
|
195
|
-
'[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',
|
|
196
|
-
{ hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },
|
|
197
|
-
);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (!messaging.isHttpDeliveryReady()) {
|
|
201
|
-
ctx.logger.warn?.(
|
|
202
|
-
'[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;
|
|
207
|
-
this.autoEnqueuer = new AutoEnqueuer(
|
|
208
|
-
engine,
|
|
209
|
-
realtime,
|
|
210
|
-
(input) => messaging.enqueueHttp(input),
|
|
211
|
-
{ ...enqOpts, logger: ctx.logger },
|
|
212
|
-
);
|
|
213
|
-
await this.autoEnqueuer.start();
|
|
214
|
-
ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);
|
|
215
|
-
ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {
|
|
219
|
-
for (const n of names) {
|
|
220
|
-
try {
|
|
221
|
-
const svc = ctx.getService<T>(n);
|
|
222
|
-
if (svc) return svc;
|
|
223
|
-
} catch {
|
|
224
|
-
// fall through
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return undefined;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is
|
|
232
|
-
* available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the
|
|
233
|
-
* better-auth session cookie — every authenticated user counts.
|
|
234
|
-
*/
|
|
235
|
-
private registerAdminRoutes(ctx: PluginContext): void {
|
|
236
|
-
const http = this.tryGetService<any>(ctx, ['http-server']);
|
|
237
|
-
if (!http || typeof http.getRawApp !== 'function') {
|
|
238
|
-
ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const rawApp = http.getRawApp();
|
|
242
|
-
const messaging = this.getMessaging(ctx);
|
|
243
|
-
if (!rawApp || !messaging) return;
|
|
244
|
-
|
|
245
|
-
rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {
|
|
246
|
-
const userId = await this.resolveSessionUserId(ctx, c);
|
|
247
|
-
if (!userId) {
|
|
248
|
-
return c.json(
|
|
249
|
-
{ success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },
|
|
250
|
-
401,
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
let body: any;
|
|
254
|
-
try {
|
|
255
|
-
body = await c.req.json();
|
|
256
|
-
} catch {
|
|
257
|
-
return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);
|
|
258
|
-
}
|
|
259
|
-
const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';
|
|
260
|
-
if (!deliveryId) {
|
|
261
|
-
return c.json(
|
|
262
|
-
{ success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },
|
|
263
|
-
400,
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
try {
|
|
267
|
-
const row = await messaging.redeliverHttp(deliveryId);
|
|
268
|
-
ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });
|
|
269
|
-
return c.json({ success: true, data: { id: row.id, status: row.status } });
|
|
270
|
-
} catch (err: any) {
|
|
271
|
-
const code = err?.code;
|
|
272
|
-
if (code === 'not_found') {
|
|
273
|
-
return c.json({ success: false, error: 'not_found', message: err.message }, 404);
|
|
274
|
-
}
|
|
275
|
-
if (code === 'not_eligible') {
|
|
276
|
-
return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);
|
|
277
|
-
}
|
|
278
|
-
ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);
|
|
279
|
-
return c.json(
|
|
280
|
-
{ success: false, error: 'internal_error', message: err?.message ?? String(err) },
|
|
281
|
-
500,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {
|
|
290
|
-
try {
|
|
291
|
-
const authService: any = this.tryGetService<any>(ctx, ['auth']);
|
|
292
|
-
if (!authService) return undefined;
|
|
293
|
-
let api: any = authService.api;
|
|
294
|
-
if (!api && typeof authService.getApi === 'function') {
|
|
295
|
-
api = await authService.getApi();
|
|
296
|
-
}
|
|
297
|
-
if (!api?.getSession) return undefined;
|
|
298
|
-
const session = await api.getSession({ headers: c.req.raw.headers });
|
|
299
|
-
const uid = session?.user?.id;
|
|
300
|
-
return typeof uid === 'string' && uid.length > 0 ? uid : undefined;
|
|
301
|
-
} catch {
|
|
302
|
-
return undefined;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|