@voltro/plugin-webhooks 0.24.0 → 0.26.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/dist/errors.d.ts CHANGED
@@ -18,7 +18,7 @@ declare const WebhookDeliveryNotFound_base: Schema.TaggedErrorClass<WebhookDeliv
18
18
  /**
19
19
  * Thrown by `WebhooksService.emit` when the payload does not decode
20
20
  * against the outgoing event's declared `payload` schema (from its
21
- * `defineOutgoingEvent` descriptor). The emit is rejected BEFORE any
21
+ * declared event's `webhook:` block). The emit is rejected BEFORE any
22
22
  * delivery row is written or workflow triggered — a schema-violating
23
23
  * payload never reaches a subscriber.
24
24
  */
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ import { ColumnDefinition } from '@voltro/database';
4
4
  import { Context } from 'effect';
5
5
  import { DataStore } from '@voltro/database';
6
6
  import { Effect } from 'effect';
7
+ import { EventDescriptor } from '@voltro/protocol';
8
+ import { EventWebhookSpec } from '@voltro/protocol';
7
9
  import { FieldDefinitions } from '@voltro/database';
8
10
  import { Schema } from 'effect';
9
11
  import { Table } from '@voltro/database';
@@ -23,6 +25,24 @@ import { WorkflowInstance } from '@effect/workflow/WorkflowEngine';
23
25
  */
24
26
  export declare const acquireRateSlots: (store: DataStore, scopes: ReadonlyArray<RateScope>, now: number, windowMs?: number) => Promise<RateAcquireResult>;
25
27
 
28
+ /**
29
+ * Project a declared event onto the outgoing-event descriptor this plugin
30
+ * already understands.
31
+ *
32
+ * A projection rather than a second registry: every downstream consumer —
33
+ * the delivery workflow, the JSON-Schema export, the dashboard's event list —
34
+ * keeps reading ONE shape. Adding a parallel path for declared events would
35
+ * mean each of them handles two, which is how the two drift.
36
+ *
37
+ * The event's `name` becomes the webhook `id`, so a subscriber that registered
38
+ * for `orders.paid` keeps working across the migration and the dashboard shows
39
+ * one event rather than two spellings of it.
40
+ */
41
+ export declare const asOutgoingEvent: <Name extends string, Key, Payload>(descriptor: EventDescriptor<Name, never, never> & {
42
+ readonly payload: unknown;
43
+ readonly webhook?: EventWebhookSpec | undefined;
44
+ }) => OutgoingEventDescriptor<Payload> | undefined;
45
+
26
46
  /** What `recordDeliveryOutcome` did — surfaced so the workflow can log
27
47
  * the auto-disable transition. */
