@objectstack/plugin-webhooks 7.5.0 → 7.6.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.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +20 -32
  2. package/CHANGELOG.md +49 -0
  3. package/dist/chunk-HWFTXTTI.js +138 -0
  4. package/dist/chunk-HWFTXTTI.js.map +1 -0
  5. package/dist/chunk-KPKLAXNA.cjs +138 -0
  6. package/dist/chunk-KPKLAXNA.cjs.map +1 -0
  7. package/dist/index.cjs +62 -616
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +41 -325
  10. package/dist/index.d.ts +41 -325
  11. package/dist/index.js +52 -606
  12. package/dist/index.js.map +1 -1
  13. package/dist/schema.cjs +2 -6
  14. package/dist/schema.cjs.map +1 -1
  15. package/dist/schema.d.cts +5 -4764
  16. package/dist/schema.d.ts +5 -4764
  17. package/dist/schema.js +3 -7
  18. package/package.json +4 -11
  19. package/src/auto-enqueuer.test.ts +83 -116
  20. package/src/auto-enqueuer.ts +38 -27
  21. package/src/index.ts +13 -40
  22. package/src/schema.ts +11 -16
  23. package/src/webhook-outbox-plugin.ts +80 -296
  24. package/tsup.config.ts +1 -1
  25. package/dist/chunk-7HS5DLU2.js +0 -319
  26. package/dist/chunk-7HS5DLU2.js.map +0 -1
  27. package/dist/chunk-HF7CCDPB.cjs +0 -256
  28. package/dist/chunk-HF7CCDPB.cjs.map +0 -1
  29. package/dist/chunk-KNGLLSSP.js +0 -256
  30. package/dist/chunk-KNGLLSSP.js.map +0 -1
  31. package/dist/chunk-TDSI7UHY.cjs +0 -319
  32. package/dist/chunk-TDSI7UHY.cjs.map +0 -1
  33. package/dist/outbox-CIn7LSyB.d.cts +0 -155
  34. package/dist/outbox-CIn7LSyB.d.ts +0 -155
  35. package/dist/sql-outbox.cjs +0 -8
  36. package/dist/sql-outbox.cjs.map +0 -1
  37. package/dist/sql-outbox.d.cts +0 -55
  38. package/dist/sql-outbox.d.ts +0 -55
  39. package/dist/sql-outbox.js +0 -8
  40. package/dist/sql-outbox.js.map +0 -1
  41. package/src/dispatcher.test.ts +0 -324
  42. package/src/dispatcher.ts +0 -218
  43. package/src/http-sender.ts +0 -187
  44. package/src/memory-outbox.test.ts +0 -86
  45. package/src/memory-outbox.ts +0 -155
  46. package/src/outbox.ts +0 -175
  47. package/src/partition.ts +0 -19
  48. package/src/retention.test.ts +0 -116
  49. package/src/retention.ts +0 -144
  50. package/src/sql-outbox.test.ts +0 -490
  51. package/src/sql-outbox.ts +0 -343
  52. package/src/sys-webhook-delivery.object.ts +0 -224
@@ -1,129 +1,74 @@
1
1
  // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2
2
 
3
3
  import type { Plugin, PluginContext } from '@objectstack/core';
4
- import { readEnvWithDeprecation } from '@objectstack/types';
5
- import type {
6
- IClusterService,
7
- IDataEngine,
8
- IRealtimeService,
9
- } from '@objectstack/spec/contracts';
4
+ import type { IDataEngine, IRealtimeService } from '@objectstack/spec/contracts';
5
+ import type { EnqueueHttpInput } from '@objectstack/service-messaging';
10
6
  import { AutoEnqueuer, type AutoEnqueuerOptions } from './auto-enqueuer.js';
