@voltro/plugin-webhooks 0.26.0 → 0.28.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 +532 -0
- package/dist/index.d.ts +120 -16
- package/dist/index.js +206 -155
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -196,6 +196,31 @@ export declare interface DeliverySummary {
|
|
|
196
196
|
* Covers ms / s / m / h / d. Default seconds (no suffix). */
|
|
197
197
|
export declare type DurationLiteral = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}` | `${number}`;
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Per-emit scoping.
|
|
201
|
+
*
|
|
202
|
+
* **The outgoing fan-out was NOT tenant-scoped, and this is what closes it.**
|
|
203
|
+
* The service is built once at boot with the app-level store, so the `tenant()`
|
|
204
|
+
* mixin on `_voltro_webhook_targets` had no request subject to scope by — the
|
|
205
|
+
* target lookup was `eq('event', name)` and nothing else. Confinement therefore
|
|
206
|
+
* rested entirely on each target's own `filter`, i.e. on the app remembering.
|
|
207
|
+
*
|
|
208
|
+
* A consumer found this by reading the CLI, and named exactly why it had looked
|
|
209
|
+
* safe: target filters usually predicate on a globally unique app id, so a
|
|
210
|
+
* cross-tenant match is impossible **by accident**. It stopped being accidental
|
|
211
|
+
* for them the moment they introduced a value deliberately equal across teams
|
|
212
|
+
* (a tenant-wide audience marker).
|
|
213
|
+
*
|
|
214
|
+
* `ctx.webhooks` binds this from the request subject, so an emit inside a
|
|
215
|
+
* handler is scoped without the handler saying anything. Absent — a background
|
|
216
|
+
* job, a schedule, a replay — keeps the unscoped behaviour, because there is no
|
|
217
|
+
* tenant to scope BY and refusing would break every system emit.
|
|
218
|
+
*/
|
|
219
|
+
export declare interface EmitOptions {
|
|
220
|
+
/** Only deliver to targets of this tenant. Absent ⇒ every target. */
|
|
221
|
+
readonly tenantId?: string | null;
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
export declare interface EmitResult {
|
|
200
225
|
/** Stable id matching the row(s) the deliverWebhook workflow
|
|
201
226
|
* writes to `_voltro_webhook_deliveries`. Use as the foreign
|
|
@@ -223,6 +248,9 @@ export declare interface EncodedPayload {
|
|
|
223
248
|
* be honestly represented in the requested format. */
|
|
224
249
|
export declare const encodePayload: (format: WireFormat, payloadJson: string) => EncodedPayload;
|
|
225
250
|
|
|
251
|
+
/** The events a subscribe names, whichever spelling was used. */
|
|
252
|
+
export declare const eventsOf: (input: SubscribeInput) => ReadonlyArray<string>;
|
|
253
|
+
|
|
226
254
|
/** Aggressive policy — retry every 30s for an hour. Use for a
|
|
227
255
|
* downstream that's expected to come back quickly. */
|
|
228
256
|
export declare const fastRetryPolicy: () => RetryPolicy;
|
|
@@ -418,7 +446,7 @@ export declare const isWebhookDescriptor: (value: unknown) => value is WebhookDe
|
|
|
418
446
|
* when the target should receive this delivery. Supports the
|
|
419
447
|
* simple key-path form `'payload.field': value` for v1; future
|
|
420
448
|
* versions can grow operators ({ gt, lt, in, … }). */
|
|
421
|
-
export declare const matchesFilter: (filter: Readonly<Record<string, unknown>> | null, payload: unknown) => boolean;
|
|
449
|
+
export declare const matchesFilter: (filter: WebhookFilter | Readonly<Record<string, unknown>> | null, payload: unknown) => boolean;
|
|
422
450
|
|
|
423
451
|
/**
|
|
424
452
|
* Build a request handler for a specific `IncomingWebhookDescriptor`.
|
|
@@ -542,7 +570,10 @@ export declare const releaseRateSlot: (store: DataStore, key: string, bucket: nu
|
|
|
542
570
|
* value wins, then the event descriptor's per-event defaults
|
|
543
571
|
* (`defaultSigning` / `defaultRetry` / `version` from
|
|
544
572
|
* the declared event's `webhook:` block), then the package-global defaults. */
|
|
545
|
-
export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingEventDescriptor<unknown
|
|
573
|
+
export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingEventDescriptor<unknown>,
|
|
574
|
+
/** The event this ROW is for, and the secret the group shares. Both default
|
|
575
|
+
* to the single-event behaviour, so existing callers are unchanged. */
|
|
576
|
+
forEvent?: string, sharedSecret?: string) => {
|
|
546
577
|
readonly id: string;
|
|
547
578
|
readonly event: string;
|
|
548
579
|
readonly url: string;
|
|
@@ -621,7 +652,8 @@ export declare const slackSignature: () => CustomSignatureScheme;
|
|
|
621
652
|
export declare const stripeSignature: (header?: string) => HmacSignatureScheme;
|
|
622
653
|
|
|
623
654
|
export declare interface SubscribeInput {
|
|
624
|
-
|
|
655
|
+
/** A single event. Use `events` for a multi-event subscription. */
|
|
656
|
+
readonly event?: string;
|
|
625
657
|
readonly url: string;
|
|
626
658
|
/** Secret used to sign deliveries to this target. Auto-generated
|
|
627
659
|
* when omitted (32-byte hex). The caller receives it ONCE in the
|
|
@@ -629,10 +661,31 @@ export declare interface SubscribeInput {
|
|
|
629
661
|
readonly secret?: string;
|
|
630
662
|
readonly signing?: SignatureScheme;
|
|
631
663
|
readonly retry?: RetryPolicy;
|
|
632
|
-
/**
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Optional routing filter — only emits whose payload matches fan out to this
|
|
666
|
+
* target. See {@link WebhookFilter}: dotted paths into `{ payload }`, a bare
|
|
667
|
+
* value for equality, or `{ eq | in | gt | gte | lt | lte }`.
|
|
668
|
+
*/
|
|
669
|
+
readonly filter?: WebhookFilter;
|
|
670
|
+
/**
|
|
671
|
+
* Subscribe ONE url to SEVERAL events at once.
|
|
672
|
+
*
|
|
673
|
+
* A subscription, as every webhook UI models it, is one URL with a list of
|
|
674
|
+
* event checkboxes — ours, Stripe's, GitHub's. The row is one event, so five
|
|
675
|
+
* checkboxes are five rows, and the gap between the two is where an app ends
|
|
676
|
+
* up hand-rolling every operation a user thinks of as single.
|
|
677
|
+
*
|
|
678
|
+
* The rows created here share ONE secret and one `scope`, which is what makes
|
|
679
|
+
* the group addressable afterwards — and it is not a convenience: the receiver
|
|
680
|
+
* verifies one signature for one URL, so N rows for one endpoint MUST sign
|
|
681
|
+
* identically. Without this the only way to say so was to read the secret
|
|
682
|
+
* column back out of `_voltro_webhook_targets`, which is exactly the coupling
|
|
683
|
+
* `listDeliveries` was added to remove, re-entered through another door.
|
|
684
|
+
*
|
|
685
|
+
* Mutually exclusive with `event`. Pass whichever reads better; one event is
|
|
686
|
+
* still one row.
|
|
687
|
+
*/
|
|
688
|
+
readonly events?: ReadonlyArray<string>;
|
|
636
689
|
/**
|
|
637
690
|
* The APP's own scoping dimension — opaque, stored and returned verbatim.
|
|
638
691
|
*
|
|
@@ -654,6 +707,12 @@ export declare interface SubscribeInput {
|
|
|
654
707
|
}
|
|
655
708
|
|
|
656
709
|
export declare interface SubscribeResult {
|
|
710
|
+
/** Every row created, when `events` named more than one. Absent for a
|
|
711
|
+
* single-event subscribe, where `id` and `event` already say it. */
|
|
712
|
+
readonly targets?: ReadonlyArray<{
|
|
713
|
+
readonly id: string;
|
|
714
|
+
readonly event: string;
|
|
715
|
+
}>;
|
|
657
716
|
readonly id: string;
|
|
658
717
|
readonly event: string;
|
|
659
718
|
readonly url: string;
|
|
@@ -669,7 +728,7 @@ export declare interface SubscribeResult {
|
|
|
669
728
|
export declare interface TargetPatch {
|
|
670
729
|
readonly url?: string;
|
|
671
730
|
readonly description?: string | null;
|
|
672
|
-
readonly filter?:
|
|
731
|
+
readonly filter?: WebhookFilter | null;
|
|
673
732
|
readonly scope?: Readonly<Record<string, unknown>> | null;
|
|
674
733
|
readonly headers?: Readonly<Record<string, string>> | null;
|
|
675
734
|
readonly rateLimitPerMinute?: number | null;
|
|
@@ -679,6 +738,23 @@ export declare interface TargetPatch {
|
|
|
679
738
|
|
|
680
739
|
export declare const TARGETS_TABLE = "_voltro_webhook_targets";
|
|
681
740
|
|
|
741
|
+
/**
|
|
742
|
+
* WHICH target(s) an operation addresses.
|
|
743
|
+
*
|
|
744
|
+
* A string is one row. A `{ scope }` is the GROUP — every row whose opaque
|
|
745
|
+
* scope matches, which for a multi-event subscription is the whole endpoint.
|
|
746
|
+
*
|
|
747
|
+
* This exists because a subscription, as a user models it, is one URL with a
|
|
748
|
+
* list of event checkboxes, while a row is one event. Without a group selector
|
|
749
|
+
* every operation the user thinks of as single — pause the endpoint, fix its
|
|
750
|
+
* URL, rotate its secret, read its history — becomes a fan-out the app writes
|
|
751
|
+
* by hand, and `rotateSecret` in particular becomes delete + re-subscribe,
|
|
752
|
+
* which mints new ids and orphans the delivery history.
|
|
753
|
+
*/
|
|
754
|
+
export declare type TargetSelector = string | {
|
|
755
|
+
readonly scope: Readonly<Record<string, unknown>>;
|
|
756
|
+
};
|
|
757
|
+
|
|
682
758
|
export declare interface TargetSummary {
|
|
683
759
|
readonly id: string;
|
|
684
760
|
/** The app's own scoping dimension as written at subscribe time, verbatim.
|
|
@@ -723,8 +799,6 @@ export declare const useWebhooks: (ctx: {
|
|
|
723
799
|
|
|
724
800
|
export declare const useWebhooksEffect: Effect.Effect<WebhooksServiceShape, never, WebhooksService>;
|
|
725
801
|
|
|
726
|
-
/** Validate subscribe input. Throws on policy violations so the
|
|
727
|
-
* caller's catch block can surface a 422-style error. */
|
|
728
802
|
export declare const validateSubscribe: (input: SubscribeInput) => void;
|
|
729
803
|
|
|
730
804
|
export declare type VerifyResult = {
|
|
@@ -972,6 +1046,24 @@ declare const WebhookDeliveryNotFound_base: Schema.TaggedErrorClass<WebhookDeliv
|
|
|
972
1046
|
|
|
973
1047
|
export declare type WebhookDescriptor = OutgoingEventDescriptor<unknown> | IncomingWebhookDescriptor<unknown> | WebhookProviderDescriptor;
|
|
974
1048
|
|
|
1049
|
+
/**
|
|
1050
|
+
* A target's routing filter: dotted paths INTO the emitted envelope.
|
|
1051
|
+
*
|
|
1052
|
+
* The root is `{ payload }`, so every path starts `payload.` — a path that does
|
|
1053
|
+
* not resolve reads as `undefined` and the comparison fails, which is how a
|
|
1054
|
+
* typo'd path silently routes nothing.
|
|
1055
|
+
*/
|
|
1056
|
+
export declare type WebhookFilter = Readonly<Record<string, WebhookFilterValue>>;
|
|
1057
|
+
|
|
1058
|
+
export declare type WebhookFilterValue = string | number | boolean | null | {
|
|
1059
|
+
readonly eq?: string | number | boolean | null;
|
|
1060
|
+
readonly in?: ReadonlyArray<string | number | boolean | null>;
|
|
1061
|
+
readonly gt?: number | string;
|
|
1062
|
+
readonly gte?: number | string;
|
|
1063
|
+
readonly lt?: number | string;
|
|
1064
|
+
readonly lte?: number | string;
|
|
1065
|
+
};
|
|
1066
|
+
|
|
975
1067
|
/** Stable identifier for an event or webhook. Drives the database
|
|
976
1068
|
* primary key, the inspect endpoint URL, the dashboard listing. Use
|
|
977
1069
|
* dotted-camelCase (`order.completed`, `user.signedUp`). */
|
|
@@ -1068,7 +1160,7 @@ export declare interface WebhooksServiceShape {
|
|
|
1068
1160
|
* discovered-events registry) the payload is DECODED against its
|
|
1069
1161
|
* schema and a mismatch throws `WebhookPayloadInvalid` before any
|
|
1070
1162
|
* delivery is created. */
|
|
1071
|
-
readonly emit: <P>(event: string | OutgoingEventDescriptor<P> | DeclaredEventLike, payload: P) => Promise<EmitResult>;
|
|
1163
|
+
readonly emit: <P>(event: string | OutgoingEventDescriptor<P> | DeclaredEventLike, payload: P, options?: EmitOptions) => Promise<EmitResult>;
|
|
1072
1164
|
/** Manual re-trigger for the dashboard's "Replay" button on a
|
|
1073
1165
|
* failed delivery row. Re-runs the workflow at attempt 1 with
|
|
1074
1166
|
* the original payload. */
|
|
@@ -1086,24 +1178,33 @@ export declare interface WebhooksServiceShape {
|
|
|
1086
1178
|
* emits against this target queue under
|
|
1087
1179
|
* `_voltro_webhook_deliveries` with status='pending' (no POST
|
|
1088
1180
|
* happens); `resumeTarget` flushes them. */
|
|
1089
|
-
|
|
1181
|
+
/** Every row a selector addresses. Refuses a scope that matches nothing —
|
|
1182
|
+
* an operation that silently affects zero rows is worse than an error. */
|
|
1183
|
+
readonly resolveTargets: (selector: TargetSelector) => Promise<ReadonlyArray<string>>;
|
|
1184
|
+
readonly pauseTarget: (target: TargetSelector) => Promise<void>;
|
|
1090
1185
|
/** Re-enable a paused target AND flush its queued
|
|
1091
1186
|
* `status='pending'` deliveries through the normal delivery
|
|
1092
1187
|
* workflow, in emit order (`createdAt` ascending — millisecond
|
|
1093
1188
|
* granularity) per target. */
|
|
1094
|
-
readonly resumeTarget: (
|
|
1189
|
+
readonly resumeTarget: (target: TargetSelector) => Promise<void>;
|
|
1095
1190
|
/** Hard-delete a target. Its queued `status='pending'` rows are
|
|
1096
1191
|
* deleted with it (nothing left to flush); an in-flight workflow
|
|
1097
1192
|
* run's `fetch-target` activity sees a null row and exits
|
|
1098
1193
|
* cleanly. */
|
|
1099
|
-
readonly deleteTarget: (
|
|
1194
|
+
readonly deleteTarget: (target: TargetSelector) => Promise<void>;
|
|
1100
1195
|
/** Rotate the per-target signing secret. Returns the new secret
|
|
1101
1196
|
* ONCE — store it client-side if you need to display it again.
|
|
1102
1197
|
* In-flight retries continue with the OLD secret since signing
|
|
1103
1198
|
* happens at attempt time using the row's then-current value
|
|
1104
1199
|
* (acceptable: providers retry within seconds, the rotation
|
|
1105
1200
|
* window is tight). */
|
|
1106
|
-
|
|
1201
|
+
/**
|
|
1202
|
+
* Rotate the signing secret. Addressed by a SCOPE this rotates every row of
|
|
1203
|
+
* the endpoint to the SAME new value — which is the point: N rows for one URL
|
|
1204
|
+
* must sign identically, and the previous way to achieve it was delete +
|
|
1205
|
+
* re-subscribe, minting new ids and orphaning the delivery history.
|
|
1206
|
+
*/
|
|
1207
|
+
readonly rotateSecret: (target: TargetSelector) => Promise<{
|
|
1107
1208
|
readonly secret: string;
|
|
1108
1209
|
}>;
|
|
1109
1210
|
/** Re-pin a target's `payloadVersion` to the event's current
|
|
@@ -1124,7 +1225,7 @@ export declare interface WebhooksServiceShape {
|
|
|
1124
1225
|
* makes it a different subscription, and the secret has `rotateSecret`, which
|
|
1125
1226
|
* returns the new value once.
|
|
1126
1227
|
*/
|
|
1127
|
-
readonly updateTarget: (
|
|
1228
|
+
readonly updateTarget: (target: TargetSelector, patch: TargetPatch) => Promise<void>;
|
|
1128
1229
|
/**
|
|
1129
1230
|
* Send one delivery to ONE target, bypassing fan-out and the filter.
|
|
1130
1231
|
*
|
|
@@ -1151,6 +1252,9 @@ export declare interface WebhooksServiceShape {
|
|
|
1151
1252
|
*/
|
|
1152
1253
|
readonly listDeliveries: (filter?: {
|
|
1153
1254
|
readonly targetId?: string;
|
|
1255
|
+
/** Every row of an endpoint's group, so a history view needs no N-way
|
|
1256
|
+
* merge-and-re-sort in the app. */
|
|
1257
|
+
readonly scope?: Readonly<Record<string, unknown>>;
|
|
1154
1258
|
readonly status?: DeliverySummary['status'];
|
|
1155
1259
|
readonly since?: Date;
|
|
1156
1260
|
readonly limit?: number;
|