@objectstack/plugin-webhooks 17.1.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 +212 -0
- package/dist/index.cjs +141 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -5
- package/dist/index.d.ts +74 -5
- package/dist/index.js +81 -32
- package/dist/index.js.map +1 -1
- package/dist/schema.d.cts +7 -0
- package/dist/schema.d.ts +7 -0
- package/package.json +6 -6
package/dist/index.d.cts
CHANGED
|
@@ -32,7 +32,15 @@ type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;
|
|
|
32
32
|
*/
|
|
33
33
|
interface OptionalLogger {
|
|
34
34
|
info?(msg: string, meta?: unknown): void;
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The GUARANTEED fallback channel (#9754). `error` stays optional — hosts do
|
|
37
|
+
* inject reduced sinks — so `warn` is where a durability report lands when
|
|
38
|
+
* `error` is absent, and a fallback that may itself be missing is not a
|
|
39
|
+
* fallback. Call sites keep the `logger?.warn?.(…)` spelling as the backstop
|
|
40
|
+
* for hosts the TYPE cannot reach; `SweepLogger` in plugin-email's
|
|
41
|
+
* `outbox-sweep.ts` carries the full reasoning and the measurement.
|
|
42
|
+
*/
|
|
43
|
+
warn(msg: string, meta?: unknown): void;
|
|
36
44
|
debug?(msg: string, meta?: unknown): void;
|
|
37
45
|
error?(msg: string, err?: unknown, meta?: unknown): void;
|
|
38
46
|
}
|
|
@@ -129,7 +137,22 @@ declare class AutoEnqueuer {
|
|
|
129
137
|
private readonly subscriptions;
|
|
130
138
|
private readonly subscriptionsObject;
|
|
131
139
|
private readonly refreshIntervalMs;
|
|
132
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Optional, and deliberately NOT defaulted to `{}` (#10556).
|
|
142
|
+
*
|
|
143
|
+
* `OptionalLogger` guarantees a `warn` channel under #9754, so `{}` stopped being a
|
|
144
|
+
* legal value of the type — which is the gate working: an empty object is a
|
|
145
|
+
* sink that declares it can report and then discards everything. The repair
|
|
146
|
+
* is to say what is TRUE — there may be no logger at all — rather than to
|
|
147
|
+
* mint a sink that lies. Runtime behaviour is unchanged in both directions:
|
|
148
|
+
* absent logger and `{}` both printed nothing before, and print nothing now.
|
|
149
|
+
*
|
|
150
|
+
* ⛔ What this deliberately does NOT decide: whether an absent host sink should
|
|
151
|
+
* instead default to a `console`-backed one. That is the open design call the
|
|
152
|
+
* #9754 ledger records against `plugin-security`'s `= {}` field, and it is a
|
|
153
|
+
* maintainer decision — not something to settle here to make a checker green.
|
|
154
|
+
*/
|
|
155
|
+
private readonly logger?;
|
|
133
156
|
private subId;
|
|
134
157
|
private subIdSelfHeal;
|
|
135
158
|
private refreshTimer;
|
|
@@ -404,6 +427,28 @@ declare class WebhookOutboxPlugin implements Plugin {
|
|
|
404
427
|
private boundEngine;
|
|
405
428
|
constructor(options?: WebhookOutboxPluginOptions);
|
|
406
429
|
init(ctx: PluginContext): Promise<void>;
|
|
430
|
+
/**
|
|
431
|
+
* Teardown — the kernel's ONLY teardown hook.
|
|
432
|
+
*
|
|
433
|
+
* [#10772] This body used to be spelled `dispose()`. `Plugin`
|
|
434
|
+
* (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)` and
|
|
435
|
+
* `destroy?()` and no `dispose()`, and `ObjectKernel.performShutdown()` /
|
|
436
|
+
* `LiteKernel.destroy()` walk the plugins in reverse calling
|
|
437
|
+
* `plugin.destroy()` — so after `await kernel.shutdown()` had RESOLVED the
|
|
438
|
+
* auto-enqueuer was still running and both engine hooks were still bound.
|
|
439
|
+
* Measured on the same revision: `dispose()` had ZERO callers anywhere in
|
|
440
|
+
* the repo, so this teardown had never run in any process at all.
|
|
441
|
+
*
|
|
442
|
+
* Idempotent: `boundEngine` is cleared as it is unbound, so a second
|
|
443
|
+
* teardown is a no-op rather than a second unbind.
|
|
444
|
+
*/
|
|
445
|
+
destroy(): Promise<void>;
|
|
446
|
+
/**
|
|
447
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
448
|
+
* exported class: an embedder may have learned to call it directly
|
|
449
|
+
* precisely BECAUSE the kernel never did, and deleting it would break them.
|
|
450
|
+
* Same signature, same return type — a direct caller sees no change.
|
|
451
|
+
*/
|
|
407
452
|
dispose(): Promise<void>;
|
|
408
453
|
private getMessaging;
|
|
409
454
|
/**
|
|
@@ -436,11 +481,35 @@ declare class WebhookOutboxPlugin implements Plugin {
|
|
|
436
481
|
private tryGetService;
|
|
437
482
|
/**
|
|
438
483
|
* Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is
|
|
439
|
-
* available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is
|
|
440
|
-
* better-auth session cookie — every authenticated user counts.
|
|
484
|
+
* available. Delegates to `messaging.redeliverHttp(deliveryId, …)`. Auth is
|
|
485
|
+
* the better-auth session cookie — every authenticated user counts.
|
|
486
|
+
*
|
|
487
|
+
* [#10740] Which is precisely why the caller's ACTIVE ORGANIZATION is
|
|
488
|
+
* resolved here and threaded into the call. `sys_http_delivery` is
|
|
489
|
+
* tenant-scoped, and this is the one door on it a request can reach: an
|
|
490
|
+
* unscoped replay from here is an authenticated user reaching another
|
|
491
|
+
* organization's delivery row on a walled deployment. With the tenant
|
|
492
|
+
* threaded, a row outside the caller's organization is simply not found.
|
|
493
|
+
*
|
|
494
|
+
* ⚠️ A session with no active organization threads `undefined`, and the
|
|
495
|
+
* driver's tenant-audit line then fires for that write. That is deliberate:
|
|
496
|
+
* the deployment could not tell us who is asking, and reporting the gap is
|
|
497
|
+
* the correct outcome. ⛔ It is never repaired with `bypassTenantAudit`,
|
|
498
|
+
* which would silence the report without closing anything.
|
|
441
499
|
*/
|
|
442
500
|
private registerAdminRoutes;
|
|
443
|
-
|
|
501
|
+
/**
|
|
502
|
+
* [#10740] The better-auth session envelope (`{ user, session }`) for this
|
|
503
|
+
* request, or `undefined`.
|
|
504
|
+
*
|
|
505
|
+
* Widened from the previous `resolveSessionUserId` because the route now
|
|
506
|
+
* needs two facts from ONE lookup: who is asking (`user.id`, the
|
|
507
|
+
* authentication gate) and which organization they are asking as
|
|
508
|
+
* (`session.activeOrganizationId`, the tenant threaded into the write).
|
|
509
|
+
* Resolving them separately would mean two `getSession` calls that can
|
|
510
|
+
* disagree.
|
|
511
|
+
*/
|
|
512
|
+
private resolveSession;
|
|
444
513
|
}
|
|
445
514
|
|
|
446
515
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -32,7 +32,15 @@ type HttpEnqueueFn = (input: EnqueueHttpInput) => Promise<string>;
|
|
|
32
32
|
*/
|
|
33
33
|
interface OptionalLogger {
|
|
34
34
|
info?(msg: string, meta?: unknown): void;
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The GUARANTEED fallback channel (#9754). `error` stays optional — hosts do
|
|
37
|
+
* inject reduced sinks — so `warn` is where a durability report lands when
|
|
38
|
+
* `error` is absent, and a fallback that may itself be missing is not a
|
|
39
|
+
* fallback. Call sites keep the `logger?.warn?.(…)` spelling as the backstop
|
|
40
|
+
* for hosts the TYPE cannot reach; `SweepLogger` in plugin-email's
|
|
41
|
+
* `outbox-sweep.ts` carries the full reasoning and the measurement.
|
|
42
|
+
*/
|
|
43
|
+
warn(msg: string, meta?: unknown): void;
|
|
36
44
|
debug?(msg: string, meta?: unknown): void;
|
|
37
45
|
error?(msg: string, err?: unknown, meta?: unknown): void;
|
|
38
46
|
}
|
|
@@ -129,7 +137,22 @@ declare class AutoEnqueuer {
|
|
|
129
137
|
private readonly subscriptions;
|
|
130
138
|
private readonly subscriptionsObject;
|
|
131
139
|
private readonly refreshIntervalMs;
|
|
132
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Optional, and deliberately NOT defaulted to `{}` (#10556).
|
|
142
|
+
*
|
|
143
|
+
* `OptionalLogger` guarantees a `warn` channel under #9754, so `{}` stopped being a
|
|
144
|
+
* legal value of the type — which is the gate working: an empty object is a
|
|
145
|
+
* sink that declares it can report and then discards everything. The repair
|
|
146
|
+
* is to say what is TRUE — there may be no logger at all — rather than to
|
|
147
|
+
* mint a sink that lies. Runtime behaviour is unchanged in both directions:
|
|
148
|
+
* absent logger and `{}` both printed nothing before, and print nothing now.
|
|
149
|
+
*
|
|
150
|
+
* ⛔ What this deliberately does NOT decide: whether an absent host sink should
|
|
151
|
+
* instead default to a `console`-backed one. That is the open design call the
|
|
152
|
+
* #9754 ledger records against `plugin-security`'s `= {}` field, and it is a
|
|
153
|
+
* maintainer decision — not something to settle here to make a checker green.
|
|
154
|
+
*/
|
|
155
|
+
private readonly logger?;
|
|
133
156
|
private subId;
|
|
134
157
|
private subIdSelfHeal;
|
|
135
158
|
private refreshTimer;
|
|
@@ -404,6 +427,28 @@ declare class WebhookOutboxPlugin implements Plugin {
|
|
|
404
427
|
private boundEngine;
|
|
405
428
|
constructor(options?: WebhookOutboxPluginOptions);
|
|
406
429
|
init(ctx: PluginContext): Promise<void>;
|
|
430
|
+
/**
|
|
431
|
+
* Teardown — the kernel's ONLY teardown hook.
|
|
432
|
+
*
|
|
433
|
+
* [#10772] This body used to be spelled `dispose()`. `Plugin`
|
|
434
|
+
* (`@objectstack/core`'s `types.ts`) declares `init()`, `start?(ctx)` and
|
|
435
|
+
* `destroy?()` and no `dispose()`, and `ObjectKernel.performShutdown()` /
|
|
436
|
+
* `LiteKernel.destroy()` walk the plugins in reverse calling
|
|
437
|
+
* `plugin.destroy()` — so after `await kernel.shutdown()` had RESOLVED the
|
|
438
|
+
* auto-enqueuer was still running and both engine hooks were still bound.
|
|
439
|
+
* Measured on the same revision: `dispose()` had ZERO callers anywhere in
|
|
440
|
+
* the repo, so this teardown had never run in any process at all.
|
|
441
|
+
*
|
|
442
|
+
* Idempotent: `boundEngine` is cleared as it is unbound, so a second
|
|
443
|
+
* teardown is a no-op rather than a second unbind.
|
|
444
|
+
*/
|
|
445
|
+
destroy(): Promise<void>;
|
|
446
|
+
/**
|
|
447
|
+
* Retained alias for {@link destroy}. Kept because it is public API of an
|
|
448
|
+
* exported class: an embedder may have learned to call it directly
|
|
449
|
+
* precisely BECAUSE the kernel never did, and deleting it would break them.
|
|
450
|
+
* Same signature, same return type — a direct caller sees no change.
|
|
451
|
+
*/
|
|
407
452
|
dispose(): Promise<void>;
|
|
408
453
|
private getMessaging;
|
|
409
454
|
/**
|
|
@@ -436,11 +481,35 @@ declare class WebhookOutboxPlugin implements Plugin {
|
|
|
436
481
|
private tryGetService;
|
|
437
482
|
/**
|
|
438
483
|
* Mount POST /api/v1/webhooks/redeliver on the host Hono app, if one is
|
|
439
|
-
* available. Delegates to `messaging.redeliverHttp(deliveryId)`. Auth is
|
|
440
|
-
* better-auth session cookie — every authenticated user counts.
|
|
484
|
+
* available. Delegates to `messaging.redeliverHttp(deliveryId, …)`. Auth is
|
|
485
|
+
* the better-auth session cookie — every authenticated user counts.
|
|
486
|
+
*
|
|
487
|
+
* [#10740] Which is precisely why the caller's ACTIVE ORGANIZATION is
|
|
488
|
+
* resolved here and threaded into the call. `sys_http_delivery` is
|
|
489
|
+
* tenant-scoped, and this is the one door on it a request can reach: an
|
|
490
|
+
* unscoped replay from here is an authenticated user reaching another
|
|
491
|
+
* organization's delivery row on a walled deployment. With the tenant
|
|
492
|
+
* threaded, a row outside the caller's organization is simply not found.
|
|
493
|
+
*
|
|
494
|
+
* ⚠️ A session with no active organization threads `undefined`, and the
|
|
495
|
+
* driver's tenant-audit line then fires for that write. That is deliberate:
|
|
496
|
+
* the deployment could not tell us who is asking, and reporting the gap is
|
|
497
|
+
* the correct outcome. ⛔ It is never repaired with `bypassTenantAudit`,
|
|
498
|
+
* which would silence the report without closing anything.
|
|
441
499
|
*/
|
|
442
500
|
private registerAdminRoutes;
|
|
443
|
-
|
|
501
|
+
/**
|
|
502
|
+
* [#10740] The better-auth session envelope (`{ user, session }`) for this
|
|
503
|
+
* request, or `undefined`.
|
|
504
|
+
*
|
|
505
|
+
* Widened from the previous `resolveSessionUserId` because the route now
|
|
506
|
+
* needs two facts from ONE lookup: who is asking (`user.id`, the
|
|
507
|
+
* authentication gate) and which organization they are asking as
|
|
508
|
+
* (`session.activeOrganizationId`, the tenant threaded into the write).
|
|
509
|
+
* Resolving them separately would mean two `getSession` calls that can
|
|
510
|
+
* disagree.
|
|
511
|
+
*/
|
|
512
|
+
private resolveSession;
|
|
444
513
|
}
|
|
445
514
|
|
|
446
515
|
/**
|
package/dist/index.js
CHANGED
|
@@ -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. */
|
|
@@ -1228,7 +1228,22 @@ var WebhookOutboxPlugin = class {
|
|
|
1228
1228
|
autoEnqueue: autoEnqueueOpt !== false
|
|
1229
1229
|
});
|
|
1230
1230
|
}
|
|
1231
|
-
|
|
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() {
|
|
1232
1247
|
await this.autoEnqueuer?.stop();
|
|
1233
1248
|
if (this.boundEngine) {
|
|
1234
1249
|
try {
|
|
@@ -1242,6 +1257,15 @@ var WebhookOutboxPlugin = class {
|
|
|
1242
1257
|
this.boundEngine = void 0;
|
|
1243
1258
|
}
|
|
1244
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
|
+
}
|
|
1245
1269
|
getMessaging(ctx) {
|
|
1246
1270
|
const svc = this.tryGetService(ctx, ["messaging"]);
|
|
1247
1271
|
return svc && typeof svc.enqueueHttp === "function" ? svc : void 0;
|
|
@@ -1353,8 +1377,21 @@ var WebhookOutboxPlugin = class {
|
|
|
1353
1377
|
}
|
|
1354
1378
|
/**
|
|
1355
1379
|
* 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.
|
|
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.
|
|
1358
1395
|
*/
|
|
1359
1396
|
registerAdminRoutes(ctx) {
|
|
1360
1397
|
const http = this.tryGetService(ctx, ["http-server"]);
|
|
@@ -1366,8 +1403,9 @@ var WebhookOutboxPlugin = class {
|
|
|
1366
1403
|
const messaging = this.getMessaging(ctx);
|
|
1367
1404
|
if (!rawApp || !messaging) return;
|
|
1368
1405
|
rawApp.post("/api/v1/webhooks/redeliver", async (c) => {
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1406
|
+
const session = await this.resolveSession(ctx, c);
|
|
1407
|
+
const userId = session?.user?.id;
|
|
1408
|
+
if (typeof userId !== "string" || userId.length === 0) {
|
|
1371
1409
|
return c.json(
|
|
1372
1410
|
{ success: false, error: { code: "UNAUTHENTICATED", message: "Sign in to redeliver webhook deliveries." } },
|
|
1373
1411
|
401
|
|
@@ -1387,8 +1425,10 @@ var WebhookOutboxPlugin = class {
|
|
|
1387
1425
|
);
|
|
1388
1426
|
}
|
|
1389
1427
|
try {
|
|
1390
|
-
const
|
|
1391
|
-
|
|
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 });
|
|
1392
1432
|
return c.json({ success: true, data: { id: row.id, status: row.status } });
|
|
1393
1433
|
} catch (err) {
|
|
1394
1434
|
const code = err?.code;
|
|
@@ -1407,7 +1447,18 @@ var WebhookOutboxPlugin = class {
|
|
|
1407
1447
|
});
|
|
1408
1448
|
ctx.logger.info?.("[webhook-outbox] redeliver endpoint mounted at POST /api/v1/webhooks/redeliver");
|
|
1409
1449
|
}
|
|
1410
|
-
|
|
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) {
|
|
1411
1462
|
try {
|
|
1412
1463
|
const authService = this.tryGetService(ctx, ["auth"]);
|
|
1413
1464
|
if (!authService) return void 0;
|
|
@@ -1416,9 +1467,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1416
1467
|
api = await authService.getApi();
|
|
1417
1468
|
}
|
|
1418
1469
|
if (!api?.getSession) return void 0;
|
|
1419
|
-
|
|
1420
|
-
const uid2 = session?.user?.id;
|
|
1421
|
-
return typeof uid2 === "string" && uid2.length > 0 ? uid2 : void 0;
|
|
1470
|
+
return await api.getSession({ headers: c.req.raw.headers });
|
|
1422
1471
|
} catch {
|
|
1423
1472
|
return void 0;
|
|
1424
1473
|
}
|