@objectstack/plugin-webhooks 17.0.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
2
 
3
- var _chunkQ4FEMGD6cjs = require('./chunk-Q4FEMGD6.cjs');
3
+ var _chunkDRHJ2M45cjs = require('./chunk-DRHJ2M45.cjs');
4
4
 
5
5
  // src/webhook-secret.ts
6
6
  var WEBHOOK_SECRET_FIELD = "signing_secret";
@@ -194,7 +194,7 @@ var AutoEnqueuer = class {
194
194
  this.droppedForSecret = /* @__PURE__ */ new Set();
195
195
  this.subscriptionsObject = _nullishCoalesce(opts.subscriptionsObject, () => ( "sys_webhook"));
196
196
  this.refreshIntervalMs = _nullishCoalesce(opts.refreshIntervalMs, () => ( 6e4));
197
- this.logger = _nullishCoalesce(opts.logger, () => ( {}));
197
+ this.logger = opts.logger;
198
198
  }
199
199
  /**
200
200
  * Load the subscription cache and start listening for events.
@@ -219,7 +219,7 @@ var AutoEnqueuer = class {
219
219
  if (this.refreshIntervalMs > 0) {
220
220
  this.refreshTimer = setInterval(() => {
221
221
  this.refresh().catch(
222
- (err) => _optionalChain([this, 'access', _7 => _7.logger, 'access', _8 => _8.warn, 'optionalCall', _9 => _9("[webhook-auto-enqueuer] periodic refresh failed", err)])
222
+ (err) => _optionalChain([this, 'access', _7 => _7.logger, 'optionalAccess', _8 => _8.warn, 'optionalCall', _9 => _9("[webhook-auto-enqueuer] periodic refresh failed", err)])
223
223
  );
224
224
  }, this.refreshIntervalMs);
225
225
  _optionalChain([this, 'access', _10 => _10.refreshTimer, 'access', _11 => _11.unref, 'optionalCall', _12 => _12()]);
@@ -252,7 +252,7 @@ var AutoEnqueuer = class {
252
252
  rearmAfterCryptoRegistered() {
253
253
  const inFlight = _nullishCoalesce(this.refreshing, () => ( Promise.resolve()));
254
254
  void inFlight.catch(() => void 0).then(() => this.running ? this.refresh() : void 0).catch(
255
- (err) => _optionalChain([this, 'access', _15 => _15.logger, 'access', _16 => _16.warn, 'optionalCall', _17 => _17(
255
+ (err) => _optionalChain([this, 'access', _15 => _15.logger, 'optionalAccess', _16 => _16.warn, 'optionalCall', _17 => _17(
256
256
  "[webhook-auto-enqueuer] re-arm after CryptoProvider registration failed",
257
257
  err
258
258
  )])
@@ -276,7 +276,7 @@ var AutoEnqueuer = class {
276
276
  where: { active: true }
277
277
  });
278
278
  } catch (err) {
279
- _optionalChain([this, 'access', _18 => _18.logger, 'access', _19 => _19.warn, 'optionalCall', _20 => _20(
279
+ _optionalChain([this, 'access', _18 => _18.logger, 'optionalAccess', _19 => _19.warn, 'optionalCall', _20 => _20(
280
280
  `[webhook-auto-enqueuer] failed to load ${this.subscriptionsObject}`,
281
281
  err
282
282
  )]);
@@ -300,7 +300,7 @@ var AutoEnqueuer = class {
300
300
  if (!live.has(id)) this.droppedForSecret.delete(id);
301
301
  }
302
302
  }
303
- _optionalChain([this, 'access', _22 => _22.logger, 'access', _23 => _23.debug, 'optionalCall', _24 => _24("[webhook-auto-enqueuer] cache refreshed", {
303
+ _optionalChain([this, 'access', _22 => _22.logger, 'optionalAccess', _23 => _23.debug, 'optionalCall', _24 => _24("[webhook-auto-enqueuer] cache refreshed", {
304
304
  objects: this.subscriptions.size,
305
305
  rows: rows.length
306
306
  })]);
@@ -371,20 +371,20 @@ var AutoEnqueuer = class {
371
371
  reportWriteFailure(sub, eventId, err, verb) {
372
372
  const meta = { webhook: sub.name, eventId, err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _25 => _25.message]), () => ( err)) };
373
373
  if (!sub.parkedReason) {
374
- _optionalChain([this, 'access', _26 => _26.logger, 'access', _27 => _27.warn, 'optionalCall', _28 => _28(`[webhook-auto-enqueuer] ${verb} failed`, meta)]);
374
+ _optionalChain([this, 'access', _26 => _26.logger, 'optionalAccess', _27 => _27.warn, 'optionalCall', _28 => _28(`[webhook-auto-enqueuer] ${verb} failed`, meta)]);
375
375
  return;
376
376
  }
377
377
  const message = `[webhook-auto-enqueuer] could not record the undeliverable event for webhook '${sub.name}' \u2014 the subscription is parked for an unresolvable credential, and this event is now DISCARDED WITH NO TRACE in sys_http_delivery, which is the durability gap #8069 closes. Most likely cause: the enqueue callback was wired directly to IHttpOutbox.enqueue instead of MessagingService.enqueueHttp \u2014 only the messaging seam routes a parked event to recordUndeliverable(), and the delivery door refuses it rather than minting a pending row that would be sent UNSIGNED.`;
378
- if (typeof this.logger.error === "function") {
379
- this.logger.error(message, err, meta);
378
+ if (typeof _optionalChain([this, 'access', _29 => _29.logger, 'optionalAccess', _30 => _30.error]) === "function") {
379
+ _optionalChain([this, 'access', _31 => _31.logger, 'optionalAccess', _32 => _32.error, 'call', _33 => _33(message, err, meta)]);
380
380
  } else {
381
- _optionalChain([this, 'access', _29 => _29.logger, 'access', _30 => _30.warn, 'optionalCall', _31 => _31(message, meta)]);
381
+ _optionalChain([this, 'access', _34 => _34.logger, 'optionalAccess', _35 => _35.warn, 'optionalCall', _36 => _36(message, meta)]);
382
382
  }
383
383
  }
384
384
  park(sub, err, credential) {
385
385
  sub.secret = void 0;
386
386
  sub.headers = void 0;
387
- sub.parkedReason = `[${WEBHOOK_SECRET_REFUSAL_CODE}/${WEBHOOK_SECRET_REFUSAL_STATUS}] webhook '${sub.name}' holds ${credential.article} that could not be decrypted, so this event was NOT delivered \u2014 recording it here rather than ${credential.ratherThan} (${credential.issue}, #8069). This row was never sent and cannot be redelivered: it carries no HMAC signature, because the ${credential.noun} that would have produced one is exactly what is missing. Fix: register a CryptoProvider (engine.setCryptoProvider \u2014 LocalCryptoProvider in dev, KMS/Vault in production) with the same key the ${credential.noun} was written under, and make sure the sys_secret row is reachable; the subscription re-arms on registration (#8022) and at the next periodic refresh, and later events are delivered normally. Cause: ${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _32 => _32.message]), () => ( String(err)))}`;
387
+ sub.parkedReason = `[${WEBHOOK_SECRET_REFUSAL_CODE}/${WEBHOOK_SECRET_REFUSAL_STATUS}] webhook '${sub.name}' holds ${credential.article} that could not be decrypted, so this event was NOT delivered \u2014 recording it here rather than ${credential.ratherThan} (${credential.issue}, #8069). This row was never sent and cannot be redelivered: it carries no HMAC signature, because the ${credential.noun} that would have produced one is exactly what is missing. Fix: register a CryptoProvider (engine.setCryptoProvider \u2014 LocalCryptoProvider in dev, KMS/Vault in production) with the same key the ${credential.noun} was written under, and make sure the sys_secret row is reachable; the subscription re-arms on registration (#8022) and at the next periodic refresh, and later events are delivered normally. Cause: ${_nullishCoalesce(_optionalChain([err, 'optionalAccess', _37 => _37.message]), () => ( String(err)))}`;
388
388
  }
389
389
  /**
390
390
  * [#7799] Resolve `sub.secret`. Returns `false` when the subscription must
@@ -428,9 +428,9 @@ var AutoEnqueuer = class {
428
428
  this.park(sub, err, SIGNING_SECRET_CREDENTIAL);
429
429
  return false;
430
430
  }
431
- const legacy = readLegacySecret(_optionalChain([row, 'optionalAccess', _33 => _33.definition_json]));
431
+ const legacy = readLegacySecret(_optionalChain([row, 'optionalAccess', _38 => _38.definition_json]));
432
432
  if (legacy) {
433
- _optionalChain([this, 'access', _34 => _34.logger, 'access', _35 => _35.warn, 'optionalCall', _36 => _36(
433
+ _optionalChain([this, 'access', _39 => _39.logger, 'optionalAccess', _40 => _40.warn, 'optionalCall', _41 => _41(
434
434
  `[webhook-auto-enqueuer] webhook '${sub.name}' still carries its signing secret as CLEARTEXT in definition_json, readable over the data API (#7799). Signing continues from it; run the boot sweep (migrateLegacyWebhookSecrets) with a CryptoProvider wired to move it into sys_secret.`,
435
435
  { id: sub.id }
436
436
  )]);
@@ -478,9 +478,9 @@ var AutoEnqueuer = class {
478
478
  this.park(sub, err, CUSTOM_HEADERS_CREDENTIAL);
479
479
  return false;
480
480
  }
481
- const legacy = readLegacyHeaders(_optionalChain([row, 'optionalAccess', _37 => _37.definition_json]));
481
+ const legacy = readLegacyHeaders(_optionalChain([row, 'optionalAccess', _42 => _42.definition_json]));
482
482
  if (legacy) {
483
- _optionalChain([this, 'access', _38 => _38.logger, 'access', _39 => _39.warn, 'optionalCall', _40 => _40(
483
+ _optionalChain([this, 'access', _43 => _43.logger, 'optionalAccess', _44 => _44.warn, 'optionalCall', _45 => _45(
484
484
  `[webhook-auto-enqueuer] webhook '${sub.name}' still carries its custom headers as CLEARTEXT in definition_json, readable over the data API (#7986) \u2014 that map is the ordinary place an Authorization header goes. Delivery continues from it; run the boot sweep (migrateLegacyWebhookSecrets) with a CryptoProvider wired to move them into sys_secret.`,
485
485
  { id: sub.id }
486
486
  )]);
@@ -520,10 +520,10 @@ var AutoEnqueuer = class {
520
520
  field: credential.field,
521
521
  code: WEBHOOK_SECRET_REFUSAL_CODE,
522
522
  status: WEBHOOK_SECRET_REFUSAL_STATUS,
523
- err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _41 => _41.message]), () => ( err))
523
+ err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _46 => _46.message]), () => ( err))
524
524
  };
525
525
  if (this.droppedForSecret.has(sub.id)) {
526
- _optionalChain([this, 'access', _42 => _42.logger, 'access', _43 => _43.debug, 'optionalCall', _44 => _44(
526
+ _optionalChain([this, 'access', _47 => _47.logger, 'optionalAccess', _48 => _48.debug, 'optionalCall', _49 => _49(
527
527
  `[webhook-auto-enqueuer] webhook '${sub.name}' is still dropped for an unresolvable ${credential.noun} (${credential.issues})`,
528
528
  meta
529
529
  )]);
@@ -531,14 +531,14 @@ var AutoEnqueuer = class {
531
531
  }
532
532
  this.droppedForSecret.add(sub.id);
533
533
  const message = `[webhook-auto-enqueuer] webhook '${sub.name}' holds ${credential.article} that could not be decrypted \u2014 the subscription is PARKED rather than ${credential.ratherThan} (${credential.issue}), so every matching record change is discarded with NO delivery, while the row keeps reading active:true in Setup. Each discarded event IS recorded in sys_http_delivery as a dead row with 0 attempts carrying this cause (#8069) \u2014 look there for the backlog; those rows can never be sent or redelivered, because a parked row has no HMAC signature. Fix: register a CryptoProvider (engine.setCryptoProvider \u2014 LocalCryptoProvider in dev, KMS/Vault in production) with the same key the ${credential.noun} was written under, and make sure the sys_secret row is reachable; the subscription re-arms on registration (#8022) and at the next periodic refresh.`;
534
- if (typeof this.logger.error === "function") {
535
- this.logger.error(message, err, meta);
534
+ if (typeof _optionalChain([this, 'access', _50 => _50.logger, 'optionalAccess', _51 => _51.error]) === "function") {
535
+ _optionalChain([this, 'access', _52 => _52.logger, 'optionalAccess', _53 => _53.error, 'call', _54 => _54(message, err, meta)]);
536
536
  } else {
537
- _optionalChain([this, 'access', _45 => _45.logger, 'access', _46 => _46.warn, 'optionalCall', _47 => _47(message, meta)]);
537
+ _optionalChain([this, 'access', _55 => _55.logger, 'optionalAccess', _56 => _56.warn, 'optionalCall', _57 => _57(message, meta)]);
538
538
  }
539
539
  }
540
540
  parseRow(row) {
541
- if (!_optionalChain([row, 'optionalAccess', _48 => _48.id]) || !_optionalChain([row, 'optionalAccess', _49 => _49.url])) return null;
541
+ if (!_optionalChain([row, 'optionalAccess', _58 => _58.id]) || !_optionalChain([row, 'optionalAccess', _59 => _59.url])) return null;
542
542
  const rawTriggers = row.triggers;
543
543
  let triggerList;
544
544
  if (Array.isArray(rawTriggers)) {
@@ -559,7 +559,7 @@ var AutoEnqueuer = class {
559
559
  const normalized = triggerList.map((t) => t.trim().toLowerCase()).filter(Boolean);
560
560
  const unknown = normalized.filter((t) => !DISPATCHABLE_WEBHOOK_TRIGGERS.has(t));
561
561
  if (unknown.length > 0) {
562
- _optionalChain([this, 'access', _50 => _50.logger, 'access', _51 => _51.warn, 'optionalCall', _52 => _52(
562
+ _optionalChain([this, 'access', _60 => _60.logger, 'optionalAccess', _61 => _61.warn, 'optionalCall', _62 => _62(
563
563
  `[webhook-auto-enqueuer] webhook '${_nullishCoalesce(row.name, () => ( row.id))}' declares trigger(s) the engine never emits: ${unknown.join(", ")} \u2014 ignored. Dispatchable triggers: ${[...DISPATCHABLE_WEBHOOK_TRIGGERS].join(", ")}.`,
564
564
  { id: row.id, unknown }
565
565
  )]);
@@ -568,7 +568,7 @@ var AutoEnqueuer = class {
568
568
  normalized.filter((t) => DISPATCHABLE_WEBHOOK_TRIGGERS.has(t))
569
569
  );
570
570
  if (triggers.size === 0) {
571
- _optionalChain([this, 'access', _53 => _53.logger, 'access', _54 => _54.warn, 'optionalCall', _55 => _55(
571
+ _optionalChain([this, 'access', _63 => _63.logger, 'optionalAccess', _64 => _64.warn, 'optionalCall', _65 => _65(
572
572
  `[webhook-auto-enqueuer] webhook '${_nullishCoalesce(row.name, () => ( row.id))}' has no dispatchable triggers \u2014 it will NEVER fire (rule webhook/without-triggers): there is no manual fire path (#3196), so this row is dead while looking armed in Setup. Declare one of: ${[...DISPATCHABLE_WEBHOOK_TRIGGERS].join(", ")}, or set it inactive if it should be off.`,
573
573
  { id: row.id }
574
574
  )]);
@@ -609,11 +609,11 @@ var AutoEnqueuer = class {
609
609
  handleEvent(event) {
610
610
  if (!event.object) return;
611
611
  if (event.object === this.subscriptionsObject) return;
612
- if (_optionalChain([event, 'access', _56 => _56.type, 'optionalAccess', _57 => _57.startsWith, 'call', _58 => _58("data.records.")])) {
612
+ if (_optionalChain([event, 'access', _66 => _66.type, 'optionalAccess', _67 => _67.startsWith, 'call', _68 => _68("data.records.")])) {
613
613
  this.handleBulkEvent(event);
614
614
  return;
615
615
  }
616
- if (!_optionalChain([event, 'access', _59 => _59.type, 'optionalAccess', _60 => _60.startsWith, 'call', _61 => _61("data.record.")])) return;
616
+ if (!_optionalChain([event, 'access', _69 => _69.type, 'optionalAccess', _70 => _70.startsWith, 'call', _71 => _71("data.record.")])) return;
617
617
  const action = event.type.slice("data.record.".length);
618
618
  const trigger = mapActionToTrigger(action);
619
619
  if (!trigger) return;
@@ -625,7 +625,7 @@ var AutoEnqueuer = class {
625
625
  const payload = _nullishCoalesce(event.payload, () => ( {}));
626
626
  const recordId = payload.recordId;
627
627
  if (typeof recordId !== "string" || recordId === "") {
628
- _optionalChain([this, 'access', _62 => _62.logger, 'access', _63 => _63.warn, 'optionalCall', _64 => _64(
628
+ _optionalChain([this, 'access', _72 => _72.logger, 'optionalAccess', _73 => _73.warn, 'optionalCall', _74 => _74(
629
629
  "[webhook-auto-enqueuer] dropping off-contract data event: payload is not a DataEvent (no top-level string `recordId`) \u2014 fix the producer",
630
630
  { type: event.type, object: event.object }
631
631
  )]);
@@ -694,7 +694,7 @@ var AutoEnqueuer = class {
694
694
  const payload = _nullishCoalesce(event.payload, () => ( {}));
695
695
  const matched = payload.matched;
696
696
  if (typeof matched !== "number" || !Number.isInteger(matched) || matched < 0) {
697
- _optionalChain([this, 'access', _65 => _65.logger, 'access', _66 => _66.warn, 'optionalCall', _67 => _67(
697
+ _optionalChain([this, 'access', _75 => _75.logger, 'optionalAccess', _76 => _76.warn, 'optionalCall', _77 => _77(
698
698
  "[webhook-auto-enqueuer] dropping off-contract bulk data event: payload is not a BulkDataEvent (no top-level non-negative integer `matched`) \u2014 fix the producer",
699
699
  { type: event.type, object: event.object }
700
700
  )]);
@@ -702,7 +702,7 @@ var AutoEnqueuer = class {
702
702
  }
703
703
  const eventUuid = payload.id;
704
704
  if (typeof eventUuid !== "string" || eventUuid === "") {
705
- _optionalChain([this, 'access', _68 => _68.logger, 'access', _69 => _69.warn, 'optionalCall', _70 => _70(
705
+ _optionalChain([this, 'access', _78 => _78.logger, 'optionalAccess', _79 => _79.warn, 'optionalCall', _80 => _80(
706
706
  "[webhook-auto-enqueuer] dropping off-contract bulk data event: payload has no top-level string `id` to dedup on \u2014 fix the producer",
707
707
  { type: event.type, object: event.object }
708
708
  )]);
@@ -737,9 +737,9 @@ var AutoEnqueuer = class {
737
737
  }
738
738
  handleSelfHealEvent(event) {
739
739
  if (event.object !== this.subscriptionsObject) return;
740
- if (!_optionalChain([event, 'access', _71 => _71.type, 'optionalAccess', _72 => _72.startsWith, 'call', _73 => _73("data.record.")]) && !_optionalChain([event, 'access', _74 => _74.type, 'optionalAccess', _75 => _75.startsWith, 'call', _76 => _76("data.records.")])) return;
740
+ if (!_optionalChain([event, 'access', _81 => _81.type, 'optionalAccess', _82 => _82.startsWith, 'call', _83 => _83("data.record.")]) && !_optionalChain([event, 'access', _84 => _84.type, 'optionalAccess', _85 => _85.startsWith, 'call', _86 => _86("data.records.")])) return;
741
741
  this.refresh().catch(
742
- (err) => _optionalChain([this, 'access', _77 => _77.logger, 'access', _78 => _78.warn, 'optionalCall', _79 => _79("[webhook-auto-enqueuer] self-heal refresh failed", err)])
742
+ (err) => _optionalChain([this, 'access', _87 => _87.logger, 'optionalAccess', _88 => _88.warn, 'optionalCall', _89 => _89("[webhook-auto-enqueuer] self-heal refresh failed", err)])
743
743
  );
744
744
  }
745
745
  /** Test / admin accessor. */