11
- import { WebhookDispatcher, type DispatcherOptions } from './dispatcher.js';
12
- import { MemoryWebhookOutbox } from './memory-outbox.js';
13
- import type { IWebhookOutbox } from './outbox.js';
14
- import {
15
- DeliveryRetentionSweeper,
16
- type DeliveryRetentionOptions,
17
- } from './retention.js';
18
- import { SqlWebhookOutbox } from './sql-outbox.js';
19
7
  import { SysWebhook } from './sys-webhook.object.js';
20
- import { SysWebhookDelivery } from './sys-webhook-delivery.object.js';
21
8
 
22
- export interface WebhookOutboxPluginOptions
23
- extends Partial<Omit<DispatcherOptions, 'cluster' | 'outbox' | 'nodeId'>> {
24
- /**
25
- * Override the outbox backend. If omitted a fresh `MemoryWebhookOutbox`
26
- * is used fine for local development, **not for production**: each
27
- * node will see only its own rows.
28
- *
29
- * Pass a factory if you need the kernel-resolved `IDataEngine`:
30
- *
31
- * ```ts
32
- * outbox: (ctx) => new SqlWebhookOutbox(
33
- * ctx.getService('objectql'), { partitionCount: 8 },
34
- * ),
35
- * ```
36
- */
37
- outbox?: IWebhookOutbox | ((ctx: PluginContext) => IWebhookOutbox);
38
-
39
- /**
40
- * Stable node id. If omitted, uses `process.env.OS_NODE_ID`
41
- * (legacy `OBJECTSTACK_NODE_ID` still honoured with deprecation warning)
42
- * or a random UUID generated at plugin init.
43
- */
44
- nodeId?: string;
45
-
46
- /**
47
- * If `false`, the plugin registers the outbox/dispatcher services but
48
- * does NOT auto-start the loop — useful for tests that want to step
49
- * the dispatcher manually via `dispatcher.tick()`.
50
- *
51
- * Default: true.
52
- */
53
- autoStart?: boolean;
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
+ }
54
20
 
21
+ export interface WebhookOutboxPluginOptions {
55
22
  /**
56
- * Auto-enqueue config. When enabled (default `true` if the realtime
57
- * + data engine services are available), the plugin subscribes to
58
- * `data.record.*` events emitted by the engine and automatically
59
- * enqueues a delivery row for every matching `sys_webhook` row.
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.
60
27
  *
61
- * Set `false` to disable and only use the imperative
62
- * `outbox.enqueue()` API.
28
+ * Set `false` to disable and enqueue webhooks imperatively elsewhere.
63
29
  */
64
30
  autoEnqueue?: boolean | AutoEnqueuerOptions;
65
-
66
- /**
67
- * Retention sweep config. When enabled (default `true` if a SQL
68
- * outbox is in use), a periodic timer prunes old `success` and
69
- * `dead` rows from `sys_webhook_delivery`.
70
- *
71
- * Set `false` to disable (e.g. when using `MemoryWebhookOutbox`).
72
- */
73
- retention?: boolean | DeliveryRetentionOptions;
74
31
  }
75
32
 
76
33
  /**
77
- * Wires a persistent, cluster-aware webhook outbox into the kernel.
34
+ * Wires webhook fan-out on top of the shared outbound-HTTP delivery substrate
35
+ * (ADR-0018 M3).
78
36
  *
79
- * Registered services:
80
- * - `webhook.outbox` → `IWebhookOutbox` (enqueue / claim / ack / list)
81
- * - `webhook.dispatcher` `WebhookDispatcher` (manual `tick()` if needed)
82
- * - `webhook.autoEnqueuer` `AutoEnqueuer` when auto-enqueue is on
83
- * - `webhook.retention` → `DeliveryRetentionSweeper` when retention is on
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.
84
46
  *
85
- * End-to-end flow once auto-enqueue is enabled:
47
+ * End-to-end flow:
86
48
  *
87
49
  * engine.insert('contact', {...})
88
50
  * → engine publishes data.record.created via IRealtimeService
89
51
  * → AutoEnqueuer matches active sys_webhook rows in O(1)
90
- * → outbox.enqueue() runs fire-and-forget (not on the write path)
91
- * → dispatcher claims and POSTs (cluster-coordinated)
52
+ * → messaging.enqueueHttp() runs fire-and-forget (off the write path)
53
+ * → messaging HttpDispatcher claims and POSTs (cluster-coordinated, retried)
92
54
  *
93
- * **Cluster requirement** this plugin depends on the cluster service
94
- * (`ClusterServicePlugin`). With the default `memory` driver the
95
- * dispatcher works correctly inside a single process; with a real driver
96
- * (`@objectstack/service-cluster-redis`) it correctly coordinates work
97
- * across nodes.
55
+ * **Requires** `MessagingServicePlugin` (`@objectstack/service-messaging`),
56
+ * which is a foundational, always-on capability.
98
57
  */
