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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +626 -0
  2. package/dist/{chunk-RBCYKJVL.js → chunk-6EPMRZ7I.js} +28 -2
  3. package/dist/chunk-6EPMRZ7I.js.map +1 -0
  4. package/dist/{chunk-PTMJ5BLL.cjs → chunk-QUTQSOQC.cjs} +28 -2
  5. package/dist/chunk-QUTQSOQC.cjs.map +1 -0
  6. package/dist/index.cjs +250 -33
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +25 -0
  9. package/dist/index.d.ts +25 -0
  10. package/dist/index.js +231 -14
  11. package/dist/index.js.map +1 -1
  12. package/dist/schema.cjs +2 -2
  13. package/dist/schema.d.cts +515 -304
  14. package/dist/schema.d.ts +515 -304
  15. package/dist/schema.js +1 -1
  16. package/dist/translations-CBQRUIS5.cjs +383 -0
  17. package/dist/translations-CBQRUIS5.cjs.map +1 -0
  18. package/dist/translations-Y3CXWOTM.js +383 -0
  19. package/dist/translations-Y3CXWOTM.js.map +1 -0
  20. package/package.json +11 -5
  21. package/.turbo/turbo-build.log +0 -36
  22. package/dist/chunk-PTMJ5BLL.cjs.map +0 -1
  23. package/dist/chunk-RBCYKJVL.js.map +0 -1
  24. package/dist/translations-EPJYANXJ.js +0 -727
  25. package/dist/translations-EPJYANXJ.js.map +0 -1
  26. package/dist/translations-L2JAQDJB.cjs +0 -727
  27. package/dist/translations-L2JAQDJB.cjs.map +0 -1
  28. package/scripts/i18n-extract.config.ts +0 -32
  29. package/src/auto-enqueuer.test.ts +0 -431
  30. package/src/auto-enqueuer.ts +0 -383
  31. package/src/index.ts +0 -29
  32. package/src/schema.ts +0 -21
  33. package/src/sys-webhook.object.ts +0 -193
  34. package/src/translations/en.objects.generated.ts +0 -187
  35. package/src/translations/es-ES.objects.generated.ts +0 -187
  36. package/src/translations/index.ts +0 -23
  37. package/src/translations/ja-JP.objects.generated.ts +0 -187
  38. package/src/translations/zh-CN.objects.generated.ts +0 -187
  39. package/src/webhook-outbox-plugin.ts +0 -259
  40. package/tsconfig.json +0 -10
  41. package/tsup.config.ts +0 -14