@@ -782,21 +782,21 @@ var _automation = require('@objectstack/spec/automation');
782
782
  var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
783
783
  function uid(prefix) {
784
784
  const g = globalThis;
785
- if (_optionalChain([g, 'access', _80 => _80.crypto, 'optionalAccess', _81 => _81.randomUUID])) return `${prefix}_${g.crypto.randomUUID()}`;
785
+ if (_optionalChain([g, 'access', _90 => _90.crypto, 'optionalAccess', _91 => _91.randomUUID])) return `${prefix}_${g.crypto.randomUUID()}`;
786
786
  return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
787
787
  }
788
788
  function readDeclared(engine, metadataService, type) {
789
789
  try {
790
- const reg = _optionalChain([engine, 'optionalAccess', _82 => _82._registry]);
791
- if (_optionalChain([reg, 'optionalAccess', _83 => _83.listItems])) {
790
+ const reg = _optionalChain([engine, 'optionalAccess', _92 => _92._registry]);
791
+ if (_optionalChain([reg, 'optionalAccess', _93 => _93.listItems])) {
792
792
  const items = (_nullishCoalesce(reg.listItems(type), () => ( []))).filter(Boolean);
793
793
  if (items.length > 0) return items;
794
794
  }
795
795
  } catch (e7) {
796
796
  }
797
797
  try {
798
- const listed = _optionalChain([metadataService, 'optionalAccess', _84 => _84.list, 'optionalCall', _85 => _85(type)]);
799
- const arr = typeof _optionalChain([listed, 'optionalAccess', _86 => _86.then]) === "function" ? [] : _nullishCoalesce(listed, () => ( []));
798
+ const listed = _optionalChain([metadataService, 'optionalAccess', _94 => _94.list, 'optionalCall', _95 => _95(type)]);
799
+ const arr = typeof _optionalChain([listed, 'optionalAccess', _96 => _96.then]) === "function" ? [] : _nullishCoalesce(listed, () => ( []));
800
800
  return Array.isArray(arr) ? arr.filter(Boolean) : [];
801
801
  } catch (e8) {
802
802
  return [];
@@ -813,9 +813,9 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
813
813
  try {
814
814
  wh = _automation.WebhookSchema.parse(raw);
815
815
  } catch (err) {
816
- _optionalChain([logger, 'optionalAccess', _87 => _87.warn, 'optionalCall', _88 => _88("[webhook] declared webhook failed validation \u2014 skipped", {
817
- name: _optionalChain([raw, 'optionalAccess', _89 => _89.name]),
818
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _90 => _90.message]), () => ( String(err)))
816
+ _optionalChain([logger, 'optionalAccess', _97 => _97.warn, 'optionalCall', _98 => _98("[webhook] declared webhook failed validation \u2014 skipped", {
817
+ name: _optionalChain([raw, 'optionalAccess', _99 => _99.name]),
818
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _100 => _100.message]), () => ( String(err)))
819
819
  })]);
820
820
  skipped += 1;
821
821
  continue;
@@ -829,7 +829,7 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
829
829
  const row = Array.isArray(existing) ? existing[0] : void 0;
830
830
  if (row) {
831
831
  if (row.managed_by === "admin") {
832
- _optionalChain([logger, 'optionalAccess', _91 => _91.warn, 'optionalCall', _92 => _92("[webhook] declared name collides with an admin-authored row \u2014 seed skipped", {
832
+ _optionalChain([logger, 'optionalAccess', _101 => _101.warn, 'optionalCall', _102 => _102("[webhook] declared name collides with an admin-authored row \u2014 seed skipped", {
833
833
  name: wh.name
834
834
  })]);
835
835
  skipped += 1;
@@ -881,18 +881,18 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
881
881
  seeded += 1;
882
882
  } catch (err) {
883
883
  const protection = isSecretProtectionFailure(err);
884
- _optionalChain([logger, 'optionalAccess', _93 => _93.warn, 'optionalCall', _94 => _94(
884
+ _optionalChain([logger, 'optionalAccess', _103 => _103.warn, 'optionalCall', _104 => _104(
885
885
  protection ? "[webhook] declared webhook NOT seeded \u2014 its signing secret cannot be stored encrypted (#7799)" : "[webhook] declared webhook seed failed",
886
886
  {
887
887
  name: wh.name,
888
888
  ...protection ? { code: WEBHOOK_SECRET_REFUSAL_CODE, status: WEBHOOK_SECRET_REFUSAL_STATUS } : {},
889
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _95 => _95.message]), () => ( String(err)))
889
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _105 => _105.message]), () => ( String(err)))
890
890
  }
891
891
  )]);
892
892
  skipped += 1;
893
893
  }
894
894
  }
895
- _optionalChain([logger, 'optionalAccess', _96 => _96.info, 'optionalCall', _97 => _97("[webhook] declared webhooks materialized into sys_webhook", {
895
+ _optionalChain([logger, 'optionalAccess', _106 => _106.info, 'optionalCall', _107 => _107("[webhook] declared webhooks materialized into sys_webhook", {
896
896
  seeded,
897
897
  skipped,
898
898
  total: declared.length
@@ -901,7 +901,7 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
901
901
  }
902
902
  async function secretPatch(engine, wh, row, subscriptionsObject) {
903
903
  const { secret } = splitWebhookSecret(wh);
904
- const hasStored = _optionalChain([row, 'optionalAccess', _98 => _98[WEBHOOK_SECRET_FIELD]]) != null && row[WEBHOOK_SECRET_FIELD] !== "";
904
+ const hasStored = _optionalChain([row, 'optionalAccess', _108 => _108[WEBHOOK_SECRET_FIELD]]) != null && row[WEBHOOK_SECRET_FIELD] !== "";
905
905
  if (!secret) return hasStored ? { [WEBHOOK_SECRET_FIELD]: null } : {};
906
906
  if (!hasStored || !canResolveSecrets(engine)) return { [WEBHOOK_SECRET_FIELD]: secret };
907
907
  try {
@@ -943,16 +943,16 @@ async function migrateLegacyWebhookSecrets(engine, logger, subscriptionsObject =
943
943
  let rows;
944
944
  try {
945
945
  const found = await engine.find(subscriptionsObject, SYSTEM_QUERY);
946
- rows = Array.isArray(found) ? found : _nullishCoalesce(_optionalChain([found, 'optionalAccess', _99 => _99.data]), () => ( []));
946
+ rows = Array.isArray(found) ? found : _nullishCoalesce(_optionalChain([found, 'optionalAccess', _109 => _109.data]), () => ( []));
947
947
  } catch (err) {
948
- _optionalChain([logger, 'optionalAccess', _100 => _100.warn, 'optionalCall', _101 => _101("[webhook] legacy secret sweep skipped \u2014 could not read subscriptions", {
948
+ _optionalChain([logger, 'optionalAccess', _110 => _110.warn, 'optionalCall', _111 => _111("[webhook] legacy secret sweep skipped \u2014 could not read subscriptions", {
949
949
  object: subscriptionsObject,
950
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _102 => _102.message]), () => ( String(err)))
950
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _112 => _112.message]), () => ( String(err)))
951
951
  })]);
952
952
  return out;
953
953
  }
954
954
  for (const row of rows) {
955
- if (!_optionalChain([row, 'optionalAccess', _103 => _103.id])) continue;
955
+ if (!_optionalChain([row, 'optionalAccess', _113 => _113.id])) continue;
956
956
  const legacySecret = readLegacySecret(row.definition_json);
957
957
  const legacyHeaders = readLegacyHeaders(row.definition_json);
958
958
  if (!legacySecret && !legacyHeaders) continue;
@@ -973,20 +973,20 @@ async function migrateLegacyWebhookSecrets(engine, logger, subscriptionsObject =
973
973
  out.failed += 1;
974
974
  const protection = isSecretProtectionFailure(err);
975
975
  const what = legacySecret && legacyHeaders ? "signing secret and custom headers" : legacySecret ? "signing secret" : "custom headers";
976
- _optionalChain([logger, 'optionalAccess', _104 => _104.warn, 'optionalCall', _105 => _105(
976
+ _optionalChain([logger, 'optionalAccess', _114 => _114.warn, 'optionalCall', _115 => _115(
977
977
  protection ? `[webhook] ${what} STILL CLEARTEXT in definition_json \u2014 no CryptoProvider to encrypt them (#7799/#7986)` : `[webhook] ${what} migration failed \u2014 row left unchanged (#7799/#7986)`,
978
978
  {
979
979
  name: _nullishCoalesce(row.name, () => ( row.id)),
980
980
  id: row.id,
981
981
  code: WEBHOOK_SECRET_REFUSAL_CODE,
982
982
  status: WEBHOOK_SECRET_REFUSAL_STATUS,
983
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _106 => _106.message]), () => ( String(err)))
983
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _116 => _116.message]), () => ( String(err)))
984
984
  }
985
985
  )]);
986
986
  }
987
987
  }
988
988
  if (out.found > 0) {
989
- _optionalChain([logger, 'optionalAccess', _107 => _107.info, 'optionalCall', _108 => _108("[webhook] legacy cleartext credentials swept into sys_secret", { ...out })]);
989
+ _optionalChain([logger, 'optionalAccess', _117 => _117.info, 'optionalCall', _118 => _118("[webhook] legacy cleartext credentials swept into sys_secret", { ...out })]);
990
990
  }
991
991
  return out;
992
992
  }
@@ -1024,14 +1024,14 @@ function createWebhookRedeliverGuard(engine, subscriptionsObject = WEBHOOK_OBJEC
1024
1024
  var WEBHOOK_PROVENANCE_PACKAGE = "plugin-webhooks:provenance";
1025
1025
  var SYSTEM_CTX3 = { isSystem: true, positions: [], permissions: [] };
1026
1026
  function bindWebhookProvenanceStamp(engine, logger) {
1027
- if (typeof _optionalChain([engine, 'optionalAccess', _109 => _109.registerHook]) !== "function") return;
1027
+ if (typeof _optionalChain([engine, 'optionalAccess', _119 => _119.registerHook]) !== "function") return;
1028
1028
  engine.registerHook(
1029
1029
  "beforeUpdate",
1030
1030
  async (ctx) => {
1031
- if (_optionalChain([ctx, 'optionalAccess', _110 => _110.session, 'optionalAccess', _111 => _111.isSystem])) return;
1032
- const id = _nullishCoalesce(_optionalChain([ctx, 'optionalAccess', _112 => _112.input, 'optionalAccess', _113 => _113.id]), () => ( _optionalChain([ctx, 'optionalAccess', _114 => _114.input, 'optionalAccess', _115 => _115.data, 'optionalAccess', _116 => _116.id])));
1031
+ if (_optionalChain([ctx, 'optionalAccess', _120 => _120.session, 'optionalAccess', _121 => _121.isSystem])) return;
1032
+ const id = _nullishCoalesce(_optionalChain([ctx, 'optionalAccess', _122 => _122.input, 'optionalAccess', _123 => _123.id]), () => ( _optionalChain([ctx, 'optionalAccess', _124 => _124.input, 'optionalAccess', _125 => _125.data, 'optionalAccess', _126 => _126.id])));
1033
1033
  if (!id) return;
1034
- const data = _optionalChain([ctx, 'optionalAccess', _117 => _117.input, 'optionalAccess', _118 => _118.data]);
1034
+ const data = _optionalChain([ctx, 'optionalAccess', _127 => _127.input, 'optionalAccess', _128 => _128.data]);
1035
1035
  if (!data || typeof data !== "object") return;
1036
1036
  try {
1037
1037
  const rows = await engine.find("sys_webhook", {
@@ -1046,22 +1046,114 @@ function bindWebhookProvenanceStamp(engine, logger) {
1046
1046
  data.customized = true;
1047
1047
  }
1048
1048
  } catch (err) {
1049
- _optionalChain([logger, 'optionalAccess', _119 => _119.warn, 'optionalCall', _120 => _120("[webhook] provenance stamp failed (edit proceeds unstamped)", {
1049
+ _optionalChain([logger, 'optionalAccess', _129 => _129.warn, 'optionalCall', _130 => _130("[webhook] provenance stamp failed (edit proceeds unstamped)", {
1050
1050
  id,
1051
- error: _optionalChain([err, 'optionalAccess', _121 => _121.message])
1051
+ error: _optionalChain([err, 'optionalAccess', _131 => _131.message])
1052
1052
  })]);
1053
1053
  }
1054
1054
  },
1055
1055
  { object: "sys_webhook", packageId: WEBHOOK_PROVENANCE_PACKAGE, priority: 150 }
1056
1056
  );
1057
- _optionalChain([logger, 'optionalAccess', _122 => _122.info, 'optionalCall', _123 => _123("[webhook] provenance stamp hook bound")]);
1057
+ _optionalChain([logger, 'optionalAccess', _132 => _132.info, 'optionalCall', _133 => _133("[webhook] provenance stamp hook bound")]);
1058
1058
  }
1059
1059
  function unbindWebhookProvenanceStamp(engine) {
1060
- if (typeof _optionalChain([engine, 'optionalAccess', _124 => _124.unregisterHooksByPackage]) === "function") {
1060
+ if (typeof _optionalChain([engine, 'optionalAccess', _134 => _134.unregisterHooksByPackage]) === "function") {
1061
1061
  engine.unregisterHooksByPackage(WEBHOOK_PROVENANCE_PACKAGE);
1062
1062
  }
1063
1063
  }
1064
1064
 
1065
+ // src/webhook-headers-gate.ts
1066
+ var WEBHOOK_HEADERS_SHAPE_REFUSAL_CODE = "VALIDATION_ERROR";
1067
+ var WEBHOOK_HEADERS_SHAPE_REFUSAL_STATUS = 400;
1068
+ var DECLARED_SHAPE = 'Custom HTTP headers sent with each delivery, as a JSON object ({"Authorization": "Bearer ..."})';
1069
+ var WebhookHeadersShapeError = class extends Error {
1070
+ constructor(object, field, diagnosis) {
1071
+ super(
1072
+ `Custom headers refused for "${object}.${field}": ${diagnosis}. The required shape is a FLAT JSON object of string values, which is what the field itself asks for \u2014 its description reads: "${DECLARED_SHAPE}". This is checked at the write door because one step later there is nothing left to check: the engine encrypts this value into sys_secret and every read path returns only the mask, so a stored value that can never be used is indistinguishable from one that works until the next delivery tries to send it \u2014 at which point the subscription parks and the report arrives an unbounded time later, in a different surface from the one it was typed into (#7986, #8558, #8566). ${HEADERS_REMEDY}`
1073
+ );
1074
+ this.code = WEBHOOK_HEADERS_SHAPE_REFUSAL_CODE;
1075
+ this.status = WEBHOOK_HEADERS_SHAPE_REFUSAL_STATUS;
1076
+ this.name = "WebhookHeadersShapeError";
1077
+ this.object = object;
1078
+ this.field = field;
1079
+ }
1080
+ };
1081
+ function describeParsed(parsed) {
1082
+ if (parsed === null) return "null";
1083
+ if (Array.isArray(parsed)) return "a JSON array";
1084
+ if (typeof parsed !== "object") return `a JSON ${typeof parsed}`;
1085
+ const entries = Object.entries(parsed);
1086
+ if (entries.length === 0) {
1087
+ return 'an EMPTY JSON object, which is not the same thing as "send no custom headers"';
1088
+ }
1089
+ const bad = entries.filter(([, v]) => typeof v !== "string");
1090
+ if (bad.length > 0) {
1091
+ const named = bad.map(([k, v]) => `${JSON.stringify(k)} (${Array.isArray(v) ? "array" : v === null ? "null" : typeof v})`).join(", ");
1092
+ return `a JSON object, but the wire carries only strings and ${bad.length === 1 ? "this value is" : "these values are"} not a string: ${named}`;
1093
+ }
1094
+ return "a JSON object the header seam does not accept";
1095
+ }
1096
+ function describeRejected(value) {
1097
+ if (typeof value === "string") {
1098
+ let parsed;
1099
+ try {
1100
+ parsed = JSON.parse(value);
1101
+ } catch (e10) {
1102
+ return "the value is a string that is not valid JSON at all \u2014 check for unquoted keys or values ({X-Team: crm}), single quotes instead of double, or a trailing comma";
1103
+ }
1104
+ return `the value parses as JSON but is ${describeParsed(parsed)}`;
1105
+ }
1106
+ return `the value is ${describeParsed(value)}`;
1107
+ }
1108
+ function assertWritableWebhookHeaders(data, object = WEBHOOK_OBJECT, field = WEBHOOK_HEADERS_FIELD) {
1109
+ if (!data || typeof data !== "object") return;
1110
+ if (!Object.prototype.hasOwnProperty.call(data, field)) return;
1111
+ const value = data[field];
1112
+ if (value === null || typeof value === "undefined") return;
1113
+ if (value === "") return;
1114
+ if (isOpaqueSecretForm(value)) return;
1115
+ let serialized;
1116
+ if (typeof value === "string") {
1117
+ serialized = value;
1118
+ } else {
1119
+ try {
1120
+ serialized = JSON.stringify(value);
1121
+ } catch (e11) {
1122
+ throw new WebhookHeadersShapeError(
1123
+ object,
1124
+ field,
1125
+ "the value cannot be serialized to JSON at all (it contains a circular reference)"
1126
+ );
1127
+ }
1128
+ if (typeof serialized !== "string") {
1129
+ throw new WebhookHeadersShapeError(object, field, `the value is a ${typeof value}`);
1130
+ }
1131
+ }
1132
+ if (parseStoredHeaders(serialized)) return;
1133
+ throw new WebhookHeadersShapeError(object, field, describeRejected(value));
1134
+ }
1135
+ var WEBHOOK_HEADERS_GATE_PACKAGE = "plugin-webhooks:headers-shape-gate";
1136
+ var GATE_PRIORITY = 50;
1137
+ function bindWebhookHeadersShapeGate(engine, logger) {
1138
+ if (typeof _optionalChain([engine, 'optionalAccess', _135 => _135.registerHook]) !== "function") return;
1139
+ const handler = (ctx) => {
1140
+ assertWritableWebhookHeaders(_optionalChain([ctx, 'optionalAccess', _136 => _136.input, 'optionalAccess', _137 => _137.data]));
1141
+ };
1142
+ for (const event of ["beforeInsert", "beforeUpdate"]) {
1143
+ engine.registerHook(event, handler, {
1144
+ object: WEBHOOK_OBJECT,
1145
+ packageId: WEBHOOK_HEADERS_GATE_PACKAGE,
1146
+ priority: GATE_PRIORITY
1147
+ });
1148
+ }
1149
+ _optionalChain([logger, 'optionalAccess', _138 => _138.info, 'optionalCall', _139 => _139("[webhook] headers_secret shape gate bound (refuses non-flat-string-map plaintext)")]);
1150
+ }
1151
+ function unbindWebhookHeadersShapeGate(engine) {
1152
+ if (typeof _optionalChain([engine, 'optionalAccess', _140 => _140.unregisterHooksByPackage]) === "function") {
1153
+ engine.unregisterHooksByPackage(WEBHOOK_HEADERS_GATE_PACKAGE);
1154
+ }
1155
+ }
1156
+
1065
1157
  // src/webhook-outbox-plugin.ts
1066
1158
  var WebhookOutboxPlugin = class {
1067
1159
  constructor(options = {}) {
@@ -1092,7 +1184,7 @@ var WebhookOutboxPlugin = class {
1092
1184
  scope: "system",
1093
1185
  name: "Webhook Schemas",
1094
1186
  description: "Registers sys_webhook (configuration). Deliveries use messaging's sys_http_delivery outbox.",
1095
- objects: [_chunkQ4FEMGD6cjs.SysWebhook],
1187
+ objects: [_chunkDRHJ2M45cjs.SysWebhook],
1096
1188
  navigationContributions: [
1097
1189
  {
1098
1190
  app: "setup",
@@ -1106,7 +1198,7 @@ var WebhookOutboxPlugin = class {
1106
1198
  ]
1107
1199
  });
1108
1200
  } else {
1109
- _optionalChain([ctx, 'access', _125 => _125.logger, 'access', _126 => _126.warn, 'optionalCall', _127 => _127(
1201
+ _optionalChain([ctx, 'access', _141 => _141.logger, 'access', _142 => _142.warn, 'optionalCall', _143 => _143(
1110
1202
  "[webhook-outbox] manifest service unavailable \u2014 sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin."
1111
1203
  )]);
1112
1204
  }
@@ -1115,12 +1207,12 @@ var WebhookOutboxPlugin = class {
1115
1207
  try {
1116
1208
  const i18n = ctx.getService("i18n");
1117
1209
  if (i18n && typeof i18n.loadTranslations === "function") {
1118
- const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-H5ZYI6YP.cjs")));
1210
+ const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-IHRALWSP.cjs")));
1119
1211
  for (const [locale, data] of Object.entries(WebhooksTranslations)) {
1120
1212
  i18n.loadTranslations(locale, data);
1121
1213
  }
1122
1214
  }
1123
- } catch (e10) {
1215
+ } catch (e12) {
1124
1216
  }
1125
1217
  });
1126
1218
  }
@@ -1132,20 +1224,48 @@ var WebhookOutboxPlugin = class {
1132
1224
  this.registerAdminRoutes(ctx);
1133
1225
  });
1134
1226
  }
1135
- _optionalChain([ctx, 'access', _128 => _128.logger, 'access', _129 => _129.info, 'optionalCall', _130 => _130("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
1227
+ _optionalChain([ctx, 'access', _144 => _144.logger, 'access', _145 => _145.info, 'optionalCall', _146 => _146("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
1136
1228
  autoEnqueue: autoEnqueueOpt !== false
1137
1229
  })]);
1138
1230
  }
1139
- async dispose() {
1140
- await _optionalChain([this, 'access', _131 => _131.autoEnqueuer, 'optionalAccess', _132 => _132.stop, 'call', _133 => _133()]);
1231
+ /**
1232
+ * Teardown the kernel's ONLY teardown hook.
1233
+ *
1234
+ * [#10772] This body used to be spelled `dispose()`. `Plugin`
1235
+ * (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)` and
1236
+ * `destroy?()` and no `dispose()`, and `ObjectKernel.performShutdown()` /
1237
+ * `LiteKernel.destroy()` walk the plugins in reverse calling
1238
+ * `plugin.destroy()` — so after `await kernel.shutdown()` had RESOLVED the
1239
+ * auto-enqueuer was still running and both engine hooks were still bound.
1240
+ * Measured on the same revision: `dispose()` had ZERO callers anywhere in
1241
+ * the repo, so this teardown had never run in any process at all.
1242
+ *
1243
+ * Idempotent: `boundEngine` is cleared as it is unbound, so a second
1244
+ * teardown is a no-op rather than a second unbind.
1245
+ */
1246
+ async destroy() {
1247
+ await _optionalChain([this, 'access', _147 => _147.autoEnqueuer, 'optionalAccess', _148 => _148.stop, 'call', _149 => _149()]);
1141
1248
  if (this.boundEngine) {
1142
1249
  try {
1143
1250
  unbindWebhookProvenanceStamp(this.boundEngine);
1144
- } catch (e11) {
1251
+ } catch (e13) {
1252
+ }
1253
+ try {
1254
+ unbindWebhookHeadersShapeGate(this.boundEngine);
1255
+ } catch (e14) {
1145
1256
  }
1146
1257
  this.boundEngine = void 0;
1147
1258
  }
1148
1259
  }
1260
+ /**
1261
+ * Retained alias for {@link destroy}. Kept because it is public API of an
1262
+ * exported class: an embedder may have learned to call it directly
1263
+ * precisely BECAUSE the kernel never did, and deleting it would break them.
1264
+ * Same signature, same return type — a direct caller sees no change.
1265
+ */
1266
+ async dispose() {
1267
+ await this.destroy();
1268
+ }
1149
1269
  getMessaging(ctx) {
1150
1270
  const svc = this.tryGetService(ctx, ["messaging"]);
1151
1271
  return svc && typeof svc.enqueueHttp === "function" ? svc : void 0;
@@ -1165,28 +1285,29 @@ var WebhookOutboxPlugin = class {
1165
1285
  async bootDeclaredWebhooks(ctx) {
1166
1286
  const engine = this.tryGetService(ctx, ["objectql", "data"]);
1167
1287
  if (!engine) {
1168
- _optionalChain([ctx, 'access', _134 => _134.logger, 'access', _135 => _135.warn, 'optionalCall', _136 => _136("[webhook] declared-webhook bootstrap skipped \u2014 no data engine available")]);
1288
+ _optionalChain([ctx, 'access', _150 => _150.logger, 'access', _151 => _151.warn, 'optionalCall', _152 => _152("[webhook] declared-webhook bootstrap skipped \u2014 no data engine available")]);
1169
1289
  return;
1170
1290
  }
1171
1291
  this.boundEngine = engine;
1172
1292
  bindWebhookProvenanceStamp(engine, ctx.logger);
1293
+ bindWebhookHeadersShapeGate(engine, ctx.logger);
1173
1294
  let metadataService;
1174
1295
  try {
1175
1296
  metadataService = ctx.getService("metadata");
1176
- } catch (e12) {
1297
+ } catch (e15) {
1177
1298
  }
1178
1299
  try {
1179
1300
  await bootstrapDeclaredWebhooks(engine, metadataService, ctx.logger);
1180
1301
  } catch (err) {
1181
- _optionalChain([ctx, 'access', _137 => _137.logger, 'access', _138 => _138.warn, 'optionalCall', _139 => _139("[webhook] declared-webhook bootstrap failed (dispatcher still serves admin rows)", {
1182
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _140 => _140.message]), () => ( String(err)))
1302
+ _optionalChain([ctx, 'access', _153 => _153.logger, 'access', _154 => _154.warn, 'optionalCall', _155 => _155("[webhook] declared-webhook bootstrap failed (dispatcher still serves admin rows)", {
1303
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _156 => _156.message]), () => ( String(err)))
1183
1304
  })]);
1184
1305
  }
1185
1306
  try {
1186
1307
  await migrateLegacyWebhookSecrets(engine, ctx.logger);
1187
1308
  } catch (err) {
1188
- _optionalChain([ctx, 'access', _141 => _141.logger, 'access', _142 => _142.warn, 'optionalCall', _143 => _143("[webhook] legacy signing-secret sweep failed (rows left unchanged)", {
1189
- error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _144 => _144.message]), () => ( String(err)))
1309
+ _optionalChain([ctx, 'access', _157 => _157.logger, 'access', _158 => _158.warn, 'optionalCall', _159 => _159("[webhook] legacy signing-secret sweep failed (rows left unchanged)", {
1310
+ error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _160 => _160.message]), () => ( String(err)))
1190
1311
  })]);
1191
1312
  }
1192
1313
  }
@@ -1196,14 +1317,14 @@ var WebhookOutboxPlugin = class {
1196
1317
  const realtime = this.tryGetService(ctx, ["realtime"]);
1197
1318
  const messaging = this.getMessaging(ctx);
1198
1319
  if (!engine || !realtime || !messaging) {
1199
- _optionalChain([ctx, 'access', _145 => _145.logger, 'access', _146 => _146.warn, 'optionalCall', _147 => _147(
1320
+ _optionalChain([ctx, 'access', _161 => _161.logger, 'access', _162 => _162.warn, 'optionalCall', _163 => _163(
1200
1321
  "[webhook-auto-enqueuer] disabled \u2014 ObjectQL, Realtime, or Messaging service not available",
1201
1322
  { hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging }
1202
1323
  )]);
1203
1324
  return;
1204
1325
  }
1205
1326
  if (!messaging.isHttpDeliveryReady()) {
1206
- _optionalChain([ctx, 'access', _148 => _148.logger, 'access', _149 => _149.warn, 'optionalCall', _150 => _150(
1327
+ _optionalChain([ctx, 'access', _164 => _164.logger, 'access', _165 => _165.warn, 'optionalCall', _166 => _166(
1207
1328
  "[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) \u2014 webhook deliveries will not be durable"
1208
1329
  )]);
1209
1330
  }
@@ -1217,7 +1338,7 @@ var WebhookOutboxPlugin = class {
1217
1338
  );
1218
1339
  await this.autoEnqueuer.start();
1219
1340
  ctx.registerService("webhook.autoEnqueuer", this.autoEnqueuer);
1220
- _optionalChain([ctx, 'access', _151 => _151.logger, 'access', _152 => _152.info, 'optionalCall', _153 => _153("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
1341
+ _optionalChain([ctx, 'access', _167 => _167.logger, 'access', _168 => _168.info, 'optionalCall', _169 => _169("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
1221
1342
  }
1222
1343
  /**
1223
1344
  * [#8069] Register {@link createWebhookRedeliverGuard} with messaging, so
@@ -1233,7 +1354,7 @@ var WebhookOutboxPlugin = class {
1233
1354
  */
1234
1355
  installRedeliverGuard(ctx, messaging, engine, subscriptionsObject) {
1235
1356
  if (typeof messaging.registerRedeliverGuard !== "function") {
1236
- _optionalChain([ctx, 'access', _154 => _154.logger, 'access', _155 => _155.error, 'optionalCall', _156 => _156(
1357
+ _optionalChain([ctx, 'access', _170 => _170.logger, 'access', _171 => _171.error, 'optionalCall', _172 => _172(
1237
1358
  "[webhook-outbox] messaging service exposes no registerRedeliverGuard() \u2014 redelivery of a webhook whose signing configuration is gone CANNOT be refused, so an operator pressing redeliver may send a delivery that can no longer be authenticated (#7799, #8069). The POST /api/v1/webhooks/redeliver endpoint is reachable by any authenticated user. Fix: upgrade @objectstack/service-messaging to a build that implements registerRedeliverGuard."
1238
1359
  )]);
1239
1360
  return;
@@ -1242,35 +1363,49 @@ var WebhookOutboxPlugin = class {
1242
1363
  "webhook",
1243
1364
  createWebhookRedeliverGuard(engine, subscriptionsObject)
1244
1365
  );
1245
- _optionalChain([ctx, 'access', _157 => _157.logger, 'access', _158 => _158.debug, 'optionalCall', _159 => _159("[webhook-outbox] redeliver guard installed for source=webhook")]);
1366
+ _optionalChain([ctx, 'access', _173 => _173.logger, 'access', _174 => _174.debug, 'optionalCall', _175 => _175("[webhook-outbox] redeliver guard installed for source=webhook")]);
1246
1367
  }
1247
1368
  tryGetService(ctx, names) {
1248
1369
  for (const n of names) {
1249
1370
  try {
1250
1371
  const svc = ctx.getService(n);
1251
1372
  if (svc) return svc;
1252
- } catch (e13) {
1373
+ } catch (e16) {
1253
1374
  }
1254
1375
  }
1255
1376
  return void 0;
1256
1377
  }
1257
1378
  /**
1258
1379
  * Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is
1259
- * available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is the
1260
- * better-auth session cookie — every authenticated user counts.
1380
+ * available. Delegates to `messaging.redeliverHttp(deliveryId, …)`. Auth is
1381
+ * the better-auth session cookie — every authenticated user counts.
1382
+ *
1383
+ * [#10740] Which is precisely why the caller's ACTIVE ORGANIZATION is
1384
+ * resolved here and threaded into the call. `sys_http_delivery` is
1385
+ * tenant-scoped, and this is the one door on it a request can reach: an
1386
+ * unscoped replay from here is an authenticated user reaching another
1387
+ * organization's delivery row on a walled deployment. With the tenant
1388
+ * threaded, a row outside the caller's organization is simply not found.
1389
+ *
1390
+ * ⚠️ A session with no active organization threads `undefined`, and the
1391
+ * driver's tenant-audit line then fires for that write. That is deliberate:
1392
+ * the deployment could not tell us who is asking, and reporting the gap is
1393
+ * the correct outcome. ⛔ It is never repaired with `bypassTenantAudit`,
1394
+ * which would silence the report without closing anything.
1261
1395
  */
1262
1396
  registerAdminRoutes(ctx) {
1263
1397
  const http = this.tryGetService(ctx, ["http-server"]);
1264
1398
  if (!http || typeof http.getRawApp !== "function") {
1265
- _optionalChain([ctx, 'access', _160 => _160.logger, 'access', _161 => _161.debug, 'optionalCall', _162 => _162("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
1399
+ _optionalChain([ctx, 'access', _176 => _176.logger, 'access', _177 => _177.debug, 'optionalCall', _178 => _178("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
1266
1400
  return;
1267
1401
  }
1268
1402
  const rawApp = http.getRawApp();
1269
1403
  const messaging = this.getMessaging(ctx);
1270
1404
  if (!rawApp || !messaging) return;
1271
1405
  rawApp.post("/api/v1/webhooks/redeliver", async (c) => {
1272
- const userId = await this.resolveSessionUserId(ctx, c);
1273
- if (!userId) {
1406
+ const session = await this.resolveSession(ctx, c);
1407
+ const userId = _optionalChain([session, 'optionalAccess', _179 => _179.user, 'optionalAccess', _180 => _180.id]);
1408
+ if (typeof userId !== "string" || userId.length === 0) {
1274
1409
  return c.json(
1275
1410
  { success: false, error: { code: "UNAUTHENTICATED", message: "Sign in to redeliver webhook deliveries." } },
1276
1411
  401
@@ -1279,10 +1414,10 @@ var WebhookOutboxPlugin = class {
1279
1414
  let body;
1280
1415
  try {
1281
1416
  body = await c.req.json();
1282
- } catch (e14) {
1417
+ } catch (e17) {
1283
1418
  return c.json({ success: false, error: { code: "INVALID_REQUEST", message: "Request body must be JSON." } }, 400);
1284
1419
  }
1285
- const deliveryId = typeof _optionalChain([body, 'optionalAccess', _163 => _163.deliveryId]) === "string" ? body.deliveryId.trim() : "";
1420
+ const deliveryId = typeof _optionalChain([body, 'optionalAccess', _181 => _181.deliveryId]) === "string" ? body.deliveryId.trim() : "";
1286
1421
  if (!deliveryId) {
1287
1422
  return c.json(
1288
1423
  { success: false, error: { code: "MISSING_REQUIRED_FIELD", message: "Body must include `deliveryId: string`." } },
@@ -1290,27 +1425,40 @@ var WebhookOutboxPlugin = class {
1290
1425
  );
1291
1426
  }
1292
1427
  try {
1293
- const row = await messaging.redeliverHttp(deliveryId);
1294
- _optionalChain([ctx, 'access', _164 => _164.logger, 'access', _165 => _165.info, 'optionalCall', _166 => _166("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId })]);
1428
+ const activeOrg = _optionalChain([session, 'optionalAccess', _182 => _182.session, 'optionalAccess', _183 => _183.activeOrganizationId]);
1429
+ const tenantId = typeof activeOrg === "string" && activeOrg.length > 0 ? activeOrg : void 0;
1430
+ const row = await messaging.redeliverHttp(deliveryId, { tenantId });
1431
+ _optionalChain([ctx, 'access', _184 => _184.logger, 'access', _185 => _185.info, 'optionalCall', _186 => _186("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId, tenantId })]);
1295
1432
  return c.json({ success: true, data: { id: row.id, status: row.status } });
1296
1433
  } catch (err) {
1297
- const code = _optionalChain([err, 'optionalAccess', _167 => _167.code]);
1434
+ const code = _optionalChain([err, 'optionalAccess', _187 => _187.code]);
1298
1435
  if (code === "RESOURCE_NOT_FOUND") {
1299
1436
  return c.json({ success: false, error: { code, message: err.message } }, 404);
1300
1437
  }
1301
1438
  if (code === "DELIVERY_NOT_ELIGIBLE" || code === "DELIVERY_NEVER_SENT") {
1302
1439
  return c.json({ success: false, error: { code, message: err.message } }, 409);
1303
1440
  }
1304
- _optionalChain([ctx, 'access', _168 => _168.logger, 'access', _169 => _169.error, 'optionalCall', _170 => _170("[webhook-outbox] redeliver failed", err)]);
1441
+ _optionalChain([ctx, 'access', _188 => _188.logger, 'access', _189 => _189.error, 'optionalCall', _190 => _190("[webhook-outbox] redeliver failed", err)]);
1305
1442
  return c.json(
1306
- { success: false, error: { code: "INTERNAL_ERROR", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _171 => _171.message]), () => ( String(err))) } },
1443
+ { success: false, error: { code: "INTERNAL_ERROR", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _191 => _191.message]), () => ( String(err))) } },
1307
1444
  500
1308
1445
  );
1309
1446
  }
1310
1447
  });
1311
- _optionalChain([ctx, 'access', _172 => _172.logger, 'access', _173 => _173.info, 'optionalCall', _174 => _174("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
1448
+ _optionalChain([ctx, 'access', _192 => _192.logger, 'access', _193 => _193.info, 'optionalCall', _194 => _194("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
1312
1449
  }
1313
- async resolveSessionUserId(ctx, c) {
1450
+ /**
1451
+ * [#10740] The better-auth session envelope (`{ user, session }`) for this
1452
+ * request, or `undefined`.
1453
+ *
1454
+ * Widened from the previous `resolveSessionUserId` because the route now
1455
+ * needs two facts from ONE lookup: who is asking (`user.id`, the
1456
+ * authentication gate) and which organization they are asking as
1457
+ * (`session.activeOrganizationId`, the tenant threaded into the write).
1458
+ * Resolving them separately would mean two `getSession` calls that can
1459
+ * disagree.
1460
+ */
1461
+ async resolveSession(ctx, c) {
1314
1462
  try {
1315
1463
  const authService = this.tryGetService(ctx, ["auth"]);
1316
1464
  if (!authService) return void 0;
@@ -1318,11 +1466,9 @@ var WebhookOutboxPlugin = class {
1318
1466
  if (!api && typeof authService.getApi === "function") {
1319
1467
  api = await authService.getApi();
1320
1468
  }
1321
- if (!_optionalChain([api, 'optionalAccess', _175 => _175.getSession])) return void 0;
1322
- const session = await api.getSession({ headers: c.req.raw.headers });
1323
- const uid2 = _optionalChain([session, 'optionalAccess', _176 => _176.user, 'optionalAccess', _177 => _177.id]);
1324
- return typeof uid2 === "string" && uid2.length > 0 ? uid2 : void 0;
1325
- } catch (e15) {
1469
+ if (!_optionalChain([api, 'optionalAccess', _195 => _195.getSession])) return void 0;
1470
+ return await api.getSession({ headers: c.req.raw.headers });
1471
+ } catch (e18) {
1326
1472
  return void 0;
1327
1473
  }
1328
1474
  }
@@ -1334,5 +1480,11 @@ var WebhookOutboxPlugin = class {
1334
1480
 
1335
1481
 
1336
1482
 
1337
- exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkQ4FEMGD6cjs.SysWebhook; exports.WEBHOOK_HEADERS_FIELD = WEBHOOK_HEADERS_FIELD; exports.WEBHOOK_SECRET_FIELD = WEBHOOK_SECRET_FIELD; exports.WebhookOutboxPlugin = WebhookOutboxPlugin; exports.migrateLegacyWebhookSecrets = migrateLegacyWebhookSecrets;
1483
+
1484
+
1485
+
1486
+
1487
+
1488
+
1489
+ exports.AutoEnqueuer = AutoEnqueuer; exports.SysWebhook = _chunkDRHJ2M45cjs.SysWebhook; exports.WEBHOOK_HEADERS_FIELD = WEBHOOK_HEADERS_FIELD; exports.WEBHOOK_HEADERS_SHAPE_REFUSAL_CODE = WEBHOOK_HEADERS_SHAPE_REFUSAL_CODE; exports.WEBHOOK_HEADERS_SHAPE_REFUSAL_STATUS = WEBHOOK_HEADERS_SHAPE_REFUSAL_STATUS; exports.WEBHOOK_SECRET_FIELD = WEBHOOK_SECRET_FIELD; exports.WebhookHeadersShapeError = WebhookHeadersShapeError; exports.WebhookOutboxPlugin = WebhookOutboxPlugin; exports.assertWritableWebhookHeaders = assertWritableWebhookHeaders; exports.bindWebhookHeadersShapeGate = bindWebhookHeadersShapeGate; exports.migrateLegacyWebhookSecrets = migrateLegacyWebhookSecrets; exports.unbindWebhookHeadersShapeGate = unbindWebhookHeadersShapeGate;
1338
1490
  //# sourceMappingURL=index.cjs.map