@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/CHANGELOG.md +426 -0
- package/dist/{chunk-Q4FEMGD6.cjs → chunk-DRHJ2M45.cjs} +61 -2
- package/dist/chunk-DRHJ2M45.cjs.map +1 -0
- package/dist/{chunk-GDCWDVDT.js → chunk-XERWWQKN.js} +61 -2
- package/dist/{chunk-GDCWDVDT.js.map → chunk-XERWWQKN.js.map} +1 -1
- package/dist/index.cjs +250 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +140 -6
- package/dist/index.d.ts +140 -6
- package/dist/index.js +187 -35
- package/dist/index.js.map +1 -1
- package/dist/schema.cjs +2 -2
- package/dist/schema.d.cts +1193 -267
- package/dist/schema.d.ts +1193 -267
- package/dist/schema.js +1 -1
- package/dist/{translations-U32QIEPB.js → translations-IAKF6NAP.js} +1 -1
- package/dist/translations-IAKF6NAP.js.map +1 -0
- package/dist/{translations-H5ZYI6YP.cjs → translations-IHRALWSP.cjs} +1 -1
- package/dist/translations-IHRALWSP.cjs.map +1 -0
- package/package.json +7 -7
- package/dist/chunk-Q4FEMGD6.cjs.map +0 -1
- package/dist/translations-H5ZYI6YP.cjs.map +0 -1
- package/dist/translations-U32QIEPB.js.map +0 -1
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
|
|
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 =
|
|
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
|
)]);
|
|
@@ -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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
791
|
-
if (_optionalChain([reg, 'optionalAccess',
|
|
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',
|
|
799
|
-
const arr = typeof _optionalChain([listed, 'optionalAccess',
|
|
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',
|
|
817
|
-
name: _optionalChain([raw, 'optionalAccess',
|
|
818
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
946
|
+
rows = Array.isArray(found) ? found : _nullishCoalesce(_optionalChain([found, 'optionalAccess', _109 => _109.data]), () => ( []));
|
|
947
947
|
} catch (err) {
|
|
948
|
-
_optionalChain([logger, 'optionalAccess',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
1032
|
-
const id = _nullishCoalesce(_optionalChain([ctx, 'optionalAccess',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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: [
|
|
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',
|
|
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-
|
|
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 (
|
|
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',
|
|
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
|
-
|
|
1140
|
-
|
|
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 (
|
|
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',
|
|
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 (
|
|
1297
|
+
} catch (e15) {
|
|
1177
1298
|
}
|
|
1178
1299
|
try {
|
|
1179
1300
|
await bootstrapDeclaredWebhooks(engine, metadataService, ctx.logger);
|
|
1180
1301
|
} catch (err) {
|
|
1181
|
-
_optionalChain([ctx, 'access',
|
|
1182
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
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',
|
|
1189
|
-
error: _nullishCoalesce(_optionalChain([err, 'optionalAccess',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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 (
|
|
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
|
|
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',
|
|
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
|
|
1273
|
-
|
|
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 (
|
|
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',
|
|
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
|
|
1294
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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',
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
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
|