@voltro/plugin-webhooks 0.25.0 → 0.27.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 +541 -0
- package/dist/index.d.ts +215 -9
- package/dist/index.js +295 -151
- package/dist/mixin.d.ts +48 -0
- package/dist/mixin.js +1 -0
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -169,6 +169,29 @@ declare interface DeliverWorkflowOptions {
|
|
|
169
169
|
readonly rateWindowMs?: number;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/** `getDelivery` adds the two heavy columns `listDeliveries` omits. */
|
|
173
|
+
export declare interface DeliveryDetail extends DeliverySummary {
|
|
174
|
+
readonly payload: unknown;
|
|
175
|
+
readonly responseBody: string | null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** One delivery ATTEMPT, as the management UI reads it. */
|
|
179
|
+
export declare interface DeliverySummary {
|
|
180
|
+
readonly id: string;
|
|
181
|
+
readonly deliveryId: string;
|
|
182
|
+
readonly targetId: string;
|
|
183
|
+
readonly event: string;
|
|
184
|
+
readonly eventId: string | null;
|
|
185
|
+
readonly attempt: number;
|
|
186
|
+
readonly status: 'pending' | 'inFlight' | 'succeeded' | 'failed' | 'retryScheduled';
|
|
187
|
+
readonly responseStatus: number | null;
|
|
188
|
+
readonly errorMessage: string | null;
|
|
189
|
+
readonly latencyMs: number | null;
|
|
190
|
+
readonly scheduledAt: string | null;
|
|
191
|
+
readonly nextAttemptAt: string | null;
|
|
192
|
+
readonly createdAt: string | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
172
195
|
/** Compact duration literal — parsed without a deps-pulling library.
|
|
173
196
|
* Covers ms / s / m / h / d. Default seconds (no suffix). */
|
|
174
197
|
export declare type DurationLiteral = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}` | `${number}`;
|
|
@@ -200,6 +223,9 @@ export declare interface EncodedPayload {
|
|
|
200
223
|
* be honestly represented in the requested format. */
|
|
201
224
|
export declare const encodePayload: (format: WireFormat, payloadJson: string) => EncodedPayload;
|
|
202
225
|
|
|
226
|
+
/** The events a subscribe names, whichever spelling was used. */
|
|
227
|
+
export declare const eventsOf: (input: SubscribeInput) => ReadonlyArray<string>;
|
|
228
|
+
|
|
203
229
|
/** Aggressive policy — retry every 30s for an hour. Use for a
|
|
204
230
|
* downstream that's expected to come back quickly. */
|
|
205
231
|
export declare const fastRetryPolicy: () => RetryPolicy;
|
|
@@ -519,7 +545,10 @@ export declare const releaseRateSlot: (store: DataStore, key: string, bucket: nu
|
|
|
519
545
|
* value wins, then the event descriptor's per-event defaults
|
|
520
546
|
* (`defaultSigning` / `defaultRetry` / `version` from
|
|
521
547
|
* the declared event's `webhook:` block), then the package-global defaults. */
|
|
522
|
-
export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingEventDescriptor<unknown
|
|
548
|
+
export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingEventDescriptor<unknown>,
|
|
549
|
+
/** The event this ROW is for, and the secret the group shares. Both default
|
|
550
|
+
* to the single-event behaviour, so existing callers are unchanged. */
|
|
551
|
+
forEvent?: string, sharedSecret?: string) => {
|
|
523
552
|
readonly id: string;
|
|
524
553
|
readonly event: string;
|
|
525
554
|
readonly url: string;
|
|
@@ -527,6 +556,7 @@ export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingE
|
|
|
527
556
|
readonly signing: SignatureScheme;
|
|
528
557
|
readonly retry: RetryPolicy;
|
|
529
558
|
readonly filter: Readonly<Record<string, unknown>> | null;
|
|
559
|
+
readonly scope: Readonly<Record<string, unknown>> | null;
|
|
530
560
|
readonly headers: Readonly<Record<string, string>> | null;
|
|
531
561
|
readonly rateLimitPerMinute: number | null;
|
|
532
562
|
readonly active: boolean;
|
|
@@ -597,7 +627,8 @@ export declare const slackSignature: () => CustomSignatureScheme;
|
|
|
597
627
|
export declare const stripeSignature: (header?: string) => HmacSignatureScheme;
|
|
598
628
|
|
|
599
629
|
export declare interface SubscribeInput {
|
|
600
|
-
|
|
630
|
+
/** A single event. Use `events` for a multi-event subscription. */
|
|
631
|
+
readonly event?: string;
|
|
601
632
|
readonly url: string;
|
|
602
633
|
/** Secret used to sign deliveries to this target. Auto-generated
|
|
603
634
|
* when omitted (32-byte hex). The caller receives it ONCE in the
|
|
@@ -609,6 +640,34 @@ export declare interface SubscribeInput {
|
|
|
609
640
|
* this predicate fan-out to this target. The shape mirrors the
|
|
610
641
|
* schema-builder's `Predicate` from `@voltro/database`. */
|
|
611
642
|
readonly filter?: Readonly<Record<string, unknown>>;
|
|
643
|
+
/**
|
|
644
|
+
* Subscribe ONE url to SEVERAL events at once.
|
|
645
|
+
*
|
|
646
|
+
* A subscription, as every webhook UI models it, is one URL with a list of
|
|
647
|
+
* event checkboxes — ours, Stripe's, GitHub's. The row is one event, so five
|
|
648
|
+
* checkboxes are five rows, and the gap between the two is where an app ends
|
|
649
|
+
* up hand-rolling every operation a user thinks of as single.
|
|
650
|
+
*
|
|
651
|
+
* The rows created here share ONE secret and one `scope`, which is what makes
|
|
652
|
+
* the group addressable afterwards — and it is not a convenience: the receiver
|
|
653
|
+
* verifies one signature for one URL, so N rows for one endpoint MUST sign
|
|
654
|
+
* identically. Without this the only way to say so was to read the secret
|
|
655
|
+
* column back out of `_voltro_webhook_targets`, which is exactly the coupling
|
|
656
|
+
* `listDeliveries` was added to remove, re-entered through another door.
|
|
657
|
+
*
|
|
658
|
+
* Mutually exclusive with `event`. Pass whichever reads better; one event is
|
|
659
|
+
* still one row.
|
|
660
|
+
*/
|
|
661
|
+
readonly events?: ReadonlyArray<string>;
|
|
662
|
+
/**
|
|
663
|
+
* The APP's own scoping dimension — opaque, stored and returned verbatim.
|
|
664
|
+
*
|
|
665
|
+
* `.with(tenant())` is one level too coarse for real deployments: endpoints
|
|
666
|
+
* are commonly scoped to a team, a project or a workspace, and a tenant has
|
|
667
|
+
* many of those. Pass whatever identifies yours; `listTargets({ scope })`
|
|
668
|
+
* filters on equality against it. The framework never interprets it.
|
|
669
|
+
*/
|
|
670
|
+
readonly scope?: Readonly<Record<string, unknown>>;
|
|
612
671
|
readonly headers?: Readonly<Record<string, string>>;
|
|
613
672
|
readonly rateLimitPerMinute?: number;
|
|
614
673
|
readonly format?: 'json' | 'form' | 'xml';
|
|
@@ -621,6 +680,12 @@ export declare interface SubscribeInput {
|
|
|
621
680
|
}
|
|
622
681
|
|
|
623
682
|
export declare interface SubscribeResult {
|
|
683
|
+
/** Every row created, when `events` named more than one. Absent for a
|
|
684
|
+
* single-event subscribe, where `id` and `event` already say it. */
|
|
685
|
+
readonly targets?: ReadonlyArray<{
|
|
686
|
+
readonly id: string;
|
|
687
|
+
readonly event: string;
|
|
688
|
+
}>;
|
|
624
689
|
readonly id: string;
|
|
625
690
|
readonly event: string;
|
|
626
691
|
readonly url: string;
|
|
@@ -632,10 +697,42 @@ export declare interface SubscribeResult {
|
|
|
632
697
|
readonly retry: RetryPolicy;
|
|
633
698
|
}
|
|
634
699
|
|
|
700
|
+
/** The patchable subset of a target. Absent keys are left alone. */
|
|
701
|
+
export declare interface TargetPatch {
|
|
702
|
+
readonly url?: string;
|
|
703
|
+
readonly description?: string | null;
|
|
704
|
+
readonly filter?: Readonly<Record<string, unknown>> | null;
|
|
705
|
+
readonly scope?: Readonly<Record<string, unknown>> | null;
|
|
706
|
+
readonly headers?: Readonly<Record<string, string>> | null;
|
|
707
|
+
readonly rateLimitPerMinute?: number | null;
|
|
708
|
+
readonly autoDisableAfter?: number | null;
|
|
709
|
+
readonly format?: 'json' | 'form' | 'xml';
|
|
710
|
+
}
|
|
711
|
+
|
|
635
712
|
export declare const TARGETS_TABLE = "_voltro_webhook_targets";
|
|
636
713
|
|
|
714
|
+
/**
|
|
715
|
+
* WHICH target(s) an operation addresses.
|
|
716
|
+
*
|
|
717
|
+
* A string is one row. A `{ scope }` is the GROUP — every row whose opaque
|
|
718
|
+
* scope matches, which for a multi-event subscription is the whole endpoint.
|
|
719
|
+
*
|
|
720
|
+
* This exists because a subscription, as a user models it, is one URL with a
|
|
721
|
+
* list of event checkboxes, while a row is one event. Without a group selector
|
|
722
|
+
* every operation the user thinks of as single — pause the endpoint, fix its
|
|
723
|
+
* URL, rotate its secret, read its history — becomes a fan-out the app writes
|
|
724
|
+
* by hand, and `rotateSecret` in particular becomes delete + re-subscribe,
|
|
725
|
+
* which mints new ids and orphans the delivery history.
|
|
726
|
+
*/
|
|
727
|
+
export declare type TargetSelector = string | {
|
|
728
|
+
readonly scope: Readonly<Record<string, unknown>>;
|
|
729
|
+
};
|
|
730
|
+
|
|
637
731
|
export declare interface TargetSummary {
|
|
638
732
|
readonly id: string;
|
|
733
|
+
/** The app's own scoping dimension as written at subscribe time, verbatim.
|
|
734
|
+
* `null` when the app stored none. */
|
|
735
|
+
readonly scope: Readonly<Record<string, unknown>> | null;
|
|
639
736
|
readonly event: string;
|
|
640
737
|
readonly url: string;
|
|
641
738
|
readonly active: boolean;
|
|
@@ -675,8 +772,6 @@ export declare const useWebhooks: (ctx: {
|
|
|
675
772
|
|
|
676
773
|
export declare const useWebhooksEffect: Effect.Effect<WebhooksServiceShape, never, WebhooksService>;
|
|
677
774
|
|
|
678
|
-
/** Validate subscribe input. Throws on policy violations so the
|
|
679
|
-
* caller's catch block can surface a 422-style error. */
|
|
680
775
|
export declare const validateSubscribe: (input: SubscribeInput) => void;
|
|
681
776
|
|
|
682
777
|
export declare type VerifyResult = {
|
|
@@ -821,6 +916,30 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
|
|
|
821
916
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
822
917
|
/** Serialised `RetryPolicy` — see `retry.ts`. */
|
|
823
918
|
readonly retry: ColumnBuilder<unknown, "json", boolean>;
|
|
919
|
+
/**
|
|
920
|
+
* The APP's own scoping dimension. Opaque to the framework.
|
|
921
|
+
*
|
|
922
|
+
* Stored and returned verbatim, never interpreted — the framework does not
|
|
923
|
+
* know what a team, a project or a workspace is, and does not need to. Reads
|
|
924
|
+
* filter on equality against the json you wrote:
|
|
925
|
+
*
|
|
926
|
+
* subscribe({ event, url, scope: { teamId: 'q970…' } })
|
|
927
|
+
* listTargets({ scope: { teamId } })
|
|
928
|
+
*
|
|
929
|
+
* It exists because `.with(tenant())` is one level too coarse for real
|
|
930
|
+
* deployments. A consumer's endpoints are scoped to a TEAM and a tenant has
|
|
931
|
+
* many teams; every read filters by it and every write guards on it, so
|
|
932
|
+
* without this column the plugin cannot hold their rows at all and they keep
|
|
933
|
+
* a parallel table.
|
|
934
|
+
*
|
|
935
|
+
* The precedent is `_voltro_presence.meta`, and it is worth stating because
|
|
936
|
+
* it decided a migration: that column is json the framework stores and never
|
|
937
|
+
* interprets, and it is the ONLY reason the same consumer's presence
|
|
938
|
+
* migration was lossless — their three denormalised columns went straight in.
|
|
939
|
+
* The general form: a plugin that stores rows in an app's database on the
|
|
940
|
+
* app's behalf needs one place for the app's own dimension.
|
|
941
|
+
*/
|
|
942
|
+
readonly scope: ColumnBuilder<unknown, "json", boolean>;
|
|
824
943
|
/** Optional predicate filter (subset of `Predicate`) — the engine
|
|
825
944
|
* evaluates this against each emit's payload to decide whether
|
|
826
945
|
* this target receives the delivery. */
|
|
@@ -1004,29 +1123,43 @@ export declare interface WebhooksServiceShape {
|
|
|
1004
1123
|
/** List currently-subscribed targets for an event, or all when
|
|
1005
1124
|
* `event` is omitted. The dashboard's "Targets" page consumes
|
|
1006
1125
|
* this. */
|
|
1007
|
-
readonly listTargets: (event?: string
|
|
1126
|
+
readonly listTargets: (event?: string,
|
|
1127
|
+
/** Filter to targets whose opaque `scope` equals this, key for key. The
|
|
1128
|
+
* match is exact on the keys you pass — a target with extra keys still
|
|
1129
|
+
* matches, so `{ teamId }` finds targets scoped to that team regardless of
|
|
1130
|
+
* what else the app stored beside it. */
|
|
1131
|
+
scope?: Readonly<Record<string, unknown>>) => Promise<ReadonlyArray<TargetSummary>>;
|
|
1008
1132
|
/** Soft-disable a target without deleting the row. While paused,
|
|
1009
1133
|
* emits against this target queue under
|
|
1010
1134
|
* `_voltro_webhook_deliveries` with status='pending' (no POST
|
|
1011
1135
|
* happens); `resumeTarget` flushes them. */
|
|
1012
|
-
|
|
1136
|
+
/** Every row a selector addresses. Refuses a scope that matches nothing —
|
|
1137
|
+
* an operation that silently affects zero rows is worse than an error. */
|
|
1138
|
+
readonly resolveTargets: (selector: TargetSelector) => Promise<ReadonlyArray<string>>;
|
|
1139
|
+
readonly pauseTarget: (target: TargetSelector) => Promise<void>;
|
|
1013
1140
|
/** Re-enable a paused target AND flush its queued
|
|
1014
1141
|
* `status='pending'` deliveries through the normal delivery
|
|
1015
1142
|
* workflow, in emit order (`createdAt` ascending — millisecond
|
|
1016
1143
|
* granularity) per target. */
|
|
1017
|
-
readonly resumeTarget: (
|
|
1144
|
+
readonly resumeTarget: (target: TargetSelector) => Promise<void>;
|
|
1018
1145
|
/** Hard-delete a target. Its queued `status='pending'` rows are
|
|
1019
1146
|
* deleted with it (nothing left to flush); an in-flight workflow
|
|
1020
1147
|
* run's `fetch-target` activity sees a null row and exits
|
|
1021
1148
|
* cleanly. */
|
|
1022
|
-
readonly deleteTarget: (
|
|
1149
|
+
readonly deleteTarget: (target: TargetSelector) => Promise<void>;
|
|
1023
1150
|
/** Rotate the per-target signing secret. Returns the new secret
|
|
1024
1151
|
* ONCE — store it client-side if you need to display it again.
|
|
1025
1152
|
* In-flight retries continue with the OLD secret since signing
|
|
1026
1153
|
* happens at attempt time using the row's then-current value
|
|
1027
1154
|
* (acceptable: providers retry within seconds, the rotation
|
|
1028
1155
|
* window is tight). */
|
|
1029
|
-
|
|
1156
|
+
/**
|
|
1157
|
+
* Rotate the signing secret. Addressed by a SCOPE this rotates every row of
|
|
1158
|
+
* the endpoint to the SAME new value — which is the point: N rows for one URL
|
|
1159
|
+
* must sign identically, and the previous way to achieve it was delete +
|
|
1160
|
+
* re-subscribe, minting new ids and orphaning the delivery history.
|
|
1161
|
+
*/
|
|
1162
|
+
readonly rotateSecret: (target: TargetSelector) => Promise<{
|
|
1030
1163
|
readonly secret: string;
|
|
1031
1164
|
}>;
|
|
1032
1165
|
/** Re-pin a target's `payloadVersion` to the event's current
|
|
@@ -1034,6 +1167,55 @@ export declare interface WebhooksServiceShape {
|
|
|
1034
1167
|
* the consumer has updated their handler to accept the new
|
|
1035
1168
|
* payload shape. */
|
|
1036
1169
|
readonly updateTargetPayloadVersion: (targetId: string, version: number) => Promise<void>;
|
|
1170
|
+
/**
|
|
1171
|
+
* Edit a target in place.
|
|
1172
|
+
*
|
|
1173
|
+
* Without it, changing a URL, description, filter, headers or rate limit
|
|
1174
|
+
* means delete + re-subscribe — which ROTATES the secret (every receiver has
|
|
1175
|
+
* to be reconfigured) and ORPHANS the delivery history (the rows point at a
|
|
1176
|
+
* target id that no longer exists). Neither is what "I fixed a typo in the
|
|
1177
|
+
* URL" should cost.
|
|
1178
|
+
*
|
|
1179
|
+
* `event` and `secret` are deliberately not patchable: changing the event
|
|
1180
|
+
* makes it a different subscription, and the secret has `rotateSecret`, which
|
|
1181
|
+
* returns the new value once.
|
|
1182
|
+
*/
|
|
1183
|
+
readonly updateTarget: (target: TargetSelector, patch: TargetPatch) => Promise<void>;
|
|
1184
|
+
/**
|
|
1185
|
+
* Send one delivery to ONE target, bypassing fan-out and the filter.
|
|
1186
|
+
*
|
|
1187
|
+
* `emit` fans out to every matching target, so there is no way to answer "is
|
|
1188
|
+
* THIS endpoint reachable" — the first button in every webhook UI. The
|
|
1189
|
+
* delivery is real: it is signed, recorded in the delivery log and retried
|
|
1190
|
+
* like any other, so what it proves is what production will do.
|
|
1191
|
+
*/
|
|
1192
|
+
readonly testTarget: (targetId: string, payload?: unknown) => Promise<{
|
|
1193
|
+
readonly deliveryId: string;
|
|
1194
|
+
}>;
|
|
1195
|
+
/**
|
|
1196
|
+
* Read the delivery log.
|
|
1197
|
+
*
|
|
1198
|
+
* There was no service method for it, so the only way to build a management
|
|
1199
|
+
* view was to query `_voltro_webhook_deliveries` directly — and a consumer
|
|
1200
|
+
* declined, correctly: the 0.24.0 `agent_messages` rename taught them what
|
|
1201
|
+
* app code coupled to a framework table name costs. That one was survivable
|
|
1202
|
+
* because it was a rename; a column change would not be.
|
|
1203
|
+
*
|
|
1204
|
+
* The two heavy columns (`payload`, `responseBody`) are omitted here and
|
|
1205
|
+
* available from `getDelivery`, so a list view does not pull response bodies
|
|
1206
|
+
* for 200 rows.
|
|
1207
|
+
*/
|
|
1208
|
+
readonly listDeliveries: (filter?: {
|
|
1209
|
+
readonly targetId?: string;
|
|
1210
|
+
/** Every row of an endpoint's group, so a history view needs no N-way
|
|
1211
|
+
* merge-and-re-sort in the app. */
|
|
1212
|
+
readonly scope?: Readonly<Record<string, unknown>>;
|
|
1213
|
+
readonly status?: DeliverySummary['status'];
|
|
1214
|
+
readonly since?: Date;
|
|
1215
|
+
readonly limit?: number;
|
|
1216
|
+
}) => Promise<ReadonlyArray<DeliverySummary>>;
|
|
1217
|
+
/** One delivery attempt including its payload and response body. */
|
|
1218
|
+
readonly getDelivery: (id: string) => Promise<DeliveryDetail | null>;
|
|
1037
1219
|
}
|
|
1038
1220
|
|
|
1039
1221
|
/**
|
|
@@ -1082,6 +1264,30 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
1082
1264
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
1083
1265
|
/** Serialised `RetryPolicy` — see `retry.ts`. */
|
|
1084
1266
|
readonly retry: ColumnBuilder<unknown, "json", boolean>;
|
|
1267
|
+
/**
|
|
1268
|
+
* The APP's own scoping dimension. Opaque to the framework.
|
|
1269
|
+
*
|
|
1270
|
+
* Stored and returned verbatim, never interpreted — the framework does not
|
|
1271
|
+
* know what a team, a project or a workspace is, and does not need to. Reads
|
|
1272
|
+
* filter on equality against the json you wrote:
|
|
1273
|
+
*
|
|
1274
|
+
* subscribe({ event, url, scope: { teamId: 'q970…' } })
|
|
1275
|
+
* listTargets({ scope: { teamId } })
|
|
1276
|
+
*
|
|
1277
|
+
* It exists because `.with(tenant())` is one level too coarse for real
|
|
1278
|
+
* deployments. A consumer's endpoints are scoped to a TEAM and a tenant has
|
|
1279
|
+
* many teams; every read filters by it and every write guards on it, so
|
|
1280
|
+
* without this column the plugin cannot hold their rows at all and they keep
|
|
1281
|
+
* a parallel table.
|
|
1282
|
+
*
|
|
1283
|
+
* The precedent is `_voltro_presence.meta`, and it is worth stating because
|
|
1284
|
+
* it decided a migration: that column is json the framework stores and never
|
|
1285
|
+
* interprets, and it is the ONLY reason the same consumer's presence
|
|
1286
|
+
* migration was lossless — their three denormalised columns went straight in.
|
|
1287
|
+
* The general form: a plugin that stores rows in an app's database on the
|
|
1288
|
+
* app's behalf needs one place for the app's own dimension.
|
|
1289
|
+
*/
|
|
1290
|
+
readonly scope: ColumnBuilder<unknown, "json", boolean>;
|
|
1085
1291
|
/** Optional predicate filter (subset of `Predicate`) — the engine
|
|
1086
1292
|
* evaluates this against each emit's payload to decide whether
|
|
1087
1293
|
* this target receives the delivery. */
|