@objectstack/plugin-webhooks 7.5.0 → 7.7.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 +60 -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
package/dist/index.cjs CHANGED
@@ -1,23 +1,13 @@
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
-
4
-
5
- var _chunkHF7CCDPBcjs = require('./chunk-HF7CCDPB.cjs');
6
-
7
-
8
-
9
-
10
- var _chunkTDSI7UHYcjs = require('./chunk-TDSI7UHY.cjs');
11
-
12
- // src/webhook-outbox-plugin.ts
13
- var _types = require('@objectstack/types');
3
+ var _chunkKPKLAXNAcjs = require('./chunk-KPKLAXNA.cjs');
14
4
 
15
5
  // src/auto-enqueuer.ts
16
6
  var AutoEnqueuer = class {
17
- constructor(engine, realtime, outbox, opts = {}) {
7
+ constructor(engine, realtime, enqueue, opts = {}) {
18
8
  this.engine = engine;
19
9
  this.realtime = realtime;
20
- this.outbox = outbox;
10
+ this.enqueue = enqueue;
21
11
  this.subscriptions = /* @__PURE__ */ new Map();
22
12
  this.running = false;
23
13
  this.subscriptionsObject = _nullishCoalesce(opts.subscriptionsObject, () => ( "sys_webhook"));
@@ -112,7 +102,7 @@ var AutoEnqueuer = class {
112
102
  if (typeof row.definition_json === "string" && row.definition_json.length > 0) {
113
103
  try {
114
104
  defn = _nullishCoalesce(JSON.parse(row.definition_json), () => ( {}));
115
- } catch (e2) {
105
+ } catch (e) {
116
106
  defn = {};
117
107
  }
118
108
  }
@@ -152,14 +142,15 @@ var AutoEnqueuer = class {
152
142
  const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;
153
143
  for (const sub of subs) {
154
144
  if (!sub.triggers.has(trigger)) continue;
155
- void this.outbox.enqueue({
156
- webhookId: sub.id,
157
- eventId,
158
- eventType: event.type,
145
+ void this.enqueue({
146
+ source: "webhook",
147
+ refId: sub.id,
148
+ dedupKey: `${sub.id}:${eventId}`,
149
+ label: event.type,
159
150
  url: sub.url,
160
151
  method: sub.method,
161
152
  headers: sub.headers,
162
- secret: sub.secret,
153
+ signingSecret: sub.secret,
163
154
  timeoutMs: sub.timeoutMs,
164
155
  payload: {
165
156
  object: event.object,
@@ -204,436 +195,16 @@ function mapActionToTrigger(action) {
204
195
  }
205
196
  }
206
197
 
207
- // src/http-sender.ts
208
- var _crypto = require('crypto');
209
- var DEFAULT_TIMEOUT_MS = 15e3;
210
- var RESPONSE_BODY_CAP = 16 * 1024;
211
- async function sendOnce(delivery, fetchImpl) {
212
- const body = typeof delivery.payload === "string" ? delivery.payload : JSON.stringify(delivery.payload);
213
- const headers = {
214
- "Content-Type": "application/json",
215
- "User-Agent": "ObjectStack-Webhooks/1.0",
216
- "X-Objectstack-Event": delivery.eventType,
217
- "X-Objectstack-Delivery": delivery.id,
218
- "X-Objectstack-Attempt": String(delivery.attempts + 1),
219
- ..._nullishCoalesce(delivery.headers, () => ( {}))
220
- };
221
- if (delivery.secret) {
222
- const sig = _crypto.createHmac.call(void 0, "sha256", delivery.secret).update(body).digest("hex");
223
- headers["X-Objectstack-Signature"] = `sha256=${sig}`;
224
- }
225
- const timeoutMs = _nullishCoalesce(delivery.timeoutMs, () => ( DEFAULT_TIMEOUT_MS));
226
- const controller = new AbortController();
227
- const timer = setTimeout(() => controller.abort(), timeoutMs);
228
- const start = Date.now();
229
- try {
230
- const res = await fetchImpl(delivery.url, {
231
- method: _nullishCoalesce(delivery.method, () => ( "POST")),
232
- headers,
233
- body,
234
- signal: controller.signal
235
- });
236
- clearTimeout(timer);
237
- const responseText = await safeReadBody(res);
238
- const durationMs = Date.now() - start;
239
- if (res.ok) {
240
- return { success: true, httpStatus: res.status, responseBody: responseText, durationMs };
241
- }
242
- const retriable = res.status === 408 || res.status === 429 || res.status >= 500;
243
- return {
244
- success: false,
245
- retriable,
246
- httpStatus: res.status,
247
- responseBody: responseText,
248
- error: `HTTP ${res.status}`,
249
- durationMs
250
- };
251
- } catch (err) {
252
- clearTimeout(timer);
253
- const durationMs = Date.now() - start;
254
- const e = err;
255
- const error = _optionalChain([e, 'optionalAccess', _32 => _32.name]) === "AbortError" ? `timeout after ${timeoutMs}ms` : _nullishCoalesce(_optionalChain([e, 'optionalAccess', _33 => _33.message]), () => ( String(err)));
256
- return { success: false, retriable: true, error, durationMs };
257
- }
258
- }
259
- async function safeReadBody(res) {
260
- try {
261
- const text = await res.text();
262
- return text.length > RESPONSE_BODY_CAP ? text.slice(0, RESPONSE_BODY_CAP) : text;
263
- } catch (e3) {
264
- return void 0;
265
- }
266
- }
267
- function nextRetryDelayMs(attemptsSoFar, rng = Math.random) {
268
- const SCHEDULE = [1e3, 1e4, 6e4, 6e5, 36e5, 216e5, 864e5];
269
- if (attemptsSoFar < 1 || attemptsSoFar > SCHEDULE.length) return null;
270
- const base = SCHEDULE[attemptsSoFar - 1];
271
- const jitter = 0.8 + rng() * 0.4;
272
- return Math.floor(base * jitter);
273
- }
274
- function classifyAttempt(outcome, attemptsSoFar, now = Date.now(), rng) {
275
- if (outcome.success) return outcome;
276
- if (!outcome.retriable) {
277
- return {
278
- success: false,
279
- httpStatus: outcome.httpStatus,
280
- responseBody: outcome.responseBody,
281
- error: outcome.error,
282
- durationMs: outcome.durationMs,
283
- dead: true
284
- };
285
- }
286
- const delay = nextRetryDelayMs(attemptsSoFar + 1, rng);
287
- if (delay === null) {
288
- return {
289
- success: false,
290
- httpStatus: outcome.httpStatus,
291
- responseBody: outcome.responseBody,
292
- error: outcome.error,
293
- durationMs: outcome.durationMs,
294
- dead: true
295
- };
296
- }
297
- return {
298
- success: false,
299
- httpStatus: outcome.httpStatus,
300
- responseBody: outcome.responseBody,
301
- error: outcome.error,
302
- durationMs: outcome.durationMs,
303
- nextRetryAt: now + delay
304
- };
305
- }
306
-
307
- // src/dispatcher.ts
308
- var WebhookDispatcher = class {
309
- constructor(options) {
310
- this.running = false;
311
- const intervalMs = _nullishCoalesce(options.intervalMs, () => ( 250));
312
- const lockTtlMs = _nullishCoalesce(options.lockTtlMs, () => ( intervalMs * 5));
313
- this.opts = {
314
- nodeId: options.nodeId,
315
- cluster: options.cluster,
316
- outbox: options.outbox,
317
- partitionCount: _nullishCoalesce(options.partitionCount, () => ( 8)),
318
- batchSize: _nullishCoalesce(options.batchSize, () => ( 32)),
319
- intervalMs,
320
- lockTtlMs,
321
- claimTtlMs: _nullishCoalesce(options.claimTtlMs, () => ( lockTtlMs * 2)),
322
- onAttempt: options.onAttempt,
323
- fetchImpl: options.fetchImpl,
324
- rng: options.rng,
325
- logger: options.logger
326
- };
327
- }
328
- /** Begin the periodic loop. Safe to call once; subsequent calls are no-ops. */
329
- start() {
330
- if (this.running) return;
331
- this.running = true;
332
- this.scheduleTick();
333
- this.timer = setInterval(() => this.scheduleTick(), this.opts.intervalMs);
334
- }
335
- /** Stop the loop and wait for the in-flight tick to drain. */
336
- async stop() {
337
- if (!this.running) return;
338
- this.running = false;
339
- if (this.timer) {
340
- clearInterval(this.timer);
341
- this.timer = void 0;
342
- }
343
- if (this.inflightTick) {
344
- try {
345
- await this.inflightTick;
346
- } catch (e4) {
347
- }
348
- }
349
- }
350
- /**
351
- * Run one full tick (all partitions, single attempt each). Exposed for
352
- * deterministic tests that want to step the dispatcher manually.
353
- */
354
- async tick() {
355
- await this.runTick();
356
- }
357
- scheduleTick() {
358
- if (this.inflightTick) return;
359
- this.inflightTick = this.runTick().catch((err) => {
360
- _optionalChain([this, 'access', _34 => _34.opts, 'access', _35 => _35.logger, 'optionalAccess', _36 => _36.warn, 'optionalCall', _37 => _37("webhook-dispatcher: tick failed", {
361
- nodeId: this.opts.nodeId,
362
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _38 => _38.message]), () => ( String(err)))
363
- })]);
364
- }).finally(() => {
365
- this.inflightTick = void 0;
366
- });
367
- }
368
- async runTick() {
369
- const partitionCount = this.opts.partitionCount;
370
- const offset = stableNodeOffset(this.opts.nodeId, partitionCount);
371
- for (let step = 0; step < partitionCount; step++) {
372
- const i = (offset + step) % partitionCount;
373
- await this.runPartition(i);
374
- }
375
- }
376
- async runPartition(index) {
377
- const key = `webhook.dispatcher.partition.${index}`;
378
- const handle = await this.opts.cluster.lock.acquire(key, {
379
- ttlMs: this.opts.lockTtlMs,
380
- // waitMs=0 → fail-fast; we'll try this partition again next tick.
381
- waitMs: 0
382
- });
383
- if (!handle) return;
384
- try {
385
- const claimed = await this.opts.outbox.claim({
386
- nodeId: this.opts.nodeId,
387
- limit: this.opts.batchSize,
388
- partition: { index, count: this.opts.partitionCount },
389
- claimTtlMs: this.opts.claimTtlMs
390
- });
391
- if (claimed.length === 0) return;
392
- await handle.renew(this.opts.lockTtlMs);
393
- for (const row of claimed) {
394
- if (!handle.isHeld()) break;
395
- await this.processRow(row);
396
- }
397
- } finally {
398
- await handle.release();
399
- }
400
- }
401
- async processRow(row) {
402
- const fetchImpl = _nullishCoalesce(this.opts.fetchImpl, () => ( globalThis.fetch));
403
- if (!fetchImpl) {
404
- _optionalChain([this, 'access', _39 => _39.opts, 'access', _40 => _40.logger, 'optionalAccess', _41 => _41.warn, 'optionalCall', _42 => _42("webhook-dispatcher: no fetch impl available", {
405
- rowId: row.id
406
- })]);
407
- await this.opts.outbox.ack(row.id, {
408
- success: false,
409
- error: "no fetch implementation",
410
- durationMs: 0,
411
- dead: true
412
- });
413
- return;
414
- }
415
- const outcome = await sendOnce(row, fetchImpl);
416
- const result = classifyAttempt(outcome, row.attempts, Date.now(), this.opts.rng);
417
- await this.opts.outbox.ack(row.id, result);
418
- _optionalChain([this, 'access', _43 => _43.opts, 'access', _44 => _44.onAttempt, 'optionalCall', _45 => _45(row, result.success)]);
419
- }
420
- };
421
- function stableNodeOffset(nodeId, partitionCount) {
422
- let h = 0;
423
- for (let i = 0; i < nodeId.length; i++) {
424
- h = h * 31 + nodeId.charCodeAt(i) | 0;
425
- }
426
- return Math.abs(h) % partitionCount;
427
- }
428
-
429
- // src/memory-outbox.ts
430
-
431
- var MemoryWebhookOutbox = class {
432
- constructor() {
433
- this.rows = /* @__PURE__ */ new Map();
434
- /** Dedup index keyed by `${eventId}::${webhookId}` -> row id. */
435
- this.dedup = /* @__PURE__ */ new Map();
436
- }
437
- async enqueue(input) {
438
- const dedupKey = `${input.eventId}::${input.webhookId}`;
439
- const existing = this.dedup.get(dedupKey);
440
- if (existing) return existing;
441
- const id = _crypto.randomUUID.call(void 0, );
442
- const now = Date.now();
443
- const row = {
444
- id,
445
- webhookId: input.webhookId,
446
- eventId: input.eventId,
447
- eventType: input.eventType,
448
- url: input.url,
449
- method: _nullishCoalesce(input.method, () => ( "POST")),
450
- headers: input.headers,
451
- secret: input.secret,
452
- timeoutMs: input.timeoutMs,
453
- payload: input.payload,
454
- status: "pending",
455
- attempts: 0,
456
- createdAt: now,
457
- updatedAt: now
458
- };
459
- this.rows.set(id, row);
460
- this.dedup.set(dedupKey, id);
461
- return id;
462
- }
463
- async claim(opts) {
464
- const now = _nullishCoalesce(opts.now, () => ( Date.now()));
465
- const claimed = [];
466
- for (const row of this.rows.values()) {
467
- if (row.status === "in_flight" && row.claimedAt !== void 0 && now - row.claimedAt > opts.claimTtlMs) {
468
- row.status = "pending";
469
- row.claimedBy = void 0;
470
- row.claimedAt = void 0;
471
- row.updatedAt = now;
472
- }
473
- }
474
- for (const row of this.rows.values()) {
475
- if (claimed.length >= opts.limit) break;
476
- if (row.status !== "pending") continue;
477
- if (row.nextRetryAt !== void 0 && row.nextRetryAt > now) continue;
478
- if (opts.partition) {
479
- const p = _chunkHF7CCDPBcjs.hashPartition.call(void 0, row.webhookId, opts.partition.count);
480
- if (p !== opts.partition.index) continue;
481
- }
482
- row.status = "in_flight";
483
- row.claimedBy = opts.nodeId;
484
- row.claimedAt = now;
485
- row.updatedAt = now;
486
- claimed.push({ ...row });
487
- }
488
- return claimed;
489
- }
490
- async ack(id, result) {
491
- const row = this.rows.get(id);
492
- if (!row) return;
493
- const now = Date.now();
494
- row.attempts += 1;
495
- row.lastAttemptedAt = now;
496
- row.updatedAt = now;
497
- row.claimedBy = void 0;
498
- row.claimedAt = void 0;
499
- row.responseCode = result.httpStatus;
500
- row.responseBody = result.responseBody;
501
- let status;
502
- if (result.success) {
503
- status = "success";
504
- row.nextRetryAt = void 0;
505
- row.error = void 0;
506
- } else if (result.dead) {
507
- status = "dead";
508
- row.error = result.error;
509
- row.nextRetryAt = void 0;
510
- } else {
511
- status = "pending";
512
- row.error = result.error;
513
- row.nextRetryAt = result.nextRetryAt;
514
- }
515
- row.status = status;
516
- }
517
- async list(filter) {
518
- const all = Array.from(this.rows.values()).map((r) => ({ ...r }));
519
- return _optionalChain([filter, 'optionalAccess', _46 => _46.status]) ? all.filter((r) => r.status === filter.status) : all;
520
- }
521
- async redeliver(id) {
522
- const row = this.rows.get(id);
523
- if (!row) {
524
- throw new (0, _chunkHF7CCDPBcjs.RedeliverError)(
525
- `Delivery row '${id}' not found`,
526
- "not_found"
527
- );
528
- }
529
- if (row.status !== "success" && row.status !== "failed" && row.status !== "dead") {
530
- throw new (0, _chunkHF7CCDPBcjs.RedeliverError)(
531
- `Delivery row '${id}' is '${row.status}', expected one of: success, failed, dead`,
532
- "not_eligible"
533
- );
534
- }
535
- const now = Date.now();
536
- row.status = "pending";
537
- row.attempts = 0;
538
- row.claimedBy = void 0;
539
- row.claimedAt = void 0;
540
- row.nextRetryAt = void 0;
541
- row.error = void 0;
542
- row.responseCode = void 0;
543
- row.responseBody = void 0;
544
- row.updatedAt = now;
545
- return { ...row };
546
- }
547
- };
548
-
549
- // src/retention.ts
550
- var DEFAULTS = {
551
- successTtlMs: 7 * 24 * 60 * 60 * 1e3,
552
- deadTtlMs: 30 * 24 * 60 * 60 * 1e3,
553
- sweepIntervalMs: 60 * 60 * 1e3
554
- };
555
- var DeliveryRetentionSweeper = class {
556
- constructor(engine, opts = {}) {
557
- this.engine = engine;
558
- this.running = false;
559
- this.objectName = _nullishCoalesce(opts.objectName, () => ( _chunkTDSI7UHYcjs.SYS_WEBHOOK_DELIVERY));
560
- this.successTtlMs = _nullishCoalesce(opts.successTtlMs, () => ( DEFAULTS.successTtlMs));
561
- this.deadTtlMs = _nullishCoalesce(opts.deadTtlMs, () => ( DEFAULTS.deadTtlMs));
562
- this.sweepIntervalMs = _nullishCoalesce(opts.sweepIntervalMs, () => ( DEFAULTS.sweepIntervalMs));
563
- this.logger = _nullishCoalesce(opts.logger, () => ( {}));
564
- }
565
- start() {
566
- if (this.running) return;
567
- this.running = true;
568
- this.timer = setInterval(() => {
569
- this.sweep().catch(
570
- (err) => _optionalChain([this, 'access', _47 => _47.logger, 'access', _48 => _48.warn, 'optionalCall', _49 => _49("[webhook-retention] sweep failed", err)])
571
- );
572
- }, this.sweepIntervalMs);
573
- _optionalChain([this, 'access', _50 => _50.timer, 'access', _51 => _51.unref, 'optionalCall', _52 => _52()]);
574
- }
575
- stop() {
576
- if (!this.running) return;
577
- this.running = false;
578
- if (this.timer) clearInterval(this.timer);
579
- this.timer = void 0;
580
- }
581
- /** Run one sweep immediately. Returns the number of rows deleted. */
582
- async sweep(now = Date.now()) {
583
- let successDeleted = 0;
584
- let deadDeleted = 0;
585
- if (this.successTtlMs > 0) {
586
- try {
587
- const res = await this.engine.delete(this.objectName, {
588
- where: {
589
- status: "success",
590
- updated_at: { $lt: now - this.successTtlMs }
591
- }
592
- });
593
- successDeleted = _nullishCoalesce(_optionalChain([res, 'optionalAccess', _53 => _53.affected]), () => ( 0));
594
- } catch (err) {
595
- _optionalChain([this, 'access', _54 => _54.logger, 'access', _55 => _55.warn, 'optionalCall', _56 => _56("[webhook-retention] success sweep failed", err)]);
596
- }
597
- }
598
- if (this.deadTtlMs > 0) {
599
- try {
600
- const res = await this.engine.delete(this.objectName, {
601
- where: {
602
- status: "dead",
603
- updated_at: { $lt: now - this.deadTtlMs }
604
- }
605
- });
606
- deadDeleted = _nullishCoalesce(_optionalChain([res, 'optionalAccess', _57 => _57.affected]), () => ( 0));
607
- } catch (err) {
608
- _optionalChain([this, 'access', _58 => _58.logger, 'access', _59 => _59.warn, 'optionalCall', _60 => _60("[webhook-retention] dead sweep failed", err)]);
609
- }
610
- }
611
- if (successDeleted + deadDeleted > 0) {
612
- _optionalChain([this, 'access', _61 => _61.logger, 'access', _62 => _62.info, 'optionalCall', _63 => _63("[webhook-retention] sweep complete", {
613
- success: successDeleted,
614
- dead: deadDeleted
615
- })]);
616
- }
617
- return { success: successDeleted, dead: deadDeleted };
618
- }
619
- };
620
-
621
198
  // src/webhook-outbox-plugin.ts
622
199
  var WebhookOutboxPlugin = class {
623
200
  constructor(options = {}) {
624
201
  this.options = options;
625
202
  this.name = "com.objectstack.plugin-webhook-outbox";
626
- this.version = "1.1.0";
203
+ this.version = "2.0.0";
627
204
  this.type = "standard";
628
- this.dependencies = ["com.objectstack.service.cluster"];
205
+ this.dependencies = ["com.objectstack.service.messaging"];
629
206
  }
630
207
  async init(ctx) {
631
- const cluster = ctx.getService("cluster");
632
- if (!cluster) {
633
- throw new Error(
634
- 'WebhookOutboxPlugin: required service "cluster" not found \u2014 register ClusterServicePlugin first'
635
- );
636
- }
637
208
  const manifest = ctx.getService("manifest");
638
209
  if (manifest && typeof manifest.register === "function") {
639
210
  manifest.register({
@@ -642,13 +213,9 @@ var WebhookOutboxPlugin = class {
642
213
  version: this.version,
643
214
  type: "plugin",
644
215
  scope: "system",
645
- name: "Webhook Outbox Schemas",
646
- description: "Registers sys_webhook (configuration) and sys_webhook_delivery (durable outbox telemetry).",
647
- objects: [_chunkTDSI7UHYcjs.SysWebhook, _chunkTDSI7UHYcjs.SysWebhookDelivery],
648
- // ADR-0029 D7 — contribute the Webhooks entries into the
649
- // Setup app's `group_integrations` slot. The plugin owns these
650
- // objects (K2.a), so it ships their menu too; when the plugin
651
- // isn't installed the slot stays empty.
216
+ name: "Webhook Schemas",
217
+ description: "Registers sys_webhook (configuration). Deliveries use messaging's sys_http_delivery outbox.",
218
+ objects: [_chunkKPKLAXNAcjs.SysWebhook],
652
219
  navigationContributions: [
653
220
  {
654
221
  app: "setup",
@@ -656,14 +223,14 @@ var WebhookOutboxPlugin = class {
656
223
  priority: 100,
657
224
  items: [
658
225
  { id: "nav_webhooks", type: "object", label: "Webhooks", objectName: "sys_webhook", icon: "webhook", requiresObject: "sys_webhook" },
659
- { id: "nav_webhook_deliveries", type: "object", label: "Webhook Deliveries", objectName: "sys_webhook_delivery", icon: "send", requiresObject: "sys_webhook_delivery" }
226
+ { id: "nav_http_deliveries", type: "object", label: "HTTP Deliveries", objectName: "sys_http_delivery", icon: "send", requiresObject: "sys_http_delivery" }
660
227
  ]
661
228
  }
662
229
  ]
663
230
  });
664
231
  } else {
665
- _optionalChain([ctx, 'access', _64 => _64.logger, 'access', _65 => _65.warn, 'optionalCall', _66 => _66(
666
- "[webhook-outbox] manifest service unavailable \u2014 sys_webhook / sys_webhook_delivery will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin."
232
+ _optionalChain([ctx, 'access', _32 => _32.logger, 'access', _33 => _33.warn, 'optionalCall', _34 => _34(
233
+ "[webhook-outbox] manifest service unavailable \u2014 sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin."
667
234
  )]);
668
235
  }
669
236
  if (typeof ctx.hook === "function") {
@@ -676,235 +243,122 @@ var WebhookOutboxPlugin = class {
676
243
  i18n.loadTranslations(locale, data);
677
244
  }
678
245
  }
679
- } catch (e5) {
246
+ } catch (e2) {
680
247
  }
681
248
  });
682
249
  }
683
- const outbox = this.resolveOutbox(ctx);
684
- this.outboxInstance = outbox;
685
- const nodeId = _nullishCoalesce(_nullishCoalesce(this.options.nodeId, () => ( _types.readEnvWithDeprecation.call(void 0, "OS_NODE_ID", "OBJECTSTACK_NODE_ID"))), () => ( `node-${Math.random().toString(36).slice(2, 10)}`));
686
- const dispatcher = new WebhookDispatcher({
687
- nodeId,
688
- cluster,
689
- outbox,
690
- partitionCount: this.options.partitionCount,
691
- batchSize: this.options.batchSize,
692
- intervalMs: this.options.intervalMs,
693
- lockTtlMs: this.options.lockTtlMs,
694
- claimTtlMs: this.options.claimTtlMs,
695
- fetchImpl: this.options.fetchImpl,
696
- onAttempt: this.options.onAttempt,
697
- rng: this.options.rng,
698
- logger: ctx.logger
699
- });
700
- this.dispatcher = dispatcher;
701
- ctx.registerService("webhook.outbox", outbox);
702
- ctx.registerService("webhook.dispatcher", dispatcher);
703
- if (this.options.autoStart !== false) {
704
- dispatcher.start();
705
- }
706
- const usingMemoryOutbox = outbox instanceof MemoryWebhookOutbox;
707
- if (usingMemoryOutbox && process.env.NODE_ENV === "production") {
708
- _optionalChain([ctx, 'access', _67 => _67.logger, 'access', _68 => _68.warn, 'optionalCall', _69 => _69(
709
- "[webhook-outbox] MemoryWebhookOutbox in production \u2014 webhook deliveries WILL be lost on process exit. Ensure ObjectQL is registered before WebhookOutboxPlugin so the SQL outbox can auto-select."
710
- )]);
711
- }
712
250
  const autoEnqueueOpt = _nullishCoalesce(this.options.autoEnqueue, () => ( true));
713
- const retentionOpt = _nullishCoalesce(this.options.retention, () => ( true));
714
- const needsReadyHook = autoEnqueueOpt !== false || retentionOpt !== false;
715
- if (needsReadyHook && typeof ctx.hook === "function") {
251
+ if (typeof ctx.hook === "function") {
716
252
  ctx.hook("kernel:ready", async () => {
717
253
  await this.bootAutoEnqueue(ctx, autoEnqueueOpt);
718
- this.bootRetention(ctx, retentionOpt);
719
- });
720
- }
721
- if (typeof ctx.hook === "function") {
722
- ctx.hook("kernel:ready", () => {
723
254
  this.registerAdminRoutes(ctx);
724
255
  });
725
256
  }
726
- _optionalChain([ctx, 'access', _70 => _70.logger, 'access', _71 => _71.info, 'optionalCall', _72 => _72("[webhook-outbox] initialised", {
727
- nodeId,
728
- partitions: _nullishCoalesce(this.options.partitionCount, () => ( 8)),
729
- interval: _nullishCoalesce(this.options.intervalMs, () => ( 250)),
730
- autoEnqueue: autoEnqueueOpt !== false,
731
- retention: retentionOpt !== false
257
+ _optionalChain([ctx, 'access', _35 => _35.logger, 'access', _36 => _36.info, 'optionalCall', _37 => _37("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
258
+ autoEnqueue: autoEnqueueOpt !== false
732
259
  })]);
733
260
  }
734
261
  async dispose() {
735
- await _optionalChain([this, 'access', _73 => _73.autoEnqueuer, 'optionalAccess', _74 => _74.stop, 'call', _75 => _75()]);
736
- _optionalChain([this, 'access', _76 => _76.retention, 'optionalAccess', _77 => _77.stop, 'call', _78 => _78()]);
737
- await _optionalChain([this, 'access', _79 => _79.dispatcher, 'optionalAccess', _80 => _80.stop, 'call', _81 => _81()]);
262
+ await _optionalChain([this, 'access', _38 => _38.autoEnqueuer, 'optionalAccess', _39 => _39.stop, 'call', _40 => _40()]);
738
263
  }
739
- resolveOutbox(ctx) {
740
- const opt = this.options.outbox;
741
- if (opt) {
742
- return typeof opt === "function" ? opt(ctx) : opt;
743
- }
744
- const engine = this.tryGetService(ctx, ["objectql", "data"]);
745
- if (engine) {
746
- const partitionCount = _nullishCoalesce(this.options.partitionCount, () => ( 8));
747
- const sql = new (0, _chunkHF7CCDPBcjs.SqlWebhookOutbox)(engine, { partitionCount });
748
- _optionalChain([ctx, 'access', _82 => _82.logger, 'access', _83 => _83.info, 'optionalCall', _84 => _84(
749
- "[webhook-outbox] auto-selected SqlWebhookOutbox (objectql engine detected)",
750
- { partitionCount }
751
- )]);
752
- return sql;
753
- }
754
- _optionalChain([ctx, 'access', _85 => _85.logger, 'access', _86 => _86.warn, 'optionalCall', _87 => _87(
755
- "[webhook-outbox] no IDataEngine available \u2014 falling back to MemoryWebhookOutbox. Deliveries will NOT survive process restart and the redeliver REST endpoint will not see rows written through ObjectQL."
756
- )]);
757
- return new MemoryWebhookOutbox();
264
+ getMessaging(ctx) {
265
+ const svc = this.tryGetService(ctx, ["messaging"]);
266
+ return svc && typeof svc.enqueueHttp === "function" ? svc : void 0;
758
267
  }
759
268
  async bootAutoEnqueue(ctx, opt) {
760
269
  if (opt === false) return;
761
270
  const engine = this.tryGetService(ctx, ["objectql", "data"]);
762
271
  const realtime = this.tryGetService(ctx, ["realtime"]);
763
- if (!engine || !realtime) {
764
- _optionalChain([ctx, 'access', _88 => _88.logger, 'access', _89 => _89.warn, 'optionalCall', _90 => _90(
765
- "[webhook-auto-enqueuer] disabled \u2014 ObjectQL or Realtime service not available",
766
- { hasEngine: !!engine, hasRealtime: !!realtime }
272
+ const messaging = this.getMessaging(ctx);
273
+ if (!engine || !realtime || !messaging) {
274
+ _optionalChain([ctx, 'access', _41 => _41.logger, 'access', _42 => _42.warn, 'optionalCall', _43 => _43(
275
+ "[webhook-auto-enqueuer] disabled \u2014 ObjectQL, Realtime, or Messaging service not available",
276
+ { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging }
767
277
  )]);
768
278
  return;
769
279
  }
770
- if (!this.outboxInstance) return;
280
+ if (!messaging.isHttpDeliveryReady()) {
281
+ _optionalChain([ctx, 'access', _44 => _44.logger, 'access', _45 => _45.warn, 'optionalCall', _46 => _46(
282
+ "[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) \u2014 webhook deliveries will not be durable"
283
+ )]);
284
+ }
771
285
  const enqOpts = typeof opt === "object" ? opt : {};
772
286
  this.autoEnqueuer = new AutoEnqueuer(
773
287
  engine,
774
288
  realtime,
775
- this.outboxInstance,
289
+ (input) => messaging.enqueueHttp(input),
776
290
  { ...enqOpts, logger: ctx.logger }
777
291
  );
778
292
  await this.autoEnqueuer.start();
779
293
  ctx.registerService("webhook.autoEnqueuer", this.autoEnqueuer);
780
- _optionalChain([ctx, 'access', _91 => _91.logger, 'access', _92 => _92.info, 'optionalCall', _93 => _93("[webhook-auto-enqueuer] started")]);
781
- }
782
- bootRetention(ctx, opt) {
783
- if (opt === false) return;
784
- if (this.outboxInstance instanceof MemoryWebhookOutbox) return;
785
- const engine = this.tryGetService(ctx, ["objectql", "data"]);
786
- if (!engine) {
787
- _optionalChain([ctx, 'access', _94 => _94.logger, 'access', _95 => _95.warn, 'optionalCall', _96 => _96(
788
- "[webhook-retention] disabled \u2014 ObjectQL service not available"
789
- )]);
790
- return;
791
- }
792
- const retOpts = typeof opt === "object" ? opt : {};
793
- this.retention = new DeliveryRetentionSweeper(engine, {
794
- ...retOpts,
795
- logger: ctx.logger
796
- });
797
- this.retention.start();
798
- ctx.registerService("webhook.retention", this.retention);
799
- _optionalChain([ctx, 'access', _97 => _97.logger, 'access', _98 => _98.info, 'optionalCall', _99 => _99("[webhook-retention] sweeper started")]);
294
+ _optionalChain([ctx, 'access', _47 => _47.logger, 'access', _48 => _48.info, 'optionalCall', _49 => _49("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
800
295
  }
801
296
  tryGetService(ctx, names) {
802
297
  for (const n of names) {
803
298
  try {
804
299
  const svc = ctx.getService(n);
805
300
  if (svc) return svc;
806
- } catch (e6) {
301
+ } catch (e3) {
807
302
  }
808
303
  }
809
304
  return void 0;
810
305
  }
811
306
  /**
812
- * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one
813
- * is available. Silently no-ops in environments without an HTTP
814
- * server (MSW, edge tests, pure library use). Auth is delegated to
815
- * the better-auth session cookie — every authenticated user counts.
307
+ * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is
308
+ * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the
309
+ * better-auth session cookie every authenticated user counts.
816
310
  */
817
311
  registerAdminRoutes(ctx) {
818
312
  const http = this.tryGetService(ctx, ["http-server"]);
819
313
  if (!http || typeof http.getRawApp !== "function") {
820
- _optionalChain([ctx, 'access', _100 => _100.logger, 'access', _101 => _101.debug, 'optionalCall', _102 => _102(
821
- "[webhook-outbox] HTTP server not available; redeliver REST endpoint not mounted"
822
- )]);
314
+ _optionalChain([ctx, 'access', _50 => _50.logger, 'access', _51 => _51.debug, 'optionalCall', _52 => _52("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
823
315
  return;
824
316
  }
825
317
  const rawApp = http.getRawApp();
826
- const outbox = this.outboxInstance;
827
- if (!rawApp || !outbox) return;
318
+ const messaging = this.getMessaging(ctx);
319
+ if (!rawApp || !messaging) return;
828
320
  rawApp.post("/api/v1/webhooks/redeliver", async (c) => {
829
321
  const userId = await this.resolveSessionUserId(ctx, c);
830
322
  if (!userId) {
831
323
  return c.json(
832
- {
833
- success: false,
834
- error: "unauthenticated",
835
- message: "Sign in to redeliver webhook deliveries."
836
- },
324
+ { success: false, error: "unauthenticated", message: "Sign in to redeliver webhook deliveries." },
837
325
  401
838
326
  );
839
327
  }
840
328
  let body;
841
329
  try {
842
330
  body = await c.req.json();
843
- } catch (e7) {
844
- return c.json(
845
- {
846
- success: false,
847
- error: "invalid_body",
848
- message: "Request body must be JSON."
849
- },
850
- 400
851
- );
331
+ } catch (e4) {
332
+ return c.json({ success: false, error: "invalid_body", message: "Request body must be JSON." }, 400);
852
333
  }
853
- const deliveryId = typeof _optionalChain([body, 'optionalAccess', _103 => _103.deliveryId]) === "string" ? body.deliveryId.trim() : "";
334
+ const deliveryId = typeof _optionalChain([body, 'optionalAccess', _53 => _53.deliveryId]) === "string" ? body.deliveryId.trim() : "";
854
335
  if (!deliveryId) {
855
336
  return c.json(
856
- {
857
- success: false,
858
- error: "missing_delivery_id",
859
- message: "Body must include `deliveryId: string`."
860
- },
337
+ { success: false, error: "missing_delivery_id", message: "Body must include `deliveryId: string`." },
861
338
  400
862
339
  );
863
340
  }
864
341
  try {
865
- const row = await outbox.redeliver(deliveryId);
866
- _optionalChain([ctx, 'access', _104 => _104.logger, 'access', _105 => _105.info, 'optionalCall', _106 => _106("[webhook-outbox] redelivered", {
867
- deliveryId,
868
- requestedBy: userId
869
- })]);
342
+ const row = await messaging.redeliverHttp(deliveryId);
343
+ _optionalChain([ctx, 'access', _54 => _54.logger, 'access', _55 => _55.info, 'optionalCall', _56 => _56("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId })]);
870
344
  return c.json({ success: true, data: { id: row.id, status: row.status } });
871
345
  } catch (err) {
872
- const code = _optionalChain([err, 'optionalAccess', _107 => _107.code]);
346
+ const code = _optionalChain([err, 'optionalAccess', _57 => _57.code]);
873
347
  if (code === "not_found") {
874
- return c.json(
875
- { success: false, error: "not_found", message: err.message },
876
- 404
877
- );
348
+ return c.json({ success: false, error: "not_found", message: err.message }, 404);
878
349
  }
879
350
  if (code === "not_eligible") {
880
- return c.json(
881
- { success: false, error: "not_eligible", message: err.message },
882
- 409
883
- );
351
+ return c.json({ success: false, error: "not_eligible", message: err.message }, 409);
884
352
  }
885
- _optionalChain([ctx, 'access', _108 => _108.logger, 'access', _109 => _109.error, 'optionalCall', _110 => _110(
886
- "[webhook-outbox] redeliver failed",
887
- err
888
- )]);
353
+ _optionalChain([ctx, 'access', _58 => _58.logger, 'access', _59 => _59.error, 'optionalCall', _60 => _60("[webhook-outbox] redeliver failed", err)]);
889
354
  return c.json(
890
- {
891
- success: false,
892
- error: "internal_error",
893
- message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _111 => _111.message]), () => ( String(err)))
894
- },
355
+ { success: false, error: "internal_error", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _61 => _61.message]), () => ( String(err))) },
895
356
  500
896
357
  );
897
358
  }
898
359
  });
899
- _optionalChain([ctx, 'access', _112 => _112.logger, 'access', _113 => _113.info, 'optionalCall', _114 => _114(
900
- "[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver"
901
- )]);
360
+ _optionalChain([ctx, 'access', _62 => _62.logger, 'access', _63 => _63.info, 'optionalCall', _64 => _64("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
902
361
  }
903
- /**
904
- * Resolve the requesting user's id from a better-auth session cookie.
905
- * Returns `undefined` for anonymous callers — the caller decides
906
- * whether that's a 401.
907
- */
908
362
  async resolveSessionUserId(ctx, c) {
909
363
  try {
910
364
  const authService = this.tryGetService(ctx, ["auth"]);
@@ -913,11 +367,11 @@ var WebhookOutboxPlugin = class {
913
367
  if (!api && typeof authService.getApi === "function") {
914
368
  api = await authService.getApi();
915
369
  }
916
- if (!_optionalChain([api, 'optionalAccess', _115 => _115.getSession])) return void 0;
370
+ if (!_optionalChain([api, 'optionalAccess', _65 => _65.getSession])) return void 0;
917
371
  const session = await api.getSession({ headers: c.req.raw.headers });
918
- const uid = _optionalChain([session, 'optionalAccess', _116 => _116.user, 'optionalAccess', _117 => _117.id]);
372
+ const uid = _optionalChain([session, 'optionalAccess', _66 => _66.user, 'optionalAccess', _67 => _67.id]);
919
373
  return typeof uid === "string" && uid.length > 0 ? uid : void 0;
920
- } catch (e8) {
374
+ } catch (e5) {
921
375
  return void 0;
922
376
  }
923
377
  }
@@ -926,13 +380,5 @@ var WebhookOutboxPlugin = class {
926
380
 
927
381
 
928
382
 
929
-
930
-
931
-
932
-
933
-
934
-
935
-
936
-
937
- exports.AutoEnqueuer = AutoEnqueuer; exports.DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_MS; exports.DeliveryRetentionSweeper = DeliveryRetentionSweeper; exports.MemoryWebhookOutbox = MemoryWebhookOutbox; exports.RedeliverError = _chunkHF7CCDPBcjs.RedeliverError; exports.WebhookDispatcher = WebhookDispatcher; exports.WebhookOutboxPlugin = WebhookOutboxPlugin; exports.classifyAttempt = classifyAttempt; exports.hashPartition = _chunkHF7CCDPBcjs.hashPartition; exports.nextRetryDelayMs = nextRetryDelayMs; exports.sendOnce = sendOnce;
383
+ exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkKPKLAXNAcjs.SysWebhook; exports.WebhookOutboxPlugin = WebhookOutboxPlugin;
938
384
  //# sourceMappingURL=index.cjs.map