@objectstack/plugin-webhooks 17.2.0 → 17.4.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 +794 -0
- package/dist/{chunk-DRHJ2M45.cjs → chunk-CWE6CIEZ.cjs} +1 -1
- package/dist/chunk-CWE6CIEZ.cjs.map +1 -0
- package/dist/{chunk-XERWWQKN.js → chunk-MEX2VC7T.js} +1 -1
- package/dist/chunk-MEX2VC7T.js.map +1 -0
- package/dist/index.cjs +202 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -2
- package/dist/index.d.ts +105 -2
- package/dist/index.js +144 -24
- package/dist/index.js.map +1 -1
- package/dist/schema.cjs +2 -2
- package/dist/schema.d.cts +357 -249
- package/dist/schema.d.ts +357 -249
- package/dist/schema.js +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/{translations-IAKF6NAP.js → translations-MNRJO53N.js} +46 -10
- package/dist/translations-MNRJO53N.js.map +1 -0
- package/dist/{translations-IHRALWSP.cjs → translations-VX3ONQXN.cjs} +47 -11
- package/dist/translations-VX3ONQXN.cjs.map +1 -0
- package/package.json +27 -14
- package/dist/chunk-DRHJ2M45.cjs.map +0 -1
- package/dist/chunk-XERWWQKN.js.map +0 -1
- package/dist/translations-IAKF6NAP.js.map +0 -1
- package/dist/translations-IHRALWSP.cjs.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -59,6 +59,30 @@ interface CachedSubscription {
|
|
|
59
59
|
headers?: Record<string, string>;
|
|
60
60
|
secret?: string;
|
|
61
61
|
timeoutMs?: number;
|
|
62
|
+
/**
|
|
63
|
+
* [#13546] The subscription's own organization — `sys_webhook` is
|
|
64
|
+
* organization-scoped (#8554), so each row carries the tenant that authored
|
|
65
|
+
* it. Stamped onto every delivery row this subscription produces
|
|
66
|
+
* (`EnqueueHttpInput.organizationId`), which is what makes the
|
|
67
|
+
* cross-organization wall on `redeliver()` (#10740) actually exclude other
|
|
68
|
+
* tenants' rows: a row enqueued without it lands `organization_id = NULL`,
|
|
69
|
+
* the driver's global-row arm, visible to every organization. There is no
|
|
70
|
+
* request context to read here — the enqueuer runs fire-and-forget off the
|
|
71
|
+
* write path — so the subscription row is the one honest source. Absent
|
|
72
|
+
* when the row itself carries no organization (a `single`-posture install):
|
|
73
|
+
* the delivery then lands NULL, which is honest for a subscription that
|
|
74
|
+
* belongs to no organization. Threaded, never fabricated (#11303's rule).
|
|
75
|
+
*
|
|
76
|
+
* [#13566] ALSO the subscription half of the organization MATCH — compared
|
|
77
|
+
* against the organization the producer stamps on the event
|
|
78
|
+
* (`DataEvent.organizationId`, #14970; `BulkDataEvent.organizationId`,
|
|
79
|
+
* #15225 / #15813) before anything is enqueued. See
|
|
80
|
+
* {@link AutoEnqueuer.admitsOrganization} for the matrix; the short form
|
|
81
|
+
* is that a subscription with no organization ownership does not receive
|
|
82
|
+
* an organization-walled event, and a subscription with one receives only
|
|
83
|
+
* its own organization's.
|
|
84
|
+
*/
|
|
85
|
+
organizationId?: string;
|
|
62
86
|
/**
|
|
63
87
|
* [#8069] Set when a credential this subscription needs could not be
|
|
64
88
|
* recovered. The subscription stays CACHED — that is the change — but every
|
|
@@ -101,11 +125,17 @@ interface AutoEnqueuerOptions {
|
|
|
101
125
|
* The handler:
|
|
102
126
|
* 1. Looks up matching subscriptions in an in-memory `Map<object, sub[]>`
|
|
103
127
|
* — O(1) per event, no DB hit on the write path.
|
|
104
|
-
* 2.
|
|
128
|
+
* 2. [#13566] Compares each candidate's own organization with the one the
|
|
129
|
+
* producer stamped on the event — one equality per candidate, no
|
|
130
|
+
* lookup (see {@link admitsOrganization}). On a walled deployment this
|
|
131
|
+
* is what keeps organization A's record events out of organization B's
|
|
132
|
+
* webhook endpoints.
|
|
133
|
+
* 3. Calls `outbox.enqueue()` fire-and-forget for each match. The
|
|
105
134
|
* enqueue itself is a single INSERT, which runs *after* the user's
|
|
106
135
|
* request has already returned.
|
|
107
136
|
*
|
|
108
|
-
* Net cost on the write path: one synchronous Map lookup (~microseconds)
|
|
137
|
+
* Net cost on the write path: one synchronous Map lookup (~microseconds)
|
|
138
|
+
* plus one string comparison per candidate subscription.
|
|
109
139
|
*
|
|
110
140
|
* ## Cache freshness
|
|
111
141
|
* The cache is rebuilt:
|
|
@@ -172,6 +202,17 @@ declare class AutoEnqueuer {
|
|
|
172
202
|
* refresh, forever.
|
|
173
203
|
*/
|
|
174
204
|
private readonly droppedForSecret;
|
|
205
|
+
/**
|
|
206
|
+
* [#13566] Webhook ids whose FIRST organization-dimension refusal has been
|
|
207
|
+
* said out loud (see {@link admitsOrganization}). Same say-once shape as
|
|
208
|
+
* {@link droppedForSecret}, for the same reason: a subscription that
|
|
209
|
+
* cannot receive a class of events must be reported once, with the
|
|
210
|
+
* remedy, and not once per event forever — an org-less `'*'` subscription
|
|
211
|
+
* on a walled deployment would otherwise warn on every write of every
|
|
212
|
+
* organization. Pruned on refresh to the rows still live, so a row that
|
|
213
|
+
* is deleted and re-created reports again.
|
|
214
|
+
*/
|
|
215
|
+
private readonly organizationRefusalReported;
|
|
175
216
|
constructor(engine: IDataEngine, realtime: IRealtimeService, enqueue: HttpEnqueueFn, opts?: AutoEnqueuerOptions);
|
|
176
217
|
/**
|
|
177
218
|
* Load the subscription cache and start listening for events.
|
|
@@ -365,6 +406,68 @@ declare class AutoEnqueuer {
|
|
|
365
406
|
* webhook opts in with `bulk_update` / `bulk_delete`.
|
|
366
407
|
*/
|
|
367
408
|
private handleBulkEvent;
|
|
409
|
+
/**
|
|
410
|
+
* [#13566] The organization dimension of the match — ONE comparison
|
|
411
|
+
* between the subscription's own organization (cached off its
|
|
412
|
+
* `sys_webhook` row, #13546) and the organization the producer stamped on
|
|
413
|
+
* the event (#14970 per record; #15225 / #15813 per batch). ⛔ No lookup:
|
|
414
|
+
* the enqueuer exists to keep this path O(1), and both halves are already
|
|
415
|
+
* in hand — the filter is a comparison, never a resolution.
|
|
416
|
+
*
|
|
417
|
+
* The cells, and why each falls where it does:
|
|
418
|
+
*
|
|
419
|
+
* | subscription | event | verdict |
|
|
420
|
+
* |--------------|--------|------------------------------------------------|
|
|
421
|
+
* | org A | org A | deliver |
|
|
422
|
+
* | org A | org B | not a match — the routine outcome of the new |
|
|
423
|
+
* | | | term, silent like an object-name mismatch |
|
|
424
|
+
* | org A | absent | REFUSE (fail-closed), said once per sub |
|
|
425
|
+
* | none | org A | REFUSE, said once per sub — the ruling's case |
|
|
426
|
+
* | none | absent | deliver — no wall on either side |
|
|
427
|
+
*
|
|
428
|
+
* **`none` × `org A` — the ruling.** A `sys_webhook` row with no
|
|
429
|
+
* organization on a walled deployment (a package-declared row bootstrapped
|
|
430
|
+
* under `isSystem`, a row authored before the column was provisioned)
|
|
431
|
+
* would otherwise receive EVERY organization's records at its URL, signed
|
|
432
|
+
* with its secret — the leak this card is. Maintainer's ruling
|
|
433
|
+
* (2026-09-07), verbatim: *a subscription with no organisation ownership
|
|
434
|
+
* does not fan out — loud refusal, never a silent cross-organisation
|
|
435
|
+
* delivery.* ADR-0131 D1 is why there is no third reading — NULL is not a
|
|
436
|
+
* state, so "no organization" is never "every organization".
|
|
437
|
+
*
|
|
438
|
+
* **`org A` × `absent` — fail-closed, on BOTH paths.** The two event
|
|
439
|
+
* families spell absence with the same key and mean different things by
|
|
440
|
+
* it (`packages/spec/src/api/events.zod.ts`, both members' docs). On a
|
|
441
|
+
* `DataEvent` it is "this record belongs to no organization" — an
|
|
442
|
+
* environment-wide row, an object outside the wall, or (the per-record
|
|
443
|
+
* producer's own docblock, `eventOrganizationId` in
|
|
444
|
+
* `packages/objectql/src/engine.ts`) no row in hand at the publish site,
|
|
445
|
+
* published absent rather than substituting the caller's organization.
|
|
446
|
+
* On a `BulkDataEvent` it is "the producer did not assert one
|
|
447
|
+
* organization for this batch" — a system or cross-membership predicate
|
|
448
|
+
* write — and the contract says outright that a tenant-scoped consumer
|
|
449
|
+
* must not deliver it inside an organization wall. Neither reading names
|
|
450
|
+
* organization A, so a subscription that belongs to A delivers on
|
|
451
|
+
* neither. ⛔ Absent is never "no tenancy concern": delivering on it
|
|
452
|
+
* would turn each of those cases into a cross-organization delivery. The
|
|
453
|
+
* cost is an environment-wide record not reaching an organization's
|
|
454
|
+
* webhook — accepted, and said once so the subscription is not dead while
|
|
455
|
+
* looking armed.
|
|
456
|
+
*
|
|
457
|
+
* **`none` × `absent` — deliver.** A `single`-posture deployment stamps
|
|
458
|
+
* nothing on either side (`postureStampsOrganization` is false there), so
|
|
459
|
+
* this cell is every event on every non-walled install; on a walled one
|
|
460
|
+
* it is an environment-wide subscription taking an environment-wide
|
|
461
|
+
* event. No organization is named anywhere, so there is no wall to cross.
|
|
462
|
+
*
|
|
463
|
+
* Both refusals are said ONCE per subscription (the #8022 say-once rule,
|
|
464
|
+
* ledger {@link organizationRefusalReported}): the first refused event is
|
|
465
|
+
* a `warn` naming the consequence and the remedy, later ones are
|
|
466
|
+
* debug-level. Without that a refused subscription reads active:true in
|
|
467
|
+
* Setup with nothing ever arriving — the "dead while looking armed" shape
|
|
468
|
+
* ADR-0078 refuses.
|
|
469
|
+
*/
|
|
470
|
+
private admitsOrganization;
|
|
368
471
|
private handleSelfHealEvent;
|
|
369
472
|
/** Test / admin accessor. */
|
|
370
473
|
snapshot(): ReadonlyMap<string, ReadonlyArray<CachedSubscription>>;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SysWebhook
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-MEX2VC7T.js";
|
|
4
4
|
|
|
5
5
|
// src/webhook-secret.ts
|
|
6
6
|
var WEBHOOK_SECRET_FIELD = "signing_secret";
|
|
@@ -192,6 +192,17 @@ var AutoEnqueuer = class {
|
|
|
192
192
|
* refresh, forever.
|
|
193
193
|
*/
|
|
194
194
|
this.droppedForSecret = /* @__PURE__ */ new Set();
|
|
195
|
+
/**
|
|
196
|
+
* [#13566] Webhook ids whose FIRST organization-dimension refusal has been
|
|
197
|
+
* said out loud (see {@link admitsOrganization}). Same say-once shape as
|
|
198
|
+
* {@link droppedForSecret}, for the same reason: a subscription that
|
|
199
|
+
* cannot receive a class of events must be reported once, with the
|
|
200
|
+
* remedy, and not once per event forever — an org-less `'*'` subscription
|
|
201
|
+
* on a walled deployment would otherwise warn on every write of every
|
|
202
|
+
* organization. Pruned on refresh to the rows still live, so a row that
|
|
203
|
+
* is deleted and re-created reports again.
|
|
204
|
+
*/
|
|
205
|
+
this.organizationRefusalReported = /* @__PURE__ */ new Set();
|
|
195
206
|
this.subscriptionsObject = opts.subscriptionsObject ?? "sys_webhook";
|
|
196
207
|
this.refreshIntervalMs = opts.refreshIntervalMs ?? 6e4;
|
|
197
208
|
this.logger = opts.logger;
|
|
@@ -294,11 +305,14 @@ var AutoEnqueuer = class {
|
|
|
294
305
|
}
|
|
295
306
|
this.subscriptions.clear();
|
|
296
307
|
for (const [k, v] of next) this.subscriptions.set(k, v);
|
|
297
|
-
if (this.droppedForSecret.size > 0) {
|
|
308
|
+
if (this.droppedForSecret.size > 0 || this.organizationRefusalReported.size > 0) {
|
|
298
309
|
const live = new Set(rows.map((r) => String(r?.id)));
|
|
299
310
|
for (const id of this.droppedForSecret) {
|
|
300
311
|
if (!live.has(id)) this.droppedForSecret.delete(id);
|
|
301
312
|
}
|
|
313
|
+
for (const id of this.organizationRefusalReported) {
|
|
314
|
+
if (!live.has(id)) this.organizationRefusalReported.delete(id);
|
|
315
|
+
}
|
|
302
316
|
}
|
|
303
317
|
this.logger?.debug?.("[webhook-auto-enqueuer] cache refreshed", {
|
|
304
318
|
objects: this.subscriptions.size,
|
|
@@ -596,7 +610,11 @@ var AutoEnqueuer = class {
|
|
|
596
610
|
// `headers` and `secret` are both filled by attachCredentials()
|
|
597
611
|
// from their encrypted columns, NOT read off the row — see #7799
|
|
598
612
|
// (secret) and #7986 (headers).
|
|
599
|
-
timeoutMs: defn.timeoutMs
|
|
613
|
+
timeoutMs: defn.timeoutMs,
|
|
614
|
+
// [#13546] The tenant column the kernel provisions on sys_webhook.
|
|
615
|
+
// This cache read is a dispatcher-side unscoped find, so the column
|
|
616
|
+
// comes back for every organization's rows.
|
|
617
|
+
organizationId: row.organization_id ? String(row.organization_id) : void 0
|
|
600
618
|
};
|
|
601
619
|
}
|
|
602
620
|
/**
|
|
@@ -631,9 +649,18 @@ var AutoEnqueuer = class {
|
|
|
631
649
|
);
|
|
632
650
|
return;
|
|
633
651
|
}
|
|
652
|
+
const organization = readEventOrganizationId(payload);
|
|
653
|
+
if (!organization) {
|
|
654
|
+
this.logger?.warn?.(
|
|
655
|
+
'[webhook-auto-enqueuer] dropping off-contract data event: `organizationId` is present but not a non-empty string (DataEventSchema refuses that at the publish site) \u2014 fix the producer; never coerced and never read as "no organization"',
|
|
656
|
+
{ type: event.type, object: event.object }
|
|
657
|
+
);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
634
660
|
const eventId = `${event.object}:${recordId}:${action}:${event.timestamp}`;
|
|
635
661
|
for (const sub of subs) {
|
|
636
662
|
if (!sub.triggers.has(trigger)) continue;
|
|
663
|
+
if (!this.admitsOrganization(sub, organization.organizationId, event)) continue;
|
|
637
664
|
void this.enqueue({
|
|
638
665
|
source: "webhook",
|
|
639
666
|
refId: sub.id,
|
|
@@ -650,6 +677,12 @@ var AutoEnqueuer = class {
|
|
|
650
677
|
// subscription, so the delivery path is byte-identical to before.
|
|
651
678
|
undeliverableReason: sub.parkedReason,
|
|
652
679
|
timeoutMs: sub.timeoutMs,
|
|
680
|
+
// [#13546] The delivery row belongs to the SUBSCRIPTION's
|
|
681
|
+
// organization — the one honest tenant in scope on this
|
|
682
|
+
// fire-and-forget path (no request context exists here).
|
|
683
|
+
// Absent for an org-less subscription; the row then lands
|
|
684
|
+
// NULL, the global-row shape.
|
|
685
|
+
organizationId: sub.organizationId,
|
|
653
686
|
// [#3946] Envelope keys are written LAST so the event payload
|
|
654
687
|
// cannot rewrite them. Behaviour-neutral for the engine's own
|
|
655
688
|
// publishers — since #4626 a `data.record.*` payload is a
|
|
@@ -709,8 +742,17 @@ var AutoEnqueuer = class {
|
|
|
709
742
|
return;
|
|
710
743
|
}
|
|
711
744
|
const eventId = `${event.object}:${event.type}:${eventUuid}`;
|
|
745
|
+
const organization = readEventOrganizationId(payload);
|
|
746
|
+
if (!organization) {
|
|
747
|
+
this.logger?.warn?.(
|
|
748
|
+
'[webhook-auto-enqueuer] dropping off-contract bulk data event: `organizationId` is present but not a non-empty string (BulkDataEventSchema refuses that at the publish site) \u2014 fix the producer; never coerced and never read as "not asserted"',
|
|
749
|
+
{ type: event.type, object: event.object }
|
|
750
|
+
);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
712
753
|
for (const sub of subs) {
|
|
713
754
|
if (!sub.triggers.has(trigger)) continue;
|
|
755
|
+
if (!this.admitsOrganization(sub, organization.organizationId, event)) continue;
|
|
714
756
|
void this.enqueue({
|
|
715
757
|
source: "webhook",
|
|
716
758
|
refId: sub.id,
|
|
@@ -724,6 +766,9 @@ var AutoEnqueuer = class {
|
|
|
724
766
|
// an undeliverable row instead of enqueuing a delivery.
|
|
725
767
|
undeliverableReason: sub.parkedReason,
|
|
726
768
|
timeoutMs: sub.timeoutMs,
|
|
769
|
+
// [#13546] See the per-record path — the subscription's own
|
|
770
|
+
// organization, absent for an org-less subscription.
|
|
771
|
+
organizationId: sub.organizationId,
|
|
727
772
|
// [#3946] Envelope keys last so the payload cannot rewrite them.
|
|
728
773
|
payload: {
|
|
729
774
|
...payload,
|
|
@@ -735,6 +780,91 @@ var AutoEnqueuer = class {
|
|
|
735
780
|
}).catch((err) => this.reportWriteFailure(sub, eventId, err, "bulk enqueue"));
|
|
736
781
|
}
|
|
737
782
|
}
|
|
783
|
+
/**
|
|
784
|
+
* [#13566] The organization dimension of the match — ONE comparison
|
|
785
|
+
* between the subscription's own organization (cached off its
|
|
786
|
+
* `sys_webhook` row, #13546) and the organization the producer stamped on
|
|
787
|
+
* the event (#14970 per record; #15225 / #15813 per batch). ⛔ No lookup:
|
|
788
|
+
* the enqueuer exists to keep this path O(1), and both halves are already
|
|
789
|
+
* in hand — the filter is a comparison, never a resolution.
|
|
790
|
+
*
|
|
791
|
+
* The cells, and why each falls where it does:
|
|
792
|
+
*
|
|
793
|
+
* | subscription | event | verdict |
|
|
794
|
+
* |--------------|--------|------------------------------------------------|
|
|
795
|
+
* | org A | org A | deliver |
|
|
796
|
+
* | org A | org B | not a match — the routine outcome of the new |
|
|
797
|
+
* | | | term, silent like an object-name mismatch |
|
|
798
|
+
* | org A | absent | REFUSE (fail-closed), said once per sub |
|
|
799
|
+
* | none | org A | REFUSE, said once per sub — the ruling's case |
|
|
800
|
+
* | none | absent | deliver — no wall on either side |
|
|
801
|
+
*
|
|
802
|
+
* **`none` × `org A` — the ruling.** A `sys_webhook` row with no
|
|
803
|
+
* organization on a walled deployment (a package-declared row bootstrapped
|
|
804
|
+
* under `isSystem`, a row authored before the column was provisioned)
|
|
805
|
+
* would otherwise receive EVERY organization's records at its URL, signed
|
|
806
|
+
* with its secret — the leak this card is. Maintainer's ruling
|
|
807
|
+
* (2026-09-07), verbatim: *a subscription with no organisation ownership
|
|
808
|
+
* does not fan out — loud refusal, never a silent cross-organisation
|
|
809
|
+
* delivery.* ADR-0131 D1 is why there is no third reading — NULL is not a
|
|
810
|
+
* state, so "no organization" is never "every organization".
|
|
811
|
+
*
|
|
812
|
+
* **`org A` × `absent` — fail-closed, on BOTH paths.** The two event
|
|
813
|
+
* families spell absence with the same key and mean different things by
|
|
814
|
+
* it (`packages/spec/src/api/events.zod.ts`, both members' docs). On a
|
|
815
|
+
* `DataEvent` it is "this record belongs to no organization" — an
|
|
816
|
+
* environment-wide row, an object outside the wall, or (the per-record
|
|
817
|
+
* producer's own docblock, `eventOrganizationId` in
|
|
818
|
+
* `packages/objectql/src/engine.ts`) no row in hand at the publish site,
|
|
819
|
+
* published absent rather than substituting the caller's organization.
|
|
820
|
+
* On a `BulkDataEvent` it is "the producer did not assert one
|
|
821
|
+
* organization for this batch" — a system or cross-membership predicate
|
|
822
|
+
* write — and the contract says outright that a tenant-scoped consumer
|
|
823
|
+
* must not deliver it inside an organization wall. Neither reading names
|
|
824
|
+
* organization A, so a subscription that belongs to A delivers on
|
|
825
|
+
* neither. ⛔ Absent is never "no tenancy concern": delivering on it
|
|
826
|
+
* would turn each of those cases into a cross-organization delivery. The
|
|
827
|
+
* cost is an environment-wide record not reaching an organization's
|
|
828
|
+
* webhook — accepted, and said once so the subscription is not dead while
|
|
829
|
+
* looking armed.
|
|
830
|
+
*
|
|
831
|
+
* **`none` × `absent` — deliver.** A `single`-posture deployment stamps
|
|
832
|
+
* nothing on either side (`postureStampsOrganization` is false there), so
|
|
833
|
+
* this cell is every event on every non-walled install; on a walled one
|
|
834
|
+
* it is an environment-wide subscription taking an environment-wide
|
|
835
|
+
* event. No organization is named anywhere, so there is no wall to cross.
|
|
836
|
+
*
|
|
837
|
+
* Both refusals are said ONCE per subscription (the #8022 say-once rule,
|
|
838
|
+
* ledger {@link organizationRefusalReported}): the first refused event is
|
|
839
|
+
* a `warn` naming the consequence and the remedy, later ones are
|
|
840
|
+
* debug-level. Without that a refused subscription reads active:true in
|
|
841
|
+
* Setup with nothing ever arriving — the "dead while looking armed" shape
|
|
842
|
+
* ADR-0078 refuses.
|
|
843
|
+
*/
|
|
844
|
+
admitsOrganization(sub, eventOrganizationId, event) {
|
|
845
|
+
if (sub.organizationId === eventOrganizationId) return true;
|
|
846
|
+
if (sub.organizationId !== void 0 && eventOrganizationId !== void 0) return false;
|
|
847
|
+
const orgless = sub.organizationId === void 0;
|
|
848
|
+
const meta = {
|
|
849
|
+
id: sub.id,
|
|
850
|
+
webhook: sub.name,
|
|
851
|
+
type: event.type,
|
|
852
|
+
object: event.object,
|
|
853
|
+
subscriptionOrganizationId: sub.organizationId,
|
|
854
|
+
eventNamesOrganization: eventOrganizationId !== void 0
|
|
855
|
+
};
|
|
856
|
+
if (this.organizationRefusalReported.has(sub.id)) {
|
|
857
|
+
this.logger?.debug?.(
|
|
858
|
+
`[webhook-auto-enqueuer] webhook '${sub.name}' still refused on the organization dimension`,
|
|
859
|
+
meta
|
|
860
|
+
);
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
this.organizationRefusalReported.add(sub.id);
|
|
864
|
+
const message = orgless ? `[webhook-auto-enqueuer] webhook '${sub.name}' belongs to NO organization, but this ${event.type} event on '${event.object}' is organization-walled (the producer stamped organizationId) \u2014 refusing to fan out: a subscription with no organization ownership does not receive an organization's records. It will receive NO organization-walled event while reading active:true in Setup; author the webhook inside the organization that should receive these events. Said once per subscription.` : `[webhook-auto-enqueuer] webhook '${sub.name}' belongs to organization '${sub.organizationId}', but this ${event.type} event on '${event.object}' names no organization \u2014 refusing to deliver it inside an organization wall: on the per-record path an absent organizationId is an environment-wide row or an object outside the wall; on the bulk path it is a batch the producer could not attribute to one organization (a system or cross-membership predicate write). A tenant-scoped subscription receives only events attributable to its own organization. Said once per subscription.`;
|
|
865
|
+
this.logger?.warn?.(message, meta);
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
738
868
|
handleSelfHealEvent(event) {
|
|
739
869
|
if (event.object !== this.subscriptionsObject) return;
|
|
740
870
|
if (!event.type?.startsWith("data.record.") && !event.type?.startsWith("data.records.")) return;
|
|
@@ -759,6 +889,12 @@ function mapActionToTrigger(action) {
|
|
|
759
889
|
return null;
|
|
760
890
|
}
|
|
761
891
|
}
|
|
892
|
+
function readEventOrganizationId(payload) {
|
|
893
|
+
const value = payload.organizationId;
|
|
894
|
+
if (value === void 0) return { organizationId: void 0 };
|
|
895
|
+
if (typeof value !== "string" || value === "") return void 0;
|
|
896
|
+
return { organizationId: value };
|
|
897
|
+
}
|
|
762
898
|
function mapBulkActionToTrigger(action) {
|
|
763
899
|
switch (action) {
|
|
764
900
|
case "updated":
|
|
@@ -1022,34 +1158,18 @@ function createWebhookRedeliverGuard(engine, subscriptionsObject = WEBHOOK_OBJEC
|
|
|
1022
1158
|
|
|
1023
1159
|
// src/webhook-provenance.ts
|
|
1024
1160
|
var WEBHOOK_PROVENANCE_PACKAGE = "plugin-webhooks:provenance";
|
|
1025
|
-
var SYSTEM_CTX3 = { isSystem: true, positions: [], permissions: [] };
|
|
1026
1161
|
function bindWebhookProvenanceStamp(engine, logger) {
|
|
1027
1162
|
if (typeof engine?.registerHook !== "function") return;
|
|
1028
1163
|
engine.registerHook(
|
|
1029
1164
|
"beforeUpdate",
|
|
1030
1165
|
async (ctx) => {
|
|
1031
1166
|
if (ctx?.session?.isSystem) return;
|
|
1032
|
-
const id = ctx?.input?.id ?? ctx?.input?.data?.id;
|
|
1033
|
-
if (!id) return;
|
|
1034
1167
|
const data = ctx?.input?.data;
|
|
1035
1168
|
if (!data || typeof data !== "object") return;
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
limit: 1,
|
|
1041
|
-
context: SYSTEM_CTX3
|
|
1042
|
-
});
|
|
1043
|
-
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
1044
|
-
if (!row) return;
|
|
1045
|
-
if ((row.managed_by === "package" || row.managed_by === "platform") && row.customized !== true) {
|
|
1046
|
-
data.customized = true;
|
|
1047
|
-
}
|
|
1048
|
-
} catch (err) {
|
|
1049
|
-
logger?.warn?.("[webhook] provenance stamp failed (edit proceeds unstamped)", {
|
|
1050
|
-
id,
|
|
1051
|
-
error: err?.message
|
|
1052
|
-
});
|
|
1169
|
+
const previous = ctx?.previous;
|
|
1170
|
+
if (!previous || typeof previous !== "object") return;
|
|
1171
|
+
if ((previous.managed_by === "package" || previous.managed_by === "platform") && previous.customized !== true) {
|
|
1172
|
+
data.customized = true;
|
|
1053
1173
|
}
|
|
1054
1174
|
},
|
|
1055
1175
|
{ object: "sys_webhook", packageId: WEBHOOK_PROVENANCE_PACKAGE, priority: 150 }
|
|
@@ -1207,7 +1327,7 @@ var WebhookOutboxPlugin = class {
|
|
|
1207
1327
|
try {
|
|
1208
1328
|
const i18n = ctx.getService("i18n");
|
|
1209
1329
|
if (i18n && typeof i18n.loadTranslations === "function") {
|
|
1210
|
-
const { WebhooksTranslations } = await import("./translations-
|
|
1330
|
+
const { WebhooksTranslations } = await import("./translations-MNRJO53N.js");
|
|
1211
1331
|
for (const [locale, data] of Object.entries(WebhooksTranslations)) {
|
|
1212
1332
|
i18n.loadTranslations(locale, data);
|
|
1213
1333
|
}
|