28
48
  export declare interface AutoDisableOutcome {
@@ -82,6 +102,22 @@ export declare interface CustomSignatureScheme {
82
102
  readonly verify: (rawBody: Uint8Array, secret: string, signatureHeader: string) => boolean;
83
103
  }
84
104
 
105
+ /** The structural slice of a `defineEvent` descriptor this plugin needs.
106
+ * Structural rather than an import so the plugin does not depend on a specific
107
+ * protocol version's class identity. */
108
+ export declare interface DeclaredEventLike {
109
+ readonly kind: 'event';
110
+ readonly name: string;
111
+ readonly payload: unknown;
112
+ readonly webhook?: {
113
+ readonly description?: string;
114
+ readonly version?: number;
115
+ readonly rateLimit?: {
116
+ readonly perMinute: number;
117
+ };
118
+ } | undefined;
119
+ }
120
+
85
121
  /** Default URL path for an incoming webhook when the descriptor
86
122
  * doesn't override. Stage 4's discovery walker uses this to
87
123
  * register routes. */
@@ -97,8 +133,6 @@ export declare const defaultRetryPolicy: () => RetryPolicy;
97
133
 
98
134
  export declare const defineIncomingWebhook: <Body>(spec: Omit<IncomingWebhookDescriptor<Body>, "_tag">) => IncomingWebhookDescriptor<Body>;
99
135
 
100
- export declare const defineOutgoingEvent: <Payload>(spec: Omit<OutgoingEventDescriptor<Payload>, "_tag">) => OutgoingEventDescriptor<Payload>;
101
-
102
136
  export declare const defineWebhookProvider: (spec: Omit<WebhookProviderDescriptor, "_tag">) => WebhookProviderDescriptor;
103
137
 
104
138
  /**
@@ -125,7 +159,8 @@ export declare const deliverWebhookWorkflow: Workflow.Workflow<"voltro.deliverWe
125
159
  }>, typeof Schema.Never>;
126
160
 
127
161
  declare interface DeliverWorkflowOptions {
128
- /** Discovered `defineOutgoingEvent` descriptors the workflow reads
162
+ /** Discovered outgoing events (declared events with a `webhook:` block,
163
+ * projected by `asOutgoingEvent`) — the workflow reads
129
164
  * the emitted event's `globalRateLimit` from here. Absent events
130
165
  * simply have no global limit. */
131
166
  readonly events?: ReadonlyArray<OutgoingEventDescriptor<unknown>>;
@@ -134,6 +169,29 @@ declare interface DeliverWorkflowOptions {
134
169
  readonly rateWindowMs?: number;
135
170
  }
136
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
+
137
195
  /** Compact duration literal — parsed without a deps-pulling library.
138
196
  * Covers ms / s / m / h / d. Default seconds (no suffix). */
139
197
  export declare type DurationLiteral = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}` | `${number}`;
@@ -176,6 +234,11 @@ export declare const getIdempotencyCache: () => IdempotencyCache;
176
234
  /** GitHub-style: `X-Hub-Signature-256: sha256=hex` (no timestamp). */
177
235
  export declare const githubSignature: () => HmacSignatureScheme;
178
236
 
237
+ /** Does this declared event opt into outbound HTTP delivery? */
238
+ export declare const hasWebhookAudience: (descriptor: {
239
+ readonly webhook?: EventWebhookSpec | undefined;
240
+ }) => boolean;
241
+
179
242
  export declare type HmacAlgorithm = 'hmacSha256' | 'hmacSha1';
180
243
 
181
244
  export declare interface HmacSignatureScheme {
@@ -478,7 +541,7 @@ export declare const releaseRateSlot: (store: DataStore, key: string, bucket: nu
478
541
  * Default resolution is three-tiered: the subscriber's explicit
479
542
  * value wins, then the event descriptor's per-event defaults
480
543
  * (`defaultSigning` / `defaultRetry` / `version` from
481
- * `defineOutgoingEvent`), then the package-global defaults. */
544
+ * the declared event's `webhook:` block), then the package-global defaults. */
482
545
  export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingEventDescriptor<unknown>) => {
483
546
  readonly id: string;
484
547
  readonly event: string;
@@ -487,6 +550,7 @@ export declare const resolveSubscribe: (input: SubscribeInput, event?: OutgoingE
487
550
  readonly signing: SignatureScheme;
488
551
  readonly retry: RetryPolicy;
489
552
  readonly filter: Readonly<Record<string, unknown>> | null;
553
+ readonly scope: Readonly<Record<string, unknown>> | null;
490
554
  readonly headers: Readonly<Record<string, string>> | null;
491
555
  readonly rateLimitPerMinute: number | null;
492
556
  readonly active: boolean;
@@ -569,6 +633,15 @@ export declare interface SubscribeInput {
569
633
  * this predicate fan-out to this target. The shape mirrors the
570
634
  * schema-builder's `Predicate` from `@voltro/database`. */
571
635
  readonly filter?: Readonly<Record<string, unknown>>;
636
+ /**
637
+ * The APP's own scoping dimension — opaque, stored and returned verbatim.
638
+ *
639
+ * `.with(tenant())` is one level too coarse for real deployments: endpoints
640
+ * are commonly scoped to a team, a project or a workspace, and a tenant has
641
+ * many of those. Pass whatever identifies yours; `listTargets({ scope })`
642
+ * filters on equality against it. The framework never interprets it.
643
+ */
644
+ readonly scope?: Readonly<Record<string, unknown>>;
572
645
  readonly headers?: Readonly<Record<string, string>>;
573
646
  readonly rateLimitPerMinute?: number;
574
647
  readonly format?: 'json' | 'form' | 'xml';
@@ -592,10 +665,25 @@ export declare interface SubscribeResult {
592
665
  readonly retry: RetryPolicy;
593
666
  }
594
667
 
668
+ /** The patchable subset of a target. Absent keys are left alone. */
669
+ export declare interface TargetPatch {
670
+ readonly url?: string;
671
+ readonly description?: string | null;
672
+ readonly filter?: Readonly<Record<string, unknown>> | null;
673
+ readonly scope?: Readonly<Record<string, unknown>> | null;
674
+ readonly headers?: Readonly<Record<string, string>> | null;
675
+ readonly rateLimitPerMinute?: number | null;
676
+ readonly autoDisableAfter?: number | null;
677
+ readonly format?: 'json' | 'form' | 'xml';
678
+ }
679
+
595
680
  export declare const TARGETS_TABLE = "_voltro_webhook_targets";
596
681
 
597
682
  export declare interface TargetSummary {
598
683
  readonly id: string;
684
+ /** The app's own scoping dimension as written at subscribe time, verbatim.
685
+ * `null` when the app stored none. */
686
+ readonly scope: Readonly<Record<string, unknown>> | null;
599
687
  readonly event: string;
600
688
  readonly url: string;
601
689
  readonly active: boolean;
@@ -781,6 +869,30 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
781
869
  readonly signing: ColumnBuilder<unknown, "json", boolean>;
782
870
  /** Serialised `RetryPolicy` — see `retry.ts`. */
783
871
  readonly retry: ColumnBuilder<unknown, "json", boolean>;
872
+ /**
873
+ * The APP's own scoping dimension. Opaque to the framework.
874
+ *
875
+ * Stored and returned verbatim, never interpreted — the framework does not
876
+ * know what a team, a project or a workspace is, and does not need to. Reads
877
+ * filter on equality against the json you wrote:
878
+ *
879
+ * subscribe({ event, url, scope: { teamId: 'q970…' } })
880
+ * listTargets({ scope: { teamId } })
881
+ *
882
+ * It exists because `.with(tenant())` is one level too coarse for real
883
+ * deployments. A consumer's endpoints are scoped to a TEAM and a tenant has
884
+ * many teams; every read filters by it and every write guards on it, so
885
+ * without this column the plugin cannot hold their rows at all and they keep
886
+ * a parallel table.
887
+ *
888
+ * The precedent is `_voltro_presence.meta`, and it is worth stating because
889
+ * it decided a migration: that column is json the framework stores and never
890
+ * interprets, and it is the ONLY reason the same consumer's presence
891
+ * migration was lossless — their three denormalised columns went straight in.
892
+ * The general form: a plugin that stores rows in an app's database on the
893
+ * app's behalf needs one place for the app's own dimension.
894
+ */
895
+ readonly scope: ColumnBuilder<unknown, "json", boolean>;
784
896
  /** Optional predicate filter (subset of `Predicate`) — the engine
785
897
  * evaluates this against each emit's payload to decide whether
786
898
  * this target receives the delivery. */
@@ -868,7 +980,7 @@ export declare type WebhookId = string;
868
980
  /**
869
981
  * Thrown by `WebhooksService.emit` when the payload does not decode
870
982
  * against the outgoing event's declared `payload` schema (from its
871
- * `defineOutgoingEvent` descriptor). The emit is rejected BEFORE any
983
+ * declared event's `webhook:` block). The emit is rejected BEFORE any
872
984
  * delivery row is written or workflow triggered — a schema-violating
873
985
  * payload never reaches a subscriber.
874
986
  */
@@ -940,7 +1052,7 @@ export declare class WebhooksService extends WebhooksService_base {
940
1052
  declare const WebhooksService_base: Context.TagClass<WebhooksService, "@voltro/webhooks/WebhooksService", WebhooksServiceShape>;
941
1053
 
942
1054
  export declare interface WebhooksServiceOptions {
943
- /** Discovered `defineOutgoingEvent` descriptors. When present,
1055
+ /** Discovered outgoing events (projected from declared events). When present,
944
1056
  * `subscribe` resolves the event's `defaultSigning` /
945
1057
  * `defaultRetry` / `version` before the package-global defaults,
946
1058
  * and `emit` decodes the payload against the event's schema. */
@@ -950,13 +1062,13 @@ export declare interface WebhooksServiceOptions {
950
1062
  export declare interface WebhooksServiceShape {
951
1063
  readonly subscribe: (input: SubscribeInput) => Promise<SubscribeResult>;
952
1064
  /** Emit an event to every subscribed target. Accepts the event id
953
- * OR the `defineOutgoingEvent` descriptor itself — passing the
1065
+ * OR the declared event itself — passing the
954
1066
  * descriptor types `payload` against its schema at the call site.
955
1067
  * Either way, when the descriptor is known (directly or via the
956
1068
  * discovered-events registry) the payload is DECODED against its
957
1069
  * schema and a mismatch throws `WebhookPayloadInvalid` before any
958
1070
  * delivery is created. */
959
- readonly emit: <P>(event: string | OutgoingEventDescriptor<P>, payload: P) => Promise<EmitResult>;
1071
+ readonly emit: <P>(event: string | OutgoingEventDescriptor<P> | DeclaredEventLike, payload: P) => Promise<EmitResult>;
960
1072
  /** Manual re-trigger for the dashboard's "Replay" button on a
961
1073
  * failed delivery row. Re-runs the workflow at attempt 1 with
962
1074
  * the original payload. */
@@ -964,7 +1076,12 @@ export declare interface WebhooksServiceShape {
964
1076
  /** List currently-subscribed targets for an event, or all when
965
1077
  * `event` is omitted. The dashboard's "Targets" page consumes
966
1078
  * this. */
967
- readonly listTargets: (event?: string) => Promise<ReadonlyArray<TargetSummary>>;
1079
+ readonly listTargets: (event?: string,
1080
+ /** Filter to targets whose opaque `scope` equals this, key for key. The
1081
+ * match is exact on the keys you pass — a target with extra keys still
1082
+ * matches, so `{ teamId }` finds targets scoped to that team regardless of
1083
+ * what else the app stored beside it. */
1084
+ scope?: Readonly<Record<string, unknown>>) => Promise<ReadonlyArray<TargetSummary>>;
968
1085
  /** Soft-disable a target without deleting the row. While paused,
969
1086
  * emits against this target queue under
970
1087
  * `_voltro_webhook_deliveries` with status='pending' (no POST
@@ -994,6 +1111,52 @@ export declare interface WebhooksServiceShape {
994
1111
  * the consumer has updated their handler to accept the new
995
1112
  * payload shape. */
996
1113
  readonly updateTargetPayloadVersion: (targetId: string, version: number) => Promise<void>;
1114
+ /**
1115
+ * Edit a target in place.
1116
+ *
1117
+ * Without it, changing a URL, description, filter, headers or rate limit
1118
+ * means delete + re-subscribe — which ROTATES the secret (every receiver has
1119
+ * to be reconfigured) and ORPHANS the delivery history (the rows point at a
1120
+ * target id that no longer exists). Neither is what "I fixed a typo in the
1121
+ * URL" should cost.
1122
+ *
1123
+ * `event` and `secret` are deliberately not patchable: changing the event
1124
+ * makes it a different subscription, and the secret has `rotateSecret`, which
1125
+ * returns the new value once.
1126
+ */
1127
+ readonly updateTarget: (targetId: string, patch: TargetPatch) => Promise<void>;
1128
+ /**
1129
+ * Send one delivery to ONE target, bypassing fan-out and the filter.
1130
+ *
1131
+ * `emit` fans out to every matching target, so there is no way to answer "is
1132
+ * THIS endpoint reachable" — the first button in every webhook UI. The
1133
+ * delivery is real: it is signed, recorded in the delivery log and retried
1134
+ * like any other, so what it proves is what production will do.
1135
+ */
1136
+ readonly testTarget: (targetId: string, payload?: unknown) => Promise<{
1137
+ readonly deliveryId: string;
1138
+ }>;
1139
+ /**
1140
+ * Read the delivery log.
1141
+ *
1142
+ * There was no service method for it, so the only way to build a management
1143
+ * view was to query `_voltro_webhook_deliveries` directly — and a consumer
1144
+ * declined, correctly: the 0.24.0 `agent_messages` rename taught them what
1145
+ * app code coupled to a framework table name costs. That one was survivable
1146
+ * because it was a rename; a column change would not be.
1147
+ *
1148
+ * The two heavy columns (`payload`, `responseBody`) are omitted here and
1149
+ * available from `getDelivery`, so a list view does not pull response bodies
1150
+ * for 200 rows.
1151
+ */
1152
+ readonly listDeliveries: (filter?: {
1153
+ readonly targetId?: string;
1154
+ readonly status?: DeliverySummary['status'];
1155
+ readonly since?: Date;
1156
+ readonly limit?: number;
1157
+ }) => Promise<ReadonlyArray<DeliverySummary>>;
1158
+ /** One delivery attempt including its payload and response body. */
1159
+ readonly getDelivery: (id: string) => Promise<DeliveryDetail | null>;
997
1160
  }
998
1161
 
999
1162
  /**
@@ -1042,6 +1205,30 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
1042
1205
  readonly signing: ColumnBuilder<unknown, "json", boolean>;
1043
1206
  /** Serialised `RetryPolicy` — see `retry.ts`. */
1044
1207
  readonly retry: ColumnBuilder<unknown, "json", boolean>;
1208
+ /**
1209
+ * The APP's own scoping dimension. Opaque to the framework.
1210
+ *
1211
+ * Stored and returned verbatim, never interpreted — the framework does not
1212
+ * know what a team, a project or a workspace is, and does not need to. Reads
1213
+ * filter on equality against the json you wrote:
1214
+ *
1215
+ * subscribe({ event, url, scope: { teamId: 'q970…' } })
1216
+ * listTargets({ scope: { teamId } })
1217
+ *
1218
+ * It exists because `.with(tenant())` is one level too coarse for real
1219
+ * deployments. A consumer's endpoints are scoped to a TEAM and a tenant has
1220
+ * many teams; every read filters by it and every write guards on it, so
1221
+ * without this column the plugin cannot hold their rows at all and they keep
1222
+ * a parallel table.
1223
+ *
1224
+ * The precedent is `_voltro_presence.meta`, and it is worth stating because
1225
+ * it decided a migration: that column is json the framework stores and never
1226
+ * interprets, and it is the ONLY reason the same consumer's presence
1227
+ * migration was lossless — their three denormalised columns went straight in.
1228
+ * The general form: a plugin that stores rows in an app's database on the
1229
+ * app's behalf needs one place for the app's own dimension.
1230
+ */
1231
+ readonly scope: ColumnBuilder<unknown, "json", boolean>;
1045
1232
  /** Optional predicate filter (subset of `Predicate`) — the engine
1046
1233
  * evaluates this against each emit's payload to decide whether
1047
1234
  * this target receives the delivery. */