@objectstack/plugin-webhooks 17.1.0 → 17.3.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/CHANGELOG.md +815 -0
- package/dist/index.cjs +156 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -5
- package/dist/index.d.ts +89 -5
- package/dist/index.js +96 -34
- package/dist/index.js.map +1 -1
- package/dist/schema.d.cts +304 -212
- package/dist/schema.d.ts +304 -212
- package/dist/{translations-IAKF6NAP.js → translations-CPXQB2NM.js} +46 -10
- package/dist/translations-CPXQB2NM.js.map +1 -0
- package/dist/{translations-IHRALWSP.cjs → translations-YO6CI7LZ.cjs} +47 -11
- package/dist/translations-YO6CI7LZ.cjs.map +1 -0
- package/package.json +27 -14
- package/dist/translations-IAKF6NAP.js.map +0 -1
- package/dist/translations-IHRALWSP.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -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 =
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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',
|
|
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',
|
|
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',
|
|
431
|
+
const legacy = readLegacySecret(_optionalChain([row, 'optionalAccess', _38 => _38.definition_json]));
|
|
432
432
|
if (legacy) {
|
|
433
|
-
_optionalChain([this, 'access',
|
|
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',
|
|
481
|
+
const legacy = readLegacyHeaders(_optionalChain([row, 'optionalAccess', _42 => _42.definition_json]));
|
|
482
482
|
if (legacy) {
|
|
483
|
-
_optionalChain([this, 'access',
|
|
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',
|
|
523
|
+
err: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _46 => _46.message]), () => ( err))
|
|
524
524
|
};
|
|
525
525
|
if (this.droppedForSecret.has(sub.id)) {
|
|
526
|
-
_optionalChain([this, 'access',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
)]);
|
|
@@ -596,7 +596,11 @@ var AutoEnqueuer = class {
|
|
|
596
596
|
// `headers` and `secret` are both filled by attachCredentials()
|
|
597
597
|
// from their encrypted columns, NOT read off the row — see #7799
|
|
598
598
|
// (secret) and #7986 (headers).
|
|
599
|
-
timeoutMs: defn.timeoutMs
|
|
599
|
+
timeoutMs: defn.timeoutMs,
|
|
600
|
+
// [#13546] The tenant column the kernel provisions on sys_webhook.
|
|
601
|
+
// This cache read is a dispatcher-side unscoped find, so the column
|
|
602
|
+
// comes back for every organization's rows.
|
|
603
|
+
organizationId: row.organization_id ? String(row.organization_id) : void 0
|
|
600
604
|
};
|
|
601
605
|
}
|
|
602
606
|
/**
|
|
@@ -609,11 +613,11 @@ var AutoEnqueuer = class {
|
|
|
609
613
|
handleEvent(event) {
|
|
610
614
|
if (!event.object) return;
|
|
611
615
|
if (event.object === this.subscriptionsObject) return;
|
|
612
|
-
if (_optionalChain([event, 'access',
|
|
616
|
+
if (_optionalChain([event, 'access', _66 => _66.type, 'optionalAccess', _67 => _67.startsWith, 'call', _68 => _68("data.records.")])) {
|
|
613
617
|
this.handleBulkEvent(event);
|
|
614
618
|
return;
|
|
615
619
|
}
|
|
616
|
-
if (!_optionalChain([event, 'access',
|
|
620
|
+
if (!_optionalChain([event, 'access', _69 => _69.type, 'optionalAccess', _70 => _70.startsWith, 'call', _71 => _71("data.record.")])) return;
|
|
617
621
|
const action = event.type.slice("data.record.".length);
|
|
618
622
|
const trigger = mapActionToTrigger(action);
|
|
619
623
|
if (!trigger) return;
|
|
@@ -625,7 +629,7 @@ var AutoEnqueuer = class {
|
|
|
625
629
|
const payload = _nullishCoalesce(event.payload, () => ( {}));
|
|
626
630
|
const recordId = payload.recordId;
|
|
627
631
|
if (typeof recordId !== "string" || recordId === "") {
|
|
628
|
-
_optionalChain([this, 'access',
|
|
632
|
+
_optionalChain([this, 'access', _72 => _72.logger, 'optionalAccess', _73 => _73.warn, 'optionalCall', _74 => _74(
|
|
629
633
|
"[webhook-auto-enqueuer] dropping off-contract data event: payload is not a DataEvent (no top-level string `recordId`) \u2014 fix the producer",
|
|
630
634
|
{ type: event.type, object: event.object }
|
|
631
635
|
)]);
|
|
@@ -650,6 +654,12 @@ var AutoEnqueuer = class {
|
|
|
650
654
|
// subscription, so the delivery path is byte-identical to before.
|
|
651
655
|
undeliverableReason: sub.parkedReason,
|
|
652
656
|
timeoutMs: sub.timeoutMs,
|
|
657
|
+
// [#13546] The delivery row belongs to the SUBSCRIPTION's
|
|
658
|
+
// organization — the one honest tenant in scope on this
|
|
659
|
+
// fire-and-forget path (no request context exists here).
|
|
660
|
+
// Absent for an org-less subscription; the row then lands
|
|
661
|
+
// NULL, the global-row shape.
|
|
662
|
+
organizationId: sub.organizationId,
|
|
653
663
|
// [#3946] Envelope keys are written LAST so the event payload
|
|
654
664
|
// cannot rewrite them. Behaviour-neutral for the engine's own
|
|
655
665
|
// publishers — since #4626 a `data.record.*` payload is a
|
|
@@ -694,7 +704,7 @@ var AutoEnqueuer = class {
|
|
|
694
704
|
const payload = _nullishCoalesce(event.payload, () => ( {}));
|
|
695
705
|
const matched = payload.matched;
|
|
696
706
|
if (typeof matched !== "number" || !Number.isInteger(matched) || matched < 0) {
|
|
697
|
-
_optionalChain([this, 'access',
|
|
707
|
+
_optionalChain([this, 'access', _75 => _75.logger, 'optionalAccess', _76 => _76.warn, 'optionalCall', _77 => _77(
|
|
698
708
|
"[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
709
|
{ type: event.type, object: event.object }
|
|
700
710
|
)]);
|
|
@@ -702,7 +712,7 @@ var AutoEnqueuer = class {
|
|
|
702
712
|
}
|
|
703
713
|
const eventUuid = payload.id;
|
|
704
714
|
if (typeof eventUuid !== "string" || eventUuid === "") {
|
|
705
|
-
_optionalChain([this, 'access',
|
|
715
|
+
_optionalChain([this, 'access', _78 => _78.logger, 'optionalAccess', _79 => _79.warn, 'optionalCall', _80 => _80(
|
|
706
716
|
"[webhook-auto-enqueuer] dropping off-contract bulk data event: payload has no top-level string `id` to dedup on \u2014 fix the producer",
|
|
707
717
|
{ type: event.type, object: event.object }
|
|
708
718
|
)]);
|
|
@@ -724,6 +734,9 @@ var AutoEnqueuer = class {
|
|
|
724
734
|
// an undeliverable row instead of enqueuing a delivery.
|
|
725
735
|
undeliverableReason: sub.parkedReason,
|
|
726
736
|
timeoutMs: sub.timeoutMs,
|
|
737
|
+
// [#13546] See the per-record path — the subscription's own
|
|
738
|
+
// organization, absent for an org-less subscription.
|
|
739
|
+
organizationId: sub.organizationId,
|
|
727
740
|
// [#3946] Envelope keys last so the payload cannot rewrite them.
|
|
728
741
|
payload: {
|
|
729
742
|
...payload,
|
|
@@ -737,9 +750,9 @@ var AutoEnqueuer = class {
|
|
|
737
750
|
}
|
|
738
751
|
handleSelfHealEvent(event) {
|
|
739
752
|
if (event.object !== this.subscriptionsObject) return;
|
|
740
|
-
if (!_optionalChain([event, 'access',
|
|
753
|
+
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
754
|
this.refresh().catch(
|
|
742
|
-
(err) => _optionalChain([this, 'access',
|
|
755
|
+
(err) => _optionalChain([this, 'access', _87 => _87.logger, 'optionalAccess', _88 => _88.warn, 'optionalCall', _89 => _89("[webhook-auto-enqueuer] self-heal refresh failed", err)])
|
|
743
756
|
);
|
|
744
757
|
}
|
|
745
758
|
/** Test / admin accessor. */
|
|
@@ -782,21 +795,21 @@ var _automation = require('@objectstack/spec/automation');
|
|
|
782
795
|
var SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
|
|
783
796
|
function uid(prefix) {
|
|
784
797
|
const g = globalThis;
|
|
785
|
-
if (_optionalChain([g, 'access',
|
|
798
|
+
if (_optionalChain([g, 'access', _90 => _90.crypto, 'optionalAccess', _91 => _91.randomUUID])) return `${prefix}_${g.crypto.randomUUID()}`;
|
|
786
799
|
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
787
800
|
}
|
|
788
801
|
function readDeclared(engine, metadataService, type) {
|
|
789
802
|
try {
|
|
790
|
-
const reg = _optionalChain([engine, 'optionalAccess',
|
|
791
|
-
if (_optionalChain([reg, 'optionalAccess',
|
|
803
|
+
const reg = _optionalChain([engine, 'optionalAccess', _92 => _92._registry]);
|
|
804
|
+
if (_optionalChain([reg, 'optionalAccess', _93 => _93.listItems])) {
|
|
792
805
|
const items = (_nullishCoalesce(reg.listItems(type), () => ( []))).filter(Boolean);
|
|
793
806
|
if (items.length > 0) return items;
|
|
794
807
|
}
|
|
795
808
|
} catch (e7) {
|
|
796
809
|
}
|
|
797
810
|
try {
|
|
798
|
-
const listed = _optionalChain([metadataService, 'optionalAccess',
|
|
799
|
-
const arr = typeof _optionalChain([listed, 'optionalAccess',
|
|
811
|
+
const listed = _optionalChain([metadataService, 'optionalAccess', _94 => _94.list, 'optionalCall', _95 => _95(type)]);
|
|
812
|
+
const arr = typeof _optionalChain([listed, 'optionalAccess', _96 => _96.then]) === "function" ? [] : _nullishCoalesce(listed, () => ( []));
|
|
800
813
|
return Array.isArray(arr) ? arr.filter(Boolean) : [];
|
|
801
814
|
} catch (e8) {
|
|
802
815
|
return [];
|
|
@@ -813,9 +826,9 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
|
|
|
813
826
|
try {
|
|
814
827
|
wh = _automation.WebhookSchema.parse(raw);
|
|
815
828
|
} catch (err) {
|
|
816
|
-
_optionalChain([logger, 'optionalAccess',
|
|
817
|
-
name: _optionalChain([raw, 'optionalAccess',
|
|
818
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
829
|
+
_optionalChain([logger, 'optionalAccess', _97 => _97.warn, 'optionalCall', _98 => _98("[webhook] declared webhook failed validation \u2014 skipped", {
|
|
830
|
+
name: _optionalChain([raw, 'optionalAccess', _99 => _99.name]),
|
|
831
|
+
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _100 => _100.message]), () => ( String(err)))
|
|
819
832
|
})]);
|
|
820
833
|
skipped += 1;
|
|
821
834
|
continue;
|
|
@@ -829,7 +842,7 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
|
|
|
829
842
|
const row = Array.isArray(existing) ? existing[0] : void 0;
|
|
830
843
|
if (row) {
|
|
831
844
|
if (row.managed_by === "admin") {
|
|
832
|
-
_optionalChain([logger, 'optionalAccess',
|
|
845
|
+
_optionalChain([logger, 'optionalAccess', _101 => _101.warn, 'optionalCall', _102 => _102("[webhook] declared name collides with an admin-authored row \u2014 seed skipped", {
|
|
833
846
|
name: wh.name
|
|
834
847
|
})]);
|
|
835
848
|
skipped += 1;
|
|
@@ -881,18 +894,18 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
|
|
|
881
894
|
seeded += 1;
|
|
882
895
|
} catch (err) {
|
|
883
896
|
const protection = isSecretProtectionFailure(err);
|
|
884
|
-
_optionalChain([logger, 'optionalAccess',
|
|
897
|
+
_optionalChain([logger, 'optionalAccess', _103 => _103.warn, 'optionalCall', _104 => _104(
|
|
885
898
|
protection ? "[webhook] declared webhook NOT seeded \u2014 its signing secret cannot be stored encrypted (#7799)" : "[webhook] declared webhook seed failed",
|
|
886
899
|
{
|
|
887
900
|
name: wh.name,
|
|
888
901
|
...protection ? { code: WEBHOOK_SECRET_REFUSAL_CODE, status: WEBHOOK_SECRET_REFUSAL_STATUS } : {},
|
|
889
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
902
|
+
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _105 => _105.message]), () => ( String(err)))
|
|
890
903
|
}
|
|
891
904
|
)]);
|
|
892
905
|
skipped += 1;
|
|
893
906
|
}
|
|
894
907
|
}
|
|
895
|
-
_optionalChain([logger, 'optionalAccess',
|
|
908
|
+
_optionalChain([logger, 'optionalAccess', _106 => _106.info, 'optionalCall', _107 => _107("[webhook] declared webhooks materialized into sys_webhook", {
|
|
896
909
|
seeded,
|
|
897
910
|
skipped,
|
|
898
911
|
total: declared.length
|
|
@@ -901,7 +914,7 @@ async function bootstrapDeclaredWebhooks(engine, metadataService, logger, subscr
|
|
|
901
914
|
}
|
|
902
915
|
async function secretPatch(engine, wh, row, subscriptionsObject) {
|
|
903
916
|
const { secret } = splitWebhookSecret(wh);
|
|
904
|
-
const hasStored = _optionalChain([row, 'optionalAccess',
|
|
917
|
+
const hasStored = _optionalChain([row, 'optionalAccess', _108 => _108[WEBHOOK_SECRET_FIELD]]) != null && row[WEBHOOK_SECRET_FIELD] !== "";
|
|
905
918
|
if (!secret) return hasStored ? { [WEBHOOK_SECRET_FIELD]: null } : {};
|
|
906
919
|
if (!hasStored || !canResolveSecrets(engine)) return { [WEBHOOK_SECRET_FIELD]: secret };
|
|
907
920
|
try {
|
|
@@ -943,16 +956,16 @@ async function migrateLegacyWebhookSecrets(engine, logger, subscriptionsObject =
|
|
|
943
956
|
let rows;
|
|
944
957
|
try {
|
|
945
958
|
const found = await engine.find(subscriptionsObject, SYSTEM_QUERY);
|
|
946
|
-
rows = Array.isArray(found) ? found : _nullishCoalesce(_optionalChain([found, 'optionalAccess',
|
|
959
|
+
rows = Array.isArray(found) ? found : _nullishCoalesce(_optionalChain([found, 'optionalAccess', _109 => _109.data]), () => ( []));
|
|
947
960
|
} catch (err) {
|
|
948
|
-
_optionalChain([logger, 'optionalAccess',
|
|
961
|
+
_optionalChain([logger, 'optionalAccess', _110 => _110.warn, 'optionalCall', _111 => _111("[webhook] legacy secret sweep skipped \u2014 could not read subscriptions", {
|
|
949
962
|
object: subscriptionsObject,
|
|
950
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
963
|
+
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _112 => _112.message]), () => ( String(err)))
|
|
951
964
|
})]);
|
|
952
965
|
return out;
|
|
953
966
|
}
|
|
954
967
|
for (const row of rows) {
|
|
955
|
-
if (!_optionalChain([row, 'optionalAccess',
|
|
968
|
+
if (!_optionalChain([row, 'optionalAccess', _113 => _113.id])) continue;
|
|
956
969
|
const legacySecret = readLegacySecret(row.definition_json);
|
|
957
970
|
const legacyHeaders = readLegacyHeaders(row.definition_json);
|
|
958
971
|
if (!legacySecret && !legacyHeaders) continue;
|
|
@@ -973,20 +986,20 @@ async function migrateLegacyWebhookSecrets(engine, logger, subscriptionsObject =
|
|
|
973
986
|
out.failed += 1;
|
|
974
987
|
const protection = isSecretProtectionFailure(err);
|
|
975
988
|
const what = legacySecret && legacyHeaders ? "signing secret and custom headers" : legacySecret ? "signing secret" : "custom headers";
|
|
976
|
-
_optionalChain([logger, 'optionalAccess',
|
|
989
|
+
_optionalChain([logger, 'optionalAccess', _114 => _114.warn, 'optionalCall', _115 => _115(
|
|
977
990
|
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
991
|
{
|
|
979
992
|
name: _nullishCoalesce(row.name, () => ( row.id)),
|
|
980
993
|
id: row.id,
|
|
981
994
|
code: WEBHOOK_SECRET_REFUSAL_CODE,
|
|
982
995
|
status: WEBHOOK_SECRET_REFUSAL_STATUS,
|
|
983
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
996
|
+
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _116 => _116.message]), () => ( String(err)))
|
|
984
997
|
}
|
|
985
998
|
)]);
|
|
986
999
|
}
|
|
987
1000
|
}
|
|
988
1001
|
if (out.found > 0) {
|
|
989
|
-
_optionalChain([logger, 'optionalAccess',
|
|
1002
|
+
_optionalChain([logger, 'optionalAccess', _117 => _117.info, 'optionalCall', _118 => _118("[webhook] legacy cleartext credentials swept into sys_secret", { ...out })]);
|
|
990
1003
|
}
|
|
991
1004
|
return out;
|
|
992
1005
|
}
|
|
@@ -1024,14 +1037,14 @@ function createWebhookRedeliverGuard(engine, subscriptionsObject = WEBHOOK_OBJEC
|
|
|
1024
1037
|
var WEBHOOK_PROVENANCE_PACKAGE = "plugin-webhooks:provenance";
|
|
1025
1038
|
var SYSTEM_CTX3 = { isSystem: true, positions: [], permissions: [] };
|
|
1026
1039
|
function bindWebhookProvenanceStamp(engine, logger) {
|
|
1027
|
-
if (typeof _optionalChain([engine, 'optionalAccess',
|
|
1040
|
+
if (typeof _optionalChain([engine, 'optionalAccess', _119 => _119.registerHook]) !== "function") return;
|
|
1028
1041
|
engine.registerHook(
|
|
1029
1042
|
"beforeUpdate",
|
|
1030
1043
|
async (ctx) => {
|
|
1031
|
-
if (_optionalChain([ctx, 'optionalAccess',
|
|
1032
|
-
const id = _nullishCoalesce(_optionalChain([ctx, 'optionalAccess',
|
|
1044
|
+
if (_optionalChain([ctx, 'optionalAccess', _120 => _120.session, 'optionalAccess', _121 => _121.isSystem])) return;
|
|
1045
|
+
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
1046
|
if (!id) return;
|
|
1034
|
-
const data = _optionalChain([ctx, 'optionalAccess',
|
|
1047
|
+
const data = _optionalChain([ctx, 'optionalAccess', _127 => _127.input, 'optionalAccess', _128 => _128.data]);
|
|
1035
1048
|
if (!data || typeof data !== "object") return;
|
|
1036
1049
|
try {
|
|
1037
1050
|
const rows = await engine.find("sys_webhook", {
|
|
@@ -1046,18 +1059,18 @@ function bindWebhookProvenanceStamp(engine, logger) {
|
|
|
1046
1059
|
data.customized = true;
|
|
1047
1060
|
}
|
|
1048
1061
|
} catch (err) {
|
|
1049
|
-
_optionalChain([logger, 'optionalAccess',
|
|
1062
|
+
_optionalChain([logger, 'optionalAccess', _129 => _129.warn, 'optionalCall', _130 => _130("[webhook] provenance stamp failed (edit proceeds unstamped)", {
|
|
1050
1063
|
id,
|
|
1051
|
-
error: _optionalChain([err, 'optionalAccess',
|
|
1064
|
+
error: _optionalChain([err, 'optionalAccess', _131 => _131.message])
|
|
1052
1065
|
})]);
|
|
1053
1066
|
}
|
|
1054
1067
|
},
|
|
1055
1068
|
{ object: "sys_webhook", packageId: WEBHOOK_PROVENANCE_PACKAGE, priority: 150 }
|
|
1056
1069
|
);
|
|
1057
|
-
_optionalChain([logger, 'optionalAccess',
|
|
1070
|
+
_optionalChain([logger, 'optionalAccess', _132 => _132.info, 'optionalCall', _133 => _133("[webhook] provenance stamp hook bound")]);
|
|
1058
1071
|
}
|
|
1059
1072
|
function unbindWebhookProvenanceStamp(engine) {
|
|
1060
|
-
if (typeof _optionalChain([engine, 'optionalAccess',
|
|
1073
|
+
if (typeof _optionalChain([engine, 'optionalAccess', _134 => _134.unregisterHooksByPackage]) === "function") {
|
|
1061
1074
|
engine.unregisterHooksByPackage(WEBHOOK_PROVENANCE_PACKAGE);
|
|
1062
1075
|
}
|
|
1063
1076
|
}
|
|
@@ -1135,9 +1148,9 @@ function assertWritableWebhookHeaders(data, object = WEBHOOK_OBJECT, field = WEB
|
|
|
1135
1148
|
var WEBHOOK_HEADERS_GATE_PACKAGE = "plugin-webhooks:headers-shape-gate";
|
|
1136
1149
|
var GATE_PRIORITY = 50;
|
|
1137
1150
|
function bindWebhookHeadersShapeGate(engine, logger) {
|
|
1138
|
-
if (typeof _optionalChain([engine, 'optionalAccess',
|
|
1151
|
+
if (typeof _optionalChain([engine, 'optionalAccess', _135 => _135.registerHook]) !== "function") return;
|
|
1139
1152
|
const handler = (ctx) => {
|
|
1140
|
-
assertWritableWebhookHeaders(_optionalChain([ctx, 'optionalAccess',
|
|
1153
|
+
assertWritableWebhookHeaders(_optionalChain([ctx, 'optionalAccess', _136 => _136.input, 'optionalAccess', _137 => _137.data]));
|
|
1141
1154
|
};
|
|
1142
1155
|
for (const event of ["beforeInsert", "beforeUpdate"]) {
|
|
1143
1156
|
engine.registerHook(event, handler, {
|
|
@@ -1146,10 +1159,10 @@ function bindWebhookHeadersShapeGate(engine, logger) {
|
|
|
1146
1159
|
priority: GATE_PRIORITY
|
|
1147
1160
|
});
|
|
1148
1161
|
}
|
|
1149
|
-
_optionalChain([logger, 'optionalAccess',
|
|
1162
|
+
_optionalChain([logger, 'optionalAccess', _138 => _138.info, 'optionalCall', _139 => _139("[webhook] headers_secret shape gate bound (refuses non-flat-string-map plaintext)")]);
|
|
1150
1163
|
}
|
|
1151
1164
|
function unbindWebhookHeadersShapeGate(engine) {
|
|
1152
|
-
if (typeof _optionalChain([engine, 'optionalAccess',
|
|
1165
|
+
if (typeof _optionalChain([engine, 'optionalAccess', _140 => _140.unregisterHooksByPackage]) === "function") {
|
|
1153
1166
|
engine.unregisterHooksByPackage(WEBHOOK_HEADERS_GATE_PACKAGE);
|
|
1154
1167
|
}
|
|
1155
1168
|
}
|
|
@@ -1198,7 +1211,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1198
1211
|
]
|
|
1199
1212
|
});
|
|
1200
1213
|
} else {
|
|
1201
|
-
_optionalChain([ctx, 'access',
|
|
1214
|
+
_optionalChain([ctx, 'access', _141 => _141.logger, 'access', _142 => _142.warn, 'optionalCall', _143 => _143(
|
|
1202
1215
|
"[webhook-outbox] manifest service unavailable \u2014 sys_webhook will NOT appear in REST or Studio nav. Register MetadataService before WebhookOutboxPlugin."
|
|
1203
1216
|
)]);
|
|
1204
1217
|
}
|
|
@@ -1207,7 +1220,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1207
1220
|
try {
|
|
1208
1221
|
const i18n = ctx.getService("i18n");
|
|
1209
1222
|
if (i18n && typeof i18n.loadTranslations === "function") {
|
|
1210
|
-
const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-
|
|
1223
|
+
const { WebhooksTranslations } = await Promise.resolve().then(() => _interopRequireWildcard(require("./translations-YO6CI7LZ.cjs")));
|
|
1211
1224
|
for (const [locale, data] of Object.entries(WebhooksTranslations)) {
|
|
1212
1225
|
i18n.loadTranslations(locale, data);
|
|
1213
1226
|
}
|
|
@@ -1224,12 +1237,27 @@ var WebhookOutboxPlugin = class {
|
|
|
1224
1237
|
this.registerAdminRoutes(ctx);
|
|
1225
1238
|
});
|
|
1226
1239
|
}
|
|
1227
|
-
_optionalChain([ctx, 'access',
|
|
1240
|
+
_optionalChain([ctx, 'access', _144 => _144.logger, 'access', _145 => _145.info, 'optionalCall', _146 => _146("[webhook-outbox] initialised (delivery via shared messaging HTTP outbox)", {
|
|
1228
1241
|
autoEnqueue: autoEnqueueOpt !== false
|
|
1229
1242
|
})]);
|
|
1230
1243
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1244
|
+
/**
|
|
1245
|
+
* Teardown — the kernel's ONLY teardown hook.
|
|
1246
|
+
*
|
|
1247
|
+
* [#10772] This body used to be spelled `dispose()`. `Plugin`
|
|
1248
|
+
* (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)` and
|
|
1249
|
+
* `destroy?()` and no `dispose()`, and `ObjectKernel.performShutdown()` /
|
|
1250
|
+
* `LiteKernel.destroy()` walk the plugins in reverse calling
|
|
1251
|
+
* `plugin.destroy()` — so after `await kernel.shutdown()` had RESOLVED the
|
|
1252
|
+
* auto-enqueuer was still running and both engine hooks were still bound.
|
|
1253
|
+
* Measured on the same revision: `dispose()` had ZERO callers anywhere in
|
|
1254
|
+
* the repo, so this teardown had never run in any process at all.
|
|
1255
|
+
*
|
|
1256
|
+
* Idempotent: `boundEngine` is cleared as it is unbound, so a second
|
|
1257
|
+
* teardown is a no-op rather than a second unbind.
|
|
1258
|
+
*/
|
|
1259
|
+
async destroy() {
|
|
1260
|
+
await _optionalChain([this, 'access', _147 => _147.autoEnqueuer, 'optionalAccess', _148 => _148.stop, 'call', _149 => _149()]);
|
|
1233
1261
|
if (this.boundEngine) {
|
|
1234
1262
|
try {
|
|
1235
1263
|
unbindWebhookProvenanceStamp(this.boundEngine);
|
|
@@ -1242,6 +1270,15 @@ var WebhookOutboxPlugin = class {
|
|
|
1242
1270
|
this.boundEngine = void 0;
|
|
1243
1271
|
}
|
|
1244
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
1275
|
+
* exported class: an embedder may have learned to call it directly
|
|
1276
|
+
* precisely BECAUSE the kernel never did, and deleting it would break them.
|
|
1277
|
+
* Same signature, same return type — a direct caller sees no change.
|
|
1278
|
+
*/
|
|
1279
|
+
async dispose() {
|
|
1280
|
+
await this.destroy();
|
|
1281
|
+
}
|
|
1245
1282
|
getMessaging(ctx) {
|
|
1246
1283
|
const svc = this.tryGetService(ctx, ["messaging"]);
|
|
1247
1284
|
return svc && typeof svc.enqueueHttp === "function" ? svc : void 0;
|
|
@@ -1261,7 +1298,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1261
1298
|
async bootDeclaredWebhooks(ctx) {
|
|
1262
1299
|
const engine = this.tryGetService(ctx, ["objectql", "data"]);
|
|
1263
1300
|
if (!engine) {
|
|
1264
|
-
_optionalChain([ctx, 'access',
|
|
1301
|
+
_optionalChain([ctx, 'access', _150 => _150.logger, 'access', _151 => _151.warn, 'optionalCall', _152 => _152("[webhook] declared-webhook bootstrap skipped \u2014 no data engine available")]);
|
|
1265
1302
|
return;
|
|
1266
1303
|
}
|
|
1267
1304
|
this.boundEngine = engine;
|
|
@@ -1275,15 +1312,15 @@ var WebhookOutboxPlugin = class {
|
|
|
1275
1312
|
try {
|
|
1276
1313
|
await bootstrapDeclaredWebhooks(engine, metadataService, ctx.logger);
|
|
1277
1314
|
} catch (err) {
|
|
1278
|
-
_optionalChain([ctx, 'access',
|
|
1279
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
1315
|
+
_optionalChain([ctx, 'access', _153 => _153.logger, 'access', _154 => _154.warn, 'optionalCall', _155 => _155("[webhook] declared-webhook bootstrap failed (dispatcher still serves admin rows)", {
|
|
1316
|
+
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _156 => _156.message]), () => ( String(err)))
|
|
1280
1317
|
})]);
|
|
1281
1318
|
}
|
|
1282
1319
|
try {
|
|
1283
1320
|
await migrateLegacyWebhookSecrets(engine, ctx.logger);
|
|
1284
1321
|
} catch (err) {
|
|
1285
|
-
_optionalChain([ctx, 'access',
|
|
1286
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
1322
|
+
_optionalChain([ctx, 'access', _157 => _157.logger, 'access', _158 => _158.warn, 'optionalCall', _159 => _159("[webhook] legacy signing-secret sweep failed (rows left unchanged)", {
|
|
1323
|
+
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _160 => _160.message]), () => ( String(err)))
|
|
1287
1324
|
})]);
|
|
1288
1325
|
}
|
|
1289
1326
|
}
|
|
@@ -1293,14 +1330,14 @@ var WebhookOutboxPlugin = class {
|
|
|
1293
1330
|
const realtime = this.tryGetService(ctx, ["realtime"]);
|
|
1294
1331
|
const messaging = this.getMessaging(ctx);
|
|
1295
1332
|
if (!engine || !realtime || !messaging) {
|
|
1296
|
-
_optionalChain([ctx, 'access',
|
|
1333
|
+
_optionalChain([ctx, 'access', _161 => _161.logger, 'access', _162 => _162.warn, 'optionalCall', _163 => _163(
|
|
1297
1334
|
"[webhook-auto-enqueuer] disabled \u2014 ObjectQL, Realtime, or Messaging service not available",
|
|
1298
1335
|
{ hasEngine: !!engine, hasRealtime: !!realtime, hasMessaging: !!messaging }
|
|
1299
1336
|
)]);
|
|
1300
1337
|
return;
|
|
1301
1338
|
}
|
|
1302
1339
|
if (!messaging.isHttpDeliveryReady()) {
|
|
1303
|
-
_optionalChain([ctx, 'access',
|
|
1340
|
+
_optionalChain([ctx, 'access', _164 => _164.logger, 'access', _165 => _165.warn, 'optionalCall', _166 => _166(
|
|
1304
1341
|
"[webhook-auto-enqueuer] messaging HTTP outbox not ready (no data engine / reliableDelivery off) \u2014 webhook deliveries will not be durable"
|
|
1305
1342
|
)]);
|
|
1306
1343
|
}
|
|
@@ -1314,7 +1351,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1314
1351
|
);
|
|
1315
1352
|
await this.autoEnqueuer.start();
|
|
1316
1353
|
ctx.registerService("webhook.autoEnqueuer", this.autoEnqueuer);
|
|
1317
|
-
_optionalChain([ctx, 'access',
|
|
1354
|
+
_optionalChain([ctx, 'access', _167 => _167.logger, 'access', _168 => _168.info, 'optionalCall', _169 => _169("[webhook-auto-enqueuer] started (enqueues source=webhook onto sys_http_delivery)")]);
|
|
1318
1355
|
}
|
|
1319
1356
|
/**
|
|
1320
1357
|
* [#8069] Register {@link createWebhookRedeliverGuard} with messaging, so
|
|
@@ -1330,7 +1367,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1330
1367
|
*/
|
|
1331
1368
|
installRedeliverGuard(ctx, messaging, engine, subscriptionsObject) {
|
|
1332
1369
|
if (typeof messaging.registerRedeliverGuard !== "function") {
|
|
1333
|
-
_optionalChain([ctx, 'access',
|
|
1370
|
+
_optionalChain([ctx, 'access', _170 => _170.logger, 'access', _171 => _171.error, 'optionalCall', _172 => _172(
|
|
1334
1371
|
"[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."
|
|
1335
1372
|
)]);
|
|
1336
1373
|
return;
|
|
@@ -1339,7 +1376,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1339
1376
|
"webhook",
|
|
1340
1377
|
createWebhookRedeliverGuard(engine, subscriptionsObject)
|
|
1341
1378
|
);
|
|
1342
|
-
_optionalChain([ctx, 'access',
|
|
1379
|
+
_optionalChain([ctx, 'access', _173 => _173.logger, 'access', _174 => _174.debug, 'optionalCall', _175 => _175("[webhook-outbox] redeliver guard installed for source=webhook")]);
|
|
1343
1380
|
}
|
|
1344
1381
|
tryGetService(ctx, names) {
|
|
1345
1382
|
for (const n of names) {
|
|
@@ -1353,21 +1390,35 @@ var WebhookOutboxPlugin = class {
|
|
|
1353
1390
|
}
|
|
1354
1391
|
/**
|
|
1355
1392
|
* Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is
|
|
1356
|
-
* available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is
|
|
1357
|
-
* better-auth session cookie — every authenticated user counts.
|
|
1393
|
+
* available. Delegates to `messaging.redeliverHttp(deliveryId, …)`. Auth is
|
|
1394
|
+
* the better-auth session cookie — every authenticated user counts.
|
|
1395
|
+
*
|
|
1396
|
+
* [#10740] Which is precisely why the caller's ACTIVE ORGANIZATION is
|
|
1397
|
+
* resolved here and threaded into the call. `sys_http_delivery` is
|
|
1398
|
+
* tenant-scoped, and this is the one door on it a request can reach: an
|
|
1399
|
+
* unscoped replay from here is an authenticated user reaching another
|
|
1400
|
+
* organization's delivery row on a walled deployment. With the tenant
|
|
1401
|
+
* threaded, a row outside the caller's organization is simply not found.
|
|
1402
|
+
*
|
|
1403
|
+
* ⚠️ A session with no active organization threads `undefined`, and the
|
|
1404
|
+
* driver's tenant-audit line then fires for that write. That is deliberate:
|
|
1405
|
+
* the deployment could not tell us who is asking, and reporting the gap is
|
|
1406
|
+
* the correct outcome. ⛔ It is never repaired with `bypassTenantAudit`,
|
|
1407
|
+
* which would silence the report without closing anything.
|
|
1358
1408
|
*/
|
|
1359
1409
|
registerAdminRoutes(ctx) {
|
|
1360
1410
|
const http = this.tryGetService(ctx, ["http-server"]);
|
|
1361
1411
|
if (!http || typeof http.getRawApp !== "function") {
|
|
1362
|
-
_optionalChain([ctx, 'access',
|
|
1412
|
+
_optionalChain([ctx, 'access', _176 => _176.logger, 'access', _177 => _177.debug, 'optionalCall', _178 => _178("[webhook-outbox] HTTP server not available; redeliver endpoint not mounted")]);
|
|
1363
1413
|
return;
|
|
1364
1414
|
}
|
|
1365
1415
|
const rawApp = http.getRawApp();
|
|
1366
1416
|
const messaging = this.getMessaging(ctx);
|
|
1367
1417
|
if (!rawApp || !messaging) return;
|
|
1368
1418
|
rawApp.post("/api/v1/webhooks/redeliver", async (c) => {
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1419
|
+
const session = await this.resolveSession(ctx, c);
|
|
1420
|
+
const userId = _optionalChain([session, 'optionalAccess', _179 => _179.user, 'optionalAccess', _180 => _180.id]);
|
|
1421
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
1371
1422
|
return c.json(
|
|
1372
1423
|
{ success: false, error: { code: "UNAUTHENTICATED", message: "Sign in to redeliver webhook deliveries." } },
|
|
1373
1424
|
401
|
|
@@ -1379,7 +1430,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1379
1430
|
} catch (e17) {
|
|
1380
1431
|
return c.json({ success: false, error: { code: "INVALID_REQUEST", message: "Request body must be JSON." } }, 400);
|
|
1381
1432
|
}
|
|
1382
|
-
const deliveryId = typeof _optionalChain([body, 'optionalAccess',
|
|
1433
|
+
const deliveryId = typeof _optionalChain([body, 'optionalAccess', _181 => _181.deliveryId]) === "string" ? body.deliveryId.trim() : "";
|
|
1383
1434
|
if (!deliveryId) {
|
|
1384
1435
|
return c.json(
|
|
1385
1436
|
{ success: false, error: { code: "MISSING_REQUIRED_FIELD", message: "Body must include `deliveryId: string`." } },
|
|
@@ -1387,27 +1438,40 @@ var WebhookOutboxPlugin = class {
|
|
|
1387
1438
|
);
|
|
1388
1439
|
}
|
|
1389
1440
|
try {
|
|
1390
|
-
const
|
|
1391
|
-
|
|
1441
|
+
const activeOrg = _optionalChain([session, 'optionalAccess', _182 => _182.session, 'optionalAccess', _183 => _183.activeOrganizationId]);
|
|
1442
|
+
const tenantId = typeof activeOrg === "string" && activeOrg.length > 0 ? activeOrg : void 0;
|
|
1443
|
+
const row = await messaging.redeliverHttp(deliveryId, { tenantId });
|
|
1444
|
+
_optionalChain([ctx, 'access', _184 => _184.logger, 'access', _185 => _185.info, 'optionalCall', _186 => _186("[webhook-outbox] redelivered", { deliveryId, requestedBy: userId, tenantId })]);
|
|
1392
1445
|
return c.json({ success: true, data: { id: row.id, status: row.status } });
|
|
1393
1446
|
} catch (err) {
|
|
1394
|
-
const code = _optionalChain([err, 'optionalAccess',
|
|
1447
|
+
const code = _optionalChain([err, 'optionalAccess', _187 => _187.code]);
|
|
1395
1448
|
if (code === "RESOURCE_NOT_FOUND") {
|
|
1396
1449
|
return c.json({ success: false, error: { code, message: err.message } }, 404);
|
|
1397
1450
|
}
|
|
1398
1451
|
if (code === "DELIVERY_NOT_ELIGIBLE" || code === "DELIVERY_NEVER_SENT") {
|
|
1399
1452
|
return c.json({ success: false, error: { code, message: err.message } }, 409);
|
|
1400
1453
|
}
|
|
1401
|
-
_optionalChain([ctx, 'access',
|
|
1454
|
+
_optionalChain([ctx, 'access', _188 => _188.logger, 'access', _189 => _189.error, 'optionalCall', _190 => _190("[webhook-outbox] redeliver failed", err)]);
|
|
1402
1455
|
return c.json(
|
|
1403
|
-
{ success: false, error: { code: "INTERNAL_ERROR", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
1456
|
+
{ success: false, error: { code: "INTERNAL_ERROR", message: _nullishCoalesce(_optionalChain([err, 'optionalAccess', _191 => _191.message]), () => ( String(err))) } },
|
|
1404
1457
|
500
|
|
1405
1458
|
);
|
|
1406
1459
|
}
|
|
1407
1460
|
});
|
|
1408
|
-
_optionalChain([ctx, 'access',
|
|
1461
|
+
_optionalChain([ctx, 'access', _192 => _192.logger, 'access', _193 => _193.info, 'optionalCall', _194 => _194("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver")]);
|
|
1409
1462
|
}
|
|
1410
|
-
|
|
1463
|
+
/**
|
|
1464
|
+
* [#10740] The better-auth session envelope (`{ user, session }`) for this
|
|
1465
|
+
* request, or `undefined`.
|
|
1466
|
+
*
|
|
1467
|
+
* Widened from the previous `resolveSessionUserId` because the route now
|
|
1468
|
+
* needs two facts from ONE lookup: who is asking (`user.id`, the
|
|
1469
|
+
* authentication gate) and which organization they are asking as
|
|
1470
|
+
* (`session.activeOrganizationId`, the tenant threaded into the write).
|
|
1471
|
+
* Resolving them separately would mean two `getSession` calls that can
|
|
1472
|
+
* disagree.
|
|
1473
|
+
*/
|
|
1474
|
+
async resolveSession(ctx, c) {
|
|
1411
1475
|
try {
|
|
1412
1476
|
const authService = this.tryGetService(ctx, ["auth"]);
|
|
1413
1477
|
if (!authService) return void 0;
|
|
@@ -1415,10 +1479,8 @@ var WebhookOutboxPlugin = class {
|
|
|
1415
1479
|
if (!api && typeof authService.getApi === "function") {
|
|
1416
1480
|
api = await authService.getApi();
|
|
1417
1481
|
}
|
|
1418
|
-
if (!_optionalChain([api, 'optionalAccess',
|
|
1419
|
-
|
|
1420
|
-
const uid2 = _optionalChain([session, 'optionalAccess', _182 => _182.user, 'optionalAccess', _183 => _183.id]);
|
|
1421
|
-
return typeof uid2 === "string" && uid2.length > 0 ? uid2 : void 0;
|
|
1482
|
+
if (!_optionalChain([api, 'optionalAccess', _195 => _195.getSession])) return void 0;
|
|
1483
|
+
return await api.getSession({ headers: c.req.raw.headers });
|
|
1422
1484
|
} catch (e18) {
|
|
1423
1485
|
return void 0;
|
|
1424
1486
|
}
|