99
58
  export class WebhookOutboxPlugin implements Plugin {
100
59
  name = 'com.objectstack.plugin-webhook-outbox';
101
- version = '1.1.0';
60
+ version = '2.0.0';
102
61
  type = 'standard' as const;
103
- dependencies = ['com.objectstack.service.cluster'];
62
+ dependencies = ['com.objectstack.service.messaging'];
104
63
 
105
- private dispatcher: WebhookDispatcher | undefined;
106
64
  private autoEnqueuer: AutoEnqueuer | undefined;
107
- private retention: DeliveryRetentionSweeper | undefined;
108
- private outboxInstance: IWebhookOutbox | undefined;
109
65
 
110
66
  constructor(private readonly options: WebhookOutboxPluginOptions = {}) {}
111
67
 
112
68
  async init(ctx: PluginContext): Promise<void> {
113
- const cluster = ctx.getService<IClusterService>('cluster');
114
- if (!cluster) {
115
- throw new Error(
116
- 'WebhookOutboxPlugin: required service "cluster" not found — register ClusterServicePlugin first',
117
- );
118
- }
119
-
120
- // Register the schemas this plugin owns at runtime (ADR-0029 K2.a).
121
- // Both `sys_webhook` (config) and `sys_webhook_delivery` (telemetry)
122
- // are now defined and owned here — the webhook plugin ships its data
123
- // model and behavior as one unit instead of importing `sys_webhook`
124
- // from the @objectstack/platform-objects monolith. Registering them
125
- // here means a stack just needs `plugins: [new WebhookOutboxPlugin(...)]`
126
- // and both objects auto-appear in REST/Studio/Setup nav.
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).
127
72
  const manifest = ctx.getService<{ register(m: any): void }>('manifest');
128
73
  if (manifest && typeof manifest.register === 'function') {
129
74
  manifest.register({
@@ -132,14 +77,9 @@ export class WebhookOutboxPlugin implements Plugin {
132
77
  version: this.version,
133
78
  type: 'plugin',
134
79
  scope: 'system',
135
- name: 'Webhook Outbox Schemas',
136
- description:
137
- 'Registers sys_webhook (configuration) and sys_webhook_delivery (durable outbox telemetry).',
138
- objects: [SysWebhook, SysWebhookDelivery],
139
- // ADR-0029 D7 — contribute the Webhooks entries into the
140
- // Setup app's `group_integrations` slot. The plugin owns these
141
- // objects (K2.a), so it ships their menu too; when the plugin
142
- // isn't installed the slot stays empty.
80
+ name: 'Webhook Schemas',
81
+ description: 'Registers sys_webhook (configuration). Deliveries use messaging\'s sys_http_delivery outbox.',
82
+ objects: [SysWebhook],
143
83
  navigationContributions: [
144
84
  {
145
85
  app: 'setup',
@@ -147,19 +87,18 @@ export class WebhookOutboxPlugin implements Plugin {
147
87
  priority: 100,
148
88
  items: [
149
89
  { id: 'nav_webhooks', type: 'object', label: 'Webhooks', objectName: 'sys_webhook', icon: 'webhook', requiresObject: 'sys_webhook' },
150
- { id: 'nav_webhook_deliveries', type: 'object', label: 'Webhook Deliveries', objectName: 'sys_webhook_delivery', icon: 'send', requiresObject: 'sys_webhook_delivery' },
90
+ { id: 'nav_http_deliveries', type: 'object', label: 'HTTP Deliveries', objectName: 'sys_http_delivery', icon: 'send', requiresObject: 'sys_http_delivery' },
151
91
  ],
152
92
  },
153
93
  ],
154
94
  });
155
95
  } else {
156
96
  ctx.logger.warn?.(
157
- '[webhook-outbox] manifest service unavailable — sys_webhook / sys_webhook_delivery will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',
97
+ '[webhook-outbox] manifest service unavailable — sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin.',
158
98
  );
159
99
  }
160
100
 
161
- // ADR-0029 D8 — contribute this plugin's object translations to the
162
- // i18n service on kernel:ready (the i18n plugin may register later).
101
+ // ADR-0029 D8 — contribute object translations once i18n is up.
163
102
  if (typeof (ctx as any).hook === 'function') {
164
103
  (ctx as any).hook('kernel:ready', async () => {
165
104
  try {
@@ -174,117 +113,27 @@ export class WebhookOutboxPlugin implements Plugin {
174
113
  });
175
114
  }
176
115
 
177
- const outbox = this.resolveOutbox(ctx);
178
- this.outboxInstance = outbox;
179
- const nodeId =
180
- this.options.nodeId ??
181
- readEnvWithDeprecation('OS_NODE_ID', 'OBJECTSTACK_NODE_ID') ??
182
- `node-${Math.random().toString(36).slice(2, 10)}`;
183
-
184
- const dispatcher = new WebhookDispatcher({
185
- nodeId,
186
- cluster,
187
- outbox,
188
- partitionCount: this.options.partitionCount,
189
- batchSize: this.options.batchSize,
190
- intervalMs: this.options.intervalMs,
191
- lockTtlMs: this.options.lockTtlMs,
192
- claimTtlMs: this.options.claimTtlMs,
193
- fetchImpl: this.options.fetchImpl,
194
- onAttempt: this.options.onAttempt,
195
- rng: this.options.rng,
196
- logger: ctx.logger,
197
- });
198
- this.dispatcher = dispatcher;
199
-
200
- ctx.registerService('webhook.outbox', outbox);
201
- ctx.registerService('webhook.dispatcher', dispatcher);
202
-
203
- if (this.options.autoStart !== false) {
204
- dispatcher.start();
205
- }
206
-
207
- // Loud warning when running with the in-memory outbox in production —
208
- // it loses data on restart and never shares rows across nodes. With
209
- // the auto-pick logic above this only fires when no IDataEngine is
210
- // available, but flag it loudly anyway.
211
- const usingMemoryOutbox = outbox instanceof MemoryWebhookOutbox;
212
- if (usingMemoryOutbox && process.env.NODE_ENV === 'production') {
213
- ctx.logger.warn?.(
214
- '[webhook-outbox] MemoryWebhookOutbox in production — webhook deliveries WILL be lost on process exit. Ensure ObjectQL is registered before WebhookOutboxPlugin so the SQL outbox can auto-select.',
215
- );
216
- }
217
-
218
- // Auto-enqueue + retention need the kernel to be fully ready
219
- // before ObjectQL / Realtime services are resolvable.
220
116
  const autoEnqueueOpt = this.options.autoEnqueue ?? true;
221
- const retentionOpt = this.options.retention ?? true;
222
117
 
223
- const needsReadyHook = autoEnqueueOpt !== false || retentionOpt !== false;
224
- if (needsReadyHook && typeof (ctx as any).hook === 'function') {
118
+ if (typeof (ctx as any).hook === 'function') {
225
119
  (ctx as any).hook('kernel:ready', async () => {
226
120
  await this.bootAutoEnqueue(ctx, autoEnqueueOpt);
227
- this.bootRetention(ctx, retentionOpt);
228
- });
229
- }
230
-
231
- // Admin REST endpoint — POST /api/v1/webhooks/redeliver { deliveryId }.
232
- // Wired in `kernel:ready` so the auth + http services are guaranteed
233
- // resolvable. Gated on a session cookie so anonymous callers cannot
234
- // replay deliveries; finer-grained RBAC (e.g. "only admins") can be
235
- // layered on later — for now any signed-in user with access to the
236
- // Setup app can redeliver. The action is also `disabled`-gated by
237
- // status on the Studio side so the button only lights up on
238
- // success / failed / dead rows.
239
- if (typeof (ctx as any).hook === 'function') {
240
- (ctx as any).hook('kernel:ready', () => {
241
121
  this.registerAdminRoutes(ctx);
242
122
  });
243
123
  }
244
124
 
245
- ctx.logger.info?.('[webhook-outbox] initialised', {
246
- nodeId,
247
- partitions: this.options.partitionCount ?? 8,
248
- interval: this.options.intervalMs ?? 250,
125
+ ctx.logger.info?.('[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)', {
249
126
  autoEnqueue: autoEnqueueOpt !== false,
250
- retention: retentionOpt !== false,
251
127
  });
252
128
  }
253
129
 
254
130
  async dispose(): Promise<void> {
255
131
  await this.autoEnqueuer?.stop();
256
- this.retention?.stop();
257
- await this.dispatcher?.stop();
258
132
  }
259
133
 
260
- private resolveOutbox(ctx: PluginContext): IWebhookOutbox {
261
- const opt = this.options.outbox;
262
- if (opt) {
263
- return typeof opt === 'function'
264
- ? (opt as (c: PluginContext) => IWebhookOutbox)(ctx)
265
- : opt;
266
- }
267
- // No explicit override — auto-pick the right backend for the host.
268
- // SqlWebhookOutbox needs an `IDataEngine`; if one is resolvable
269
- // (the usual case in CLI-served stacks), use it so durable rows
270
- // in `sys_webhook_delivery` actually round-trip through the
271
- // dispatcher and the redeliver REST endpoint. Memory is only a
272
- // last-resort fallback for tests / edge environments without an
273
- // engine.
274
- const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);
275
- if (engine) {
276
- const partitionCount = this.options.partitionCount ?? 8;
277
- const sql = new SqlWebhookOutbox(engine, { partitionCount });
278
- ctx.logger.info?.(
279
- '[webhook-outbox] auto-selected SqlWebhookOutbox (objectql engine detected)',
280
- { partitionCount },
281
- );
282
- return sql;
283
- }
284
- ctx.logger.warn?.(
285
- '[webhook-outbox] no IDataEngine available — falling back to MemoryWebhookOutbox. Deliveries will NOT survive process restart and the redeliver REST endpoint will not see rows written through ObjectQL.',
286
- );
287
- return new MemoryWebhookOutbox();
134
+ private getMessaging(ctx: PluginContext): MessagingHttpSurface | undefined {
135
+ const svc = this.tryGetService<MessagingHttpSurface>(ctx, ['messaging']);
136
+ return svc && typeof svc.enqueueHttp === 'function' ? svc : undefined;
288
137
  }
289
138
 
290
139
  private async bootAutoEnqueue(
@@ -294,49 +143,30 @@ export class WebhookOutboxPlugin implements Plugin {
294
143
  if (opt === false) return;
295
144
  const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);
296
145
  const realtime = this.tryGetService<IRealtimeService>(ctx, ['realtime']);
297
- if (!engine || !realtime) {
146
+ const messaging = this.getMessaging(ctx);
147
+ if (!engine || !realtime || !messaging) {
298
148
  ctx.logger.warn?.(
299
- '[webhook-auto-enqueuer] disabled — ObjectQL or Realtime service not available',
300
- { hasEngine: !!engine, hasRealtime: !!realtime },
149
+ '[webhook-auto-enqueuer] disabled — ObjectQL, Realtime, or Messaging service not available',
150
+ { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging },
301
151
  );
302
152
  return;
303
153
  }
304
- if (!this.outboxInstance) return;
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
+ }
305
159
 
306
160
  const enqOpts = (typeof opt === 'object' ? opt : {}) as AutoEnqueuerOptions;
307
161
  this.autoEnqueuer = new AutoEnqueuer(
308
162
  engine,
309
163
  realtime,
310
- this.outboxInstance,
164
+ (input) => messaging.enqueueHttp(input),
311
165
  { ...enqOpts, logger: ctx.logger },
312
166
  );
313
167
  await this.autoEnqueuer.start();
314
168
  ctx.registerService('webhook.autoEnqueuer', this.autoEnqueuer);
315
- ctx.logger.info?.('[webhook-auto-enqueuer] started');
316
- }
317
-
318
- private bootRetention(
319
- ctx: PluginContext,
320
- opt: boolean | DeliveryRetentionOptions,
321
- ): void {
322
- if (opt === false) return;
323
- // Only meaningful for SQL outbox — Memory has its own (process-lifetime) GC.
324
- if (this.outboxInstance instanceof MemoryWebhookOutbox) return;
325
- const engine = this.tryGetService<IDataEngine>(ctx, ['objectql', 'data']);
326
- if (!engine) {
327
- ctx.logger.warn?.(
328
- '[webhook-retention] disabled — ObjectQL service not available',
329
- );
330
- return;
331
- }
332
- const retOpts = (typeof opt === 'object' ? opt : {}) as DeliveryRetentionOptions;
333
- this.retention = new DeliveryRetentionSweeper(engine, {
334
- ...retOpts,
335
- logger: ctx.logger,
336
- });
337
- this.retention.start();
338
- ctx.registerService('webhook.retention', this.retention);
339
- ctx.logger.info?.('[webhook-retention] sweeper started');
169
+ ctx.logger.info?.('[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)');
340
170
  }
341
171
 
342
172
  private tryGetService<T>(ctx: PluginContext, names: string[]): T | undefined {
@@ -352,33 +182,25 @@ export class WebhookOutboxPlugin implements Plugin {
352
182
  }
353
183
 
354
184
  /**
355
- * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one
356
- * is available. Silently no-ops in environments without an HTTP
357
- * server (MSW, edge tests, pure library use). Auth is delegated to
358
- * the better-auth session cookie — every authenticated user counts.
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.
359
188
  */
360
189
  private registerAdminRoutes(ctx: PluginContext): void {
361
190
  const http = this.tryGetService<any>(ctx, ['http-server']);
362
191
  if (!http || typeof http.getRawApp !== 'function') {
363
- ctx.logger.debug?.(
364
- '[webhook-outbox] HTTP server not available; redeliver REST endpoint not mounted',
365
- );
192
+ ctx.logger.debug?.('[webhook-outbox] HTTP server not available; redeliver endpoint not mounted');
366
193
  return;
367
194
  }
368
195
  const rawApp = http.getRawApp();
369
- const outbox = this.outboxInstance;
370
- if (!rawApp || !outbox) return;
196
+ const messaging = this.getMessaging(ctx);
197
+ if (!rawApp || !messaging) return;
371
198
 
372
199
  rawApp.post('/api/v1/webhooks/redeliver', async (c: any) => {
373
- // Auth gate — require a signed-in session.
374
200
  const userId = await this.resolveSessionUserId(ctx, c);
375
201
  if (!userId) {
376
202
  return c.json(
377
- {
378
- success: false,
379
- error: 'unauthenticated',
380
- message: 'Sign in to redeliver webhook deliveries.',
381
- },
203
+ { success: false, error: 'unauthenticated', message: 'Sign in to redeliver webhook deliveries.' },
382
204
  401,
383
205
  );
384
206
  }
@@ -386,77 +208,39 @@ export class WebhookOutboxPlugin implements Plugin {
386
208
  try {
387
209
  body = await c.req.json();
388
210
  } catch {
389
- return c.json(
390
- {
391
- success: false,
392
- error: 'invalid_body',
393
- message: 'Request body must be JSON.',
394
- },
395
- 400,
396
- );
211
+ return c.json({ success: false, error: 'invalid_body', message: 'Request body must be JSON.' }, 400);
397
212
  }
398
- const deliveryId =
399
- typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';
213
+ const deliveryId = typeof body?.deliveryId === 'string' ? body.deliveryId.trim() : '';
400
214
  if (!deliveryId) {
401
215
  return c.json(
402
- {
403
- success: false,
404
- error: 'missing_delivery_id',
405
- message: 'Body must include `deliveryId: string`.',
406
- },
216
+ { success: false, error: 'missing_delivery_id', message: 'Body must include `deliveryId: string`.' },
407
217
  400,
408
218
  );
409
219
  }
410
220
  try {
411
- const row = await outbox.redeliver(deliveryId);
412
- ctx.logger.info?.('[webhook-outbox] redelivered', {
413
- deliveryId,
414
- requestedBy: userId,
415
- });
221
+ const row = await messaging.redeliverHttp(deliveryId);
222
+ ctx.logger.info?.('[webhook-outbox] redelivered', { deliveryId, requestedBy: userId });
416
223
  return c.json({ success: true, data: { id: row.id, status: row.status } });
417
224
  } catch (err: any) {
418
225
  const code = err?.code;
419
226
  if (code === 'not_found') {
420
- return c.json(
421
- { success: false, error: 'not_found', message: err.message },
422
- 404,
423
- );
227
+ return c.json({ success: false, error: 'not_found', message: err.message }, 404);
424
228
  }
425
229
  if (code === 'not_eligible') {
426
- return c.json(
427
- { success: false, error: 'not_eligible', message: err.message },
428
- 409,
429
- );
230
+ return c.json({ success: false, error: 'not_eligible', message: err.message }, 409);
430
231
  }
431
- ctx.logger.error?.(
432
- '[webhook-outbox] redeliver failed',
433
- err as Error,
434
- );
232
+ ctx.logger.error?.('[webhook-outbox] redeliver failed', err as Error);
435
233
  return c.json(
436
- {
437
- success: false,
438
- error: 'internal_error',
439
- message: err?.message ?? String(err),
440
- },
234
+ { success: false, error: 'internal_error', message: err?.message ?? String(err) },
441
235
  500,
442
236
  );
443
237
  }
444
238
  });
445
239
 
446
- ctx.logger.info?.(
447
- '[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver',
448
- );
240
+ ctx.logger.info?.('[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver');
449
241
  }
450
242
 
451
- /**
452
- * Resolve the requesting user's id from a better-auth session cookie.
453
- * Returns `undefined` for anonymous callers — the caller decides
454
- * whether that's a 401.
455
- */
456
- private async resolveSessionUserId(
457
- ctx: PluginContext,
458
- c: any,
459
- ): Promise<string | undefined> {
243
+ private async resolveSessionUserId(ctx: PluginContext, c: any): Promise<string | undefined> {
460
244
  try {
461
245
  const authService: any = this.tryGetService<any>(ctx, ['auth']);
462
246
  if (!authService) return undefined;
package/tsup.config.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { defineConfig } from 'tsup';
4
4
 
5
5
  export default defineConfig({
6
- entry: ['src/index.ts', 'src/sql-outbox.ts', 'src/schema.ts'],
6
+ entry: ['src/index.ts', 'src/schema.ts'],
7
7
  splitting: true,
8
8
  sourcemap: true,
9
9
  clean: true,