@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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SysWebhook
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XERWWQKN.js";
|
|
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 = opts.subscriptionsObject ?? "sys_webhook";
|
|
196
196
|
this.refreshIntervalMs = opts.refreshIntervalMs ?? 6e4;
|
|
197
|
-
this.logger = opts.logger
|
|
197
|
+
this.logger = opts.logger;
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
200
|
* Load the subscription cache and start listening for events.
|
|
@@ -219,7 +219,7 @@ var AutoEnqueuer = class {
|
|
|
219
219
|
if (this.refreshIntervalMs > 0) {
|
|
220
220
|
this.refreshTimer = setInterval(() => {
|
|
221
221
|
this.refresh().catch(
|
|
222
|
-
(err) => this.logger
|
|
222
|
+
(err) => this.logger?.warn?.("[webhook-auto-enqueuer] periodic refresh failed", err)
|
|
223
223
|
);
|
|
224
224
|
}, this.refreshIntervalMs);
|
|
225
225
|
this.refreshTimer.unref?.();
|
|
@@ -252,7 +252,7 @@ var AutoEnqueuer = class {
|
|
|
252
252
|
rearmAfterCryptoRegistered() {
|
|
253
253
|
const inFlight = this.refreshing ?? Promise.resolve();
|
|
254
254
|
void inFlight.catch(() => void 0).then(() => this.running ? this.refresh() : void 0).catch(
|
|
255
|
-
(err) => this.logger
|
|
255
|
+
(err) => this.logger?.warn?.(
|
|
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
|
-
this.logger
|
|
279
|
+
this.logger?.warn?.(
|
|
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
|
-
this.logger
|
|
303
|
+
this.logger?.debug?.("[webhook-auto-enqueuer] cache refreshed", {
|
|
304
304
|
objects: this.subscriptions.size,
|
|
305
305
|
rows: rows.length
|
|
306
306
|
});
|
|
@@ -371,14 +371,14 @@ var AutoEnqueuer = class {
|
|
|
371
371
|
reportWriteFailure(sub, eventId, err, verb) {
|
|
372
372
|
const meta = { webhook: sub.name, eventId, err: err?.message ?? err };
|
|
373
373
|
if (!sub.parkedReason) {
|
|
374
|
-
this.logger
|
|
374
|
+
this.logger?.warn?.(`[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
|
|
379
|
-
this.logger
|
|
378
|
+
if (typeof this.logger?.error === "function") {
|
|
379
|
+
this.logger?.error(message, err, meta);
|
|
380
380
|
} else {
|
|
381
|
-
this.logger
|
|
381
|
+
this.logger?.warn?.(message, meta);
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
384
|
park(sub, err, credential) {
|
|
@@ -430,7 +430,7 @@ var AutoEnqueuer = class {
|
|
|
430
430
|
}
|
|
431
431
|
const legacy = readLegacySecret(row?.definition_json);
|
|
432
432
|
if (legacy) {
|
|
433
|
-
this.logger
|
|
433
|
+
this.logger?.warn?.(
|
|
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
|
);
|
|
@@ -480,7 +480,7 @@ var AutoEnqueuer = class {
|
|
|
480
480
|
}
|
|
481
481
|
const legacy = readLegacyHeaders(row?.definition_json);
|
|
482
482
|
if (legacy) {
|
|
483
|
-
this.logger
|
|
483
|
+
this.logger?.warn?.(
|
|
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
|
);
|
|
@@ -523,7 +523,7 @@ var AutoEnqueuer = class {
|
|
|
523
523
|
err: err?.message ?? err
|
|
524
524
|
};
|
|
525
525
|
if (this.droppedForSecret.has(sub.id)) {
|
|
526
|
-
this.logger
|
|
526
|
+
this.logger?.debug?.(
|
|
527
527
|
`[webhook-auto-enqueuer] webhook '${sub.name}' is still dropped for an unresolvable ${credential.noun} (${credential.issues})`,
|
|
528
528
|
meta
|
|
529
529
|
);
|
|
@@ -531,10 +531,10 @@ 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
|
|
535
|
-
this.logger
|
|
534
|
+
if (typeof this.logger?.error === "function") {
|
|
535
|
+
this.logger?.error(message, err, meta);
|
|
536
536
|
} else {
|
|
537
|
-
this.logger
|
|
537
|
+
this.logger?.warn?.(message, meta);
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
540
|
parseRow(row) {
|
|
@@ -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
|
-
this.logger
|
|
562
|
+
this.logger?.warn?.(
|
|
563
563
|
`[webhook-auto-enqueuer] webhook '${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
|
-
this.logger
|
|
571
|
+
this.logger?.warn?.(
|
|
572
572
|
`[webhook-auto-enqueuer] webhook '${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
|
);
|
|
@@ -625,7 +625,7 @@ var AutoEnqueuer = class {
|
|
|
625
625
|
const payload = event.payload ?? {};
|
|
626
626
|
const recordId = payload.recordId;
|
|
627
627
|
if (typeof recordId !== "string" || recordId === "") {
|
|
628
|
-
this.logger
|
|
628
|
+
this.logger?.warn?.(
|
|
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 = event.payload ?? {};
|
|
695
695
|
const matched = payload.matched;
|
|
696
696
|
if (typeof matched !== "number" || !Number.isInteger(matched) || matched < 0) {
|
|
697
|
-
this.logger
|
|
697
|
+
this.logger?.warn?.(
|
|
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
|
-
this.logger
|
|
705
|
+
this.logger?.warn?.(
|
|
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
|
);
|
|
@@ -739,7 +739,7 @@ var AutoEnqueuer = class {
|
|
|
739
739
|
if (event.object !== this.subscriptionsObject) return;
|
|
740
740
|
if (!event.type?.startsWith("data.record.") && !event.type?.startsWith("data.records.")) return;
|
|
741
741
|
this.refresh().catch(
|
|
742
|
-
(err) => this.logger
|
|
742
|
+
(err) => this.logger?.warn?.("[webhook-auto-enqueuer] self-heal refresh failed", err)
|
|
743
743
|
);
|
|
744
744
|
}
|
|
745
745
|
/** Test / admin accessor. */
|
|
@@ -1062,6 +1062,98 @@ function unbindWebhookProvenanceStamp(engine) {
|
|
|
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 {
|
|
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 {
|
|
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 engine?.registerHook !== "function") return;
|
|
1139
|
+
const handler = (ctx) => {
|
|
1140
|
+
assertWritableWebhookHeaders(ctx?.input?.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
|
+
logger?.info?.("[webhook] headers_secret shape gate bound (refuses non-flat-string-map plaintext)");
|
|
1150
|
+
}
|
|
1151
|
+
function unbindWebhookHeadersShapeGate(engine) {
|
|
1152
|
+
if (typeof engine?.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 = {}) {
|
|
@@ -1115,7 +1207,7 @@ 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 import("./translations-
|
|
1210
|
+
const { WebhooksTranslations } = await import("./translations-IAKF6NAP.js");
|
|
1119
1211
|
for (const [locale, data] of Object.entries(WebhooksTranslations)) {
|
|
1120
1212
|
i18n.loadTranslations(locale, data);
|
|
1121
1213
|
}
|
|
@@ -1136,16 +1228,44 @@ var WebhookOutboxPlugin = class {
|
|
|
1136
1228
|
autoEnqueue: autoEnqueueOpt !== false
|
|
1137
1229
|
});
|
|
1138
1230
|
}
|
|
1139
|
-
|
|
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() {
|
|
1140
1247
|
await this.autoEnqueuer?.stop();
|
|
1141
1248
|
if (this.boundEngine) {
|
|
1142
1249
|
try {
|
|
1143
1250
|
unbindWebhookProvenanceStamp(this.boundEngine);
|
|
1144
1251
|
} catch {
|
|
1145
1252
|
}
|
|
1253
|
+
try {
|
|
1254
|
+
unbindWebhookHeadersShapeGate(this.boundEngine);
|
|
1255
|
+
} catch {
|
|
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;
|
|
@@ -1170,6 +1290,7 @@ var WebhookOutboxPlugin = class {
|
|
|
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");
|
|
@@ -1256,8 +1377,21 @@ var WebhookOutboxPlugin = class {
|
|
|
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"]);
|
|
@@ -1269,8 +1403,9 @@ var WebhookOutboxPlugin = class {
|
|
|
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 = session?.user?.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
|
|
@@ -1290,8 +1425,10 @@ var WebhookOutboxPlugin = class {
|
|
|
1290
1425
|
);
|
|
1291
1426
|
}
|
|
1292
1427
|
try {
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1428
|
+
const activeOrg = session?.session?.activeOrganizationId;
|
|
1429
|
+
const tenantId = typeof activeOrg === "string" && activeOrg.length > 0 ? activeOrg : void 0;
|
|
1430
|
+
const row = await messaging.redeliverHttp(deliveryId, { tenantId });
|
|
1431
|
+
ctx.logger.info?.("[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
1434
|
const code = err?.code;
|
|
@@ -1310,7 +1447,18 @@ var WebhookOutboxPlugin = class {
|
|
|
1310
1447
|
});
|
|
1311
1448
|
ctx.logger.info?.("[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;
|
|
@@ -1319,9 +1467,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1319
1467
|
api = await authService.getApi();
|
|
1320
1468
|
}
|
|
1321
1469
|
if (!api?.getSession) return void 0;
|
|
1322
|
-
|
|
1323
|
-
const uid2 = session?.user?.id;
|
|
1324
|
-
return typeof uid2 === "string" && uid2.length > 0 ? uid2 : void 0;
|
|
1470
|
+
return await api.getSession({ headers: c.req.raw.headers });
|
|
1325
1471
|
} catch {
|
|
1326
1472
|
return void 0;
|
|
1327
1473
|
}
|
|
@@ -1331,8 +1477,14 @@ export {
|
|
|
1331
1477
|
AutoEnqueuer,
|
|
1332
1478
|
SysWebhook,
|
|
1333
1479
|
WEBHOOK_HEADERS_FIELD,
|
|
1480
|
+
WEBHOOK_HEADERS_SHAPE_REFUSAL_CODE,
|
|
1481
|
+
WEBHOOK_HEADERS_SHAPE_REFUSAL_STATUS,
|
|
1334
1482
|
WEBHOOK_SECRET_FIELD,
|
|
1483
|
+
WebhookHeadersShapeError,
|
|
1335
1484
|
WebhookOutboxPlugin,
|
|
1336
|
-
|
|
1485
|
+
assertWritableWebhookHeaders,
|
|
1486
|
+
bindWebhookHeadersShapeGate,
|
|
1487
|
+
migrateLegacyWebhookSecrets,
|
|
1488
|
+
unbindWebhookHeadersShapeGate
|
|
1337
1489
|
};
|
|
1338
1490
|
//# sourceMappingURL=index.js.map
|