@@ -1,187 +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
- },
35
- url: {
36
- label: "目标 URL",
37
- help: "接收 POST 请求的外部端点"
38
- },
39
- method: {
40
- label: "HTTP 方法",
41
- help: "GET / POST / PUT / PATCH / DELETE"
42
- },
43
- description: {
44
- label: "描述"
45
- },
46
- active: {
47
- label: "启用",
48
- help: "停用的 Webhook 会被调度器跳过"
49
- },
50
- definition_json: {
51
- label: "定义",
52
- help: "序列化的 Webhook JSON(参见 @objectstack/spec/automation/webhook)——包含完整的 headers/auth/retry/payload 配置"
53
- },
54
- created_at: {
55
- label: "创建时间"
56
- },
57
- updated_at: {
58
- label: "更新时间"
59
- }
60
- },
61
- _views: {
62
- active: {
63
- label: "启用"
64
- },
65
- inactive: {
66
- label: "停用"
67
- },
68
- by_object: {
69
- label: "按对象"
70
- },
71
- all_webhooks: {
72
- label: "全部"
73
- }
74
- }
75
- },
76
- sys_webhook_delivery: {
77
- label: "Webhook Delivery",
78
- pluralLabel: "Webhook Deliveries",
79
- description: "Durable outbox row for one webhook attempt. Managed by @objectstack/plugin-webhooks; do not write directly.",
80
- fields: {
81
- id: {
82
- label: "Delivery ID",
83
- help: "UUID — also doubles as the receiver-side idempotency key"
84
- },
85
- webhook_id: {
86
- label: "Webhook ID",
87
- help: "FK to sys_webhook.id (loosely coupled — denormalised URL/secret on row)"
88
- },
89
- event_id: {
90
- label: "Event ID",
91
- help: "Source event id; UNIQUE(event_id, webhook_id) for dedup"
92
- },
93
- event_type: {
94
- label: "Event Type",
95
- help: "e.g. data.record.created"
96
- },
97
- url: {
98
- label: "Target URL",
99
- help: "Snapshotted at enqueue so config edits do not rewrite live rows"
100
- },
101
- method: {
102
- label: "Method"
103
- },
104
- headers_json: {
105
- label: "Headers JSON"
106
- },
107
- secret: {
108
- label: "HMAC Secret"
109
- },
110
- timeout_ms: {
111
- label: "Timeout (ms)"
112
- },
113
- payload_json: {
114
- label: "Payload JSON"
115
- },
116
- partition_key: {
117
- label: "Partition",
118
- help: "hash(webhook_id) mod partitionCount — precomputed for cheap WHERE"
119
- },
120
- status: {
121
- label: "Status",
122
- help: "pending | in_flight | success | failed | dead"
123
- },
124
- attempts: {
125
- label: "Attempts",
126
- help: "Number of POST attempts made so far"
127
- },
128
- claimed_by: {
129
- label: "Claimed By"
130
- },
131
- claimed_at: {
132
- label: "Claimed At (ms)"
133
- },
134
- next_retry_at: {
135
- label: "Next Retry At (ms)"
136
- },
137
- last_attempted_at: {
138
- label: "Last Attempted At (ms)"
139
- },
140
- response_code: {
141
- label: "HTTP Status"
142
- },
143
- response_body: {
144
- label: "Response Body (capped)"
145
- },
146
- error: {
147
- label: "Error"
148
- },
149
- created_at: {
150
- label: "Created At (ms)"
151
- },
152
- updated_at: {
153
- label: "Updated At (ms)"
154
- }
155
- },
156
- _views: {
157
- recent: {
158
- label: "Recent"
159
- },
160
- failures: {
161
- label: "Failures"
162
- },
163
- in_flight: {
164
- label: "In Flight"
165
- },
166
- pending: {
167
- label: "Pending"
168
- },
169
- by_status: {
170
- label: "By Status"
171
- },
172
- by_webhook: {
173
- label: "By Webhook"
174
- },
175
- all_deliveries: {
176
- label: "All"
177
- }
178
- },
179
- _actions: {
180
- redeliver: {
181
- label: "Redeliver",
182
- confirmText: "Replay this delivery? The receiver will get the original payload again — they must be idempotent on the X-Objectstack-Delivery header.",
183
- successMessage: "Queued for redelivery"
184
- }
185
- }
186
- }
187
- };
@@ -1,259 +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
-
9
- /**
10
- * Structural view of `@objectstack/service-messaging`'s HTTP-outbox surface
11
- * (ADR-0018 M3) — declared locally so this plugin doesn't take a hard runtime
12
- * import on the service. Webhook deliveries are enqueued onto the shared
13
- * `sys_http_delivery` outbox and drained by the messaging `HttpDispatcher`.
14
- */
15
- interface MessagingHttpSurface {
16
- isHttpDeliveryReady(): boolean;
17
- enqueueHttp(input: EnqueueHttpInput): Promise<string>;
18
- redeliverHttp(id: string): Promise<{ id: string; status: string }>;
19
- }
20
-
21
- export interface WebhookOutboxPluginOptions {
22
- /**
23
- * Auto-enqueue config. When enabled (default `true` if the realtime + data
24
- * engine services are available), the plugin subscribes to `data.record.*`
25
- * events and enqueues a delivery onto the shared messaging HTTP outbox for
26
- * every matching `sys_webhook` row.
27
- *
28
- * Set `false` to disable and enqueue webhooks imperatively elsewhere.
29
- */
30
- autoEnqueue?: boolean | AutoEnqueuerOptions;
31
- }
32
-
33
- /**
34
- * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate
35
- * (ADR-0018 M3).
36
- *
37
- * Webhooks are no longer their own delivery engine: the durable outbox, the
38
- * cluster-coordinated dispatcher, the retry/backoff/dead-letter schedule, and
39
- * the retention sweep all live in `@objectstack/service-messaging`
40
- * (`sys_http_delivery` + `HttpDispatcher`). This plugin owns only the
41
- * webhook-specific concerns:
42
- * - the `sys_webhook` configuration object,
43
- * - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox
44
- * rows (`source: 'webhook'`), and
45
- * - the redeliver admin endpoint.
46
- *
47
- * End-to-end flow:
48
- *
49
- * engine.insert('contact', {...})
50
- * → engine publishes data.record.created via IRealtimeService
51
- * → AutoEnqueuer matches active sys_webhook rows in O(1)
52
- * → messaging.enqueueHttp() runs fire-and-forget (off the write path)
53
- * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)
54
- *
55
- * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),
56
- * which is a foundational, always-on capability.
57
- */
58
- export class WebhookOutboxPlugin implements Plugin {
59
- name = 'com.objectstack.plugin-webhook-outbox';
60
- version = '2.0.0';
61
- type = 'standard' as const;
62
- dependencies = ['com.objectstack.service.messaging'];
63
-
64
- private autoEnqueuer: AutoEnqueuer | undefined;
65
-
66
- constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}
67
-
68
- async init(ctx: PluginContext): Promise<void> {
69
- // Register the webhook config object (ADR-0029 K2.a). The delivery
70
- // telemetry now lives in messaging's `sys_http_delivery`, so the nav's
71
- // "Deliveries" entry points there (filtered to source=webhook in views).
72
- const manifest = ctx.getService<{ register(m: any): void }>('manifest');
73
- if (manifest && typeof manifest.register === 'function') {
74
- manifest.register({
75
- id: 'com.objectstack.plugin-webhook-outbox.schema',
76
- namespace: 'sys',
77
- version: this.version,
78
- type: 'plugin',
79
- scope: 'system',
80
- name: 'Webhook Schemas',
81
- description: 'Registers sys_webhook (configuration). Deliveries use messaging\'s sys_http_delivery outbox.',
82
- objects: [SysWebhook],
83
- navigationContributions: [
84
- {
85
- app: 'setup',
86
- group: 'group_integrations',
87
- priority: 100,
88
- items: [
89
- { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },
90
- { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },
91
- ],
92
- },
93
- ],
94
- });
95
- } else {
96
- ctx.logger.warn?.(
97
- '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',
98
- );
99
- }
100
-
101
- // ADR-0029 D8 — contribute object translations once i18n is up.
102
- if (typeof (ctx as any).hook === 'function') {
103
- (ctx as any).hook('kernel:ready', async () => {
104
- try {
105
- const i18n = ctx.getService<any>('i18n');
106
- if (i18n && typeof i18n.loadTranslations === 'function') {
107
- const { WebhooksTranslations } = await import('./translations/index.js');
108
- for (const [locale, data] of Object.entries(WebhooksTranslations)) {
109
- i18n.loadTranslations(locale, data as Record<string, unknown>);
110
- }
111
- }
112
- } catch { /* i18n optional */ }
113
- });
114
- }
115
-
116
- const autoEnqueueOpt = this.options.autoEnqueue ?? true;
117
-
118
- if (typeof (ctx as any).hook === 'function') {
119
- (ctx as any).hook('kernel:ready', async () => {
120
- await this.bootAutoEnqueue(ctx, autoEnqueueOpt);
121
- this.registerAdminRoutes(ctx);
122
- });
123
- }
124
-
125
- ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {
126
- autoEnqueue: autoEnqueueOpt !== false,
127
- });
128
- }
129
-
130
- async dispose(): Promise<void> {
131
- await this.autoEnqueuer?.stop();
132
- }
133
-
134
- private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {
135
- const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);
136
- return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;
137
- }
138
-
139
- private async bootAutoEnqueue(
140
- ctx: PluginContext,
141
- opt: boolean | AutoEnqueuerOptions,
142
- ): Promise<void> {
143
- if (opt === false) return;
144
- const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);
145
- const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);
146
- const messaging = this.getMessaging(ctx);
147
- if (!engine || !realtime || !messaging) {
148
- ctx.logger.warn?.(
149
- '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',
150
- { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },
151
- );
152
- return;
153
- }
154
- if (!messaging.isHttpDeliveryReady()) {
155
- ctx.logger.warn?.(
156
- '[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) — webhook deliveries will not be durable',
157
- );
158
- }
159
-
160
- const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;
161
- this.autoEnqueuer = new AutoEnqueuer(
162
- engine,
163
- realtime,
164
- (input) => messaging.enqueueHttp(input),
165
- { ...enqOpts, logger: ctx.logger },
166
- );
167
- await this.autoEnqueuer.start();
168
- ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);
169
- ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');
170
- }
171
-
172
- private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {
173
- for (const n of names) {
174
- try {
175
- const svc = ctx.getService<T>(n);
176
- if (svc) return svc;
177
- } catch {
178
- // fall through
179
- }
180
- }
181
- return undefined;
182
- }
183
-
184
- /**
185
- * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is
186
- * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the
187
- * better-auth session cookie — every authenticated user counts.
188
- */
189
- private registerAdminRoutes(ctx: PluginContext): void {
190
- const http = this.tryGetService<any>(ctx, ['http-server']);
191
- if (!http || typeof http.getRawApp !== 'function') {
192
- ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');
193
- return;
194
- }
195
- const rawApp = http.getRawApp();
196
- const messaging = this.getMessaging(ctx);
197
- if (!rawApp || !messaging) return;
198
-
199
- rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {
200
- const userId = await this.resolveSessionUserId(ctx, c);
201
- if (!userId) {
202
- return c.json(
203
- { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },
204
- 401,
205
- );
206
- }
207
- let body: any;
208
- try {
209
- body = await c.req.json();
210
- } catch {
211
- return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);
212
- }
213
- const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';
214
- if (!deliveryId) {
215
- return c.json(
216
- { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },
217
- 400,
218
- );
219
- }
220
- try {
221
- const row = await messaging.redeliverHttp(deliveryId);
222
- ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });
223
- return c.json({ success: true, data: { id: row.id, status: row.status } });
224
- } catch (err: any) {
225
- const code = err?.code;
226
- if (code === 'not_found') {
227
- return c.json({ success: false, error: 'not_found', message: err.message }, 404);
228
- }
229
- if (code === 'not_eligible') {
230
- return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);
231
- }
232
- ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);
233
- return c.json(
234
- { success: false, error: 'internal_error', message: err?.message ?? String(err) },
235
- 500,
236
- );
237
- }
238
- });
239
-
240
- ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');
241
- }
242
-
243
- private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {
244
- try {
245
- const authService: any = this.tryGetService<any>(ctx, ['auth']);
246
- if (!authService) return undefined;
247
- let api: any = authService.api;
248
- if (!api && typeof authService.getApi === 'function') {
249
- api = await authService.getApi();
250
- }
251
- if (!api?.getSession) return undefined;
252
- const session = await api.getSession({ headers: c.req.raw.headers });
253
- const uid = session?.user?.id;
254
- return typeof uid === 'string' && uid.length > 0 ? uid : undefined;
255
- } catch {
256
- return undefined;
257
- }
258
- }
259
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "types": ["node"]
7
- },
8
- "include": ["src"],
9
- "exclude": ["node_modules", "dist"]
10
- }
package/tsup.config.ts DELETED
@@ -1,14 +0,0 @@
1
- // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { defineConfig } from 'tsup';
4
-
5
- export default defineConfig({
6
- entry: ['src/index.ts', 'src/schema.ts'],
7
- splitting: true,
8
- sourcemap: true,
9
- clean: true,
10
- dts: !process.env.OS_SKIP_DTS,
11
- format: ['esm', 'cjs'],
12
- target: 'es2020',
13
- external: ['vitest'],
14
- });