@voltro/plugin-webhooks 0.85.0 → 0.87.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 +5987 -0
- package/THIRD-PARTY-NOTICES.md +864 -253
- package/dist/autoDisable.js +41 -0
- package/dist/deferredEmit.js +17 -0
- package/dist/deliverWorkflow.js +350 -0
- package/dist/dsl.js +14 -0
- package/dist/encode.js +53 -0
- package/dist/eventAudience.js +14 -0
- package/dist/idempotency.js +49 -0
- package/dist/incoming.js +205 -0
- package/dist/index.d.ts +46 -4
- package/dist/index.js +17 -1304
- package/dist/mixin.d.ts +16 -2
- package/dist/mixin.js +2 -0
- package/dist/plugin.js +19 -0
- package/dist/providers/generic.js +19 -0
- package/dist/providers/github.js +16 -0
- package/dist/providers/index.js +5 -86
- package/dist/providers/slack.js +39 -0
- package/dist/providers/stripe.js +30 -0
- package/dist/rateLimit.js +61 -0
- package/dist/retry.js +65 -0
- package/dist/service.js +527 -0
- package/dist/signing.js +145 -0
- package/dist/standardWebhooks.js +88 -0
- package/package.json +7 -7
- package/dist/signing-DeHObNv6.js +0 -234
package/dist/incoming.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { verifyRequest as e } from "./signing.js";
|
|
2
|
+
import { getIdempotencyCache as t, parseTtl as n } from "./idempotency.js";
|
|
3
|
+
import { Schema as r } from "effect";
|
|
4
|
+
import { publishServerError as i } from "@voltro/protocol";
|
|
5
|
+
import { createLogger as a } from "@voltro/logger";
|
|
6
|
+
//#region src/incoming.ts
|
|
7
|
+
var o = (e, t) => ({
|
|
8
|
+
status: e,
|
|
9
|
+
contentType: "application/json; charset=utf-8",
|
|
10
|
+
body: JSON.stringify(t)
|
|
11
|
+
}), s = "voltroWebhookVerification", c = class extends Error {
|
|
12
|
+
webhookId;
|
|
13
|
+
name = "UnverifiedIncomingWebhook";
|
|
14
|
+
constructor(e) {
|
|
15
|
+
super(`incoming webhook '${e}' declares no verification. It would be mounted as a public, unauthenticated POST that runs your application code, so the framework refuses to mount it. Pick one on the descriptor:\n · provider: stripeWebhookProvider() — a preset (HMAC scheme + idempotency + body type)\n · signature: { _tag: 'hmac', algorithm: 'hmacSha256', header: 'X-Signature', encoding: 'hex', includeTimestamp: true }\n · verification: 'provider' — the handler verifies with the provider's own SDK\n · verification: 'none' — deliberately public; a gateway / IP allow-list owns it\nA signature-verified webhook reads its shared secret from VOLTRO_WEBHOOK_SECRET_${e.toUpperCase().replace(/[^A-Z0-9]/g, "_")}. The framework never invents that value — the sender holds the other half of it.`), this.webhookId = e;
|
|
16
|
+
}
|
|
17
|
+
}, l = (e) => `VOLTRO_WEBHOOK_SECRET_${e.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`, u = (e) => {
|
|
18
|
+
let t = e.signature ?? e.provider?.signature ?? null;
|
|
19
|
+
return (e.verification === void 0 || e.verification === "signature") && t === null ? null : e.verification ?? "signature";
|
|
20
|
+
}, d = (d, f) => {
|
|
21
|
+
let p = d.provider, m = d.signature ?? p?.signature ?? null, h = d.idempotency ?? p?.idempotency ?? null, g = d.bodyType ?? p?.bodyType ?? "json", _ = u(d);
|
|
22
|
+
if (_ === null) throw new c(d.id);
|
|
23
|
+
let v = _;
|
|
24
|
+
v === "none" && a({ scope: `webhook:${d.id}` }).warn("incoming webhook mounted WITHOUT verification — anyone who can reach this URL can run its handler", {
|
|
25
|
+
webhookId: d.id,
|
|
26
|
+
verification: "none"
|
|
27
|
+
});
|
|
28
|
+
let y = f.idempotencyCache ?? t(), b = n(h?.ttl), x = f.log ?? ((e) => {
|
|
29
|
+
a({ scope: `webhook:${e.webhookId}` }).debug("incoming webhook", {
|
|
30
|
+
status: e.status,
|
|
31
|
+
signatureOk: e.signatureOk,
|
|
32
|
+
idempotency: e.idempotency,
|
|
33
|
+
durationMs: e.durationMs,
|
|
34
|
+
...e.errorMessage === void 0 ? {} : { errorMessage: e.errorMessage }
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return Object.assign(async (t) => {
|
|
38
|
+
let n = Date.now();
|
|
39
|
+
if (t.method !== "POST") {
|
|
40
|
+
let e = o(405, {
|
|
41
|
+
error: "method not allowed",
|
|
42
|
+
expected: "POST"
|
|
43
|
+
});
|
|
44
|
+
return x({
|
|
45
|
+
webhookId: d.id,
|
|
46
|
+
status: 405,
|
|
47
|
+
signatureOk: "skipped",
|
|
48
|
+
idempotency: "skipped",
|
|
49
|
+
durationMs: Date.now() - n
|
|
50
|
+
}), e;
|
|
51
|
+
}
|
|
52
|
+
let a = "skipped";
|
|
53
|
+
if (m !== null && v === "signature") {
|
|
54
|
+
let r = await f.resolveSecret(d.id);
|
|
55
|
+
if (r === null) {
|
|
56
|
+
let e = `no signing secret configured — set ${l(d.id)}`, t = o(503, {
|
|
57
|
+
error: "webhook not configured",
|
|
58
|
+
reason: e
|
|
59
|
+
});
|
|
60
|
+
return x({
|
|
61
|
+
webhookId: d.id,
|
|
62
|
+
status: 503,
|
|
63
|
+
signatureOk: !1,
|
|
64
|
+
idempotency: "skipped",
|
|
65
|
+
durationMs: Date.now() - n,
|
|
66
|
+
errorMessage: e
|
|
67
|
+
}), t;
|
|
68
|
+
}
|
|
69
|
+
{
|
|
70
|
+
let i = e(m, {
|
|
71
|
+
rawBody: t.rawBody,
|
|
72
|
+
secret: r,
|
|
73
|
+
headers: t.headers
|
|
74
|
+
});
|
|
75
|
+
if (a = i.ok, !i.ok) {
|
|
76
|
+
let e = o(401, {
|
|
77
|
+
error: "invalid signature",
|
|
78
|
+
reason: i.reason
|
|
79
|
+
});
|
|
80
|
+
return x({
|
|
81
|
+
webhookId: d.id,
|
|
82
|
+
status: 401,
|
|
83
|
+
signatureOk: !1,
|
|
84
|
+
idempotency: "skipped",
|
|
85
|
+
durationMs: Date.now() - n,
|
|
86
|
+
errorMessage: i.reason
|
|
87
|
+
}), e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let s;
|
|
92
|
+
try {
|
|
93
|
+
if (g === "json") {
|
|
94
|
+
let e = new TextDecoder().decode(t.rawBody);
|
|
95
|
+
s = e.length === 0 ? null : JSON.parse(e);
|
|
96
|
+
} else if (g === "form") {
|
|
97
|
+
let e = new TextDecoder().decode(t.rawBody), n = new URLSearchParams(e), r = {};
|
|
98
|
+
for (let [e, t] of n.entries()) r[e] = t;
|
|
99
|
+
s = r;
|
|
100
|
+
} else s = t.rawBody;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
let t = o(400, {
|
|
103
|
+
error: "malformed body",
|
|
104
|
+
reason: e.message
|
|
105
|
+
});
|
|
106
|
+
return x({
|
|
107
|
+
webhookId: d.id,
|
|
108
|
+
status: 400,
|
|
109
|
+
signatureOk: a,
|
|
110
|
+
idempotency: "skipped",
|
|
111
|
+
durationMs: Date.now() - n,
|
|
112
|
+
errorMessage: e.message
|
|
113
|
+
}), t;
|
|
114
|
+
}
|
|
115
|
+
let c;
|
|
116
|
+
try {
|
|
117
|
+
c = r.decodeUnknownSync(d.payload)(s);
|
|
118
|
+
} catch (e) {
|
|
119
|
+
let t = o(422, {
|
|
120
|
+
error: "schema validation failed",
|
|
121
|
+
reason: e.message
|
|
122
|
+
});
|
|
123
|
+
return x({
|
|
124
|
+
webhookId: d.id,
|
|
125
|
+
status: 422,
|
|
126
|
+
signatureOk: a,
|
|
127
|
+
idempotency: "skipped",
|
|
128
|
+
durationMs: Date.now() - n,
|
|
129
|
+
errorMessage: e.message
|
|
130
|
+
}), t;
|
|
131
|
+
}
|
|
132
|
+
let u, p = "skipped";
|
|
133
|
+
if (h !== null) {
|
|
134
|
+
let e = h.from;
|
|
135
|
+
if (u = typeof e == "string" ? t.headers[e.toLowerCase()] : e(t.headers, t.rawBody), u !== void 0 && u.length > 0) {
|
|
136
|
+
let e = y.claim(u, b);
|
|
137
|
+
if (p = e, e === "duplicate") {
|
|
138
|
+
let e = o(200, {
|
|
139
|
+
received: !0,
|
|
140
|
+
duplicate: !0
|
|
141
|
+
});
|
|
142
|
+
return x({
|
|
143
|
+
webhookId: d.id,
|
|
144
|
+
status: 200,
|
|
145
|
+
signatureOk: a,
|
|
146
|
+
idempotency: "duplicate",
|
|
147
|
+
durationMs: Date.now() - n
|
|
148
|
+
}), e;
|
|
149
|
+
}
|
|
150
|
+
if (e === "inflight") {
|
|
151
|
+
let e = o(409, {
|
|
152
|
+
error: "request already in flight",
|
|
153
|
+
idempotencyKey: u
|
|
154
|
+
});
|
|
155
|
+
return x({
|
|
156
|
+
webhookId: d.id,
|
|
157
|
+
status: 409,
|
|
158
|
+
signatureOk: a,
|
|
159
|
+
idempotency: "inflight",
|
|
160
|
+
durationMs: Date.now() - n
|
|
161
|
+
}), e;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
let e = f.resolveWorkflows?.();
|
|
167
|
+
await d.handler({
|
|
168
|
+
body: c,
|
|
169
|
+
rawBody: t.rawBody,
|
|
170
|
+
headers: t.headers,
|
|
171
|
+
idempotencyKey: u ?? "",
|
|
172
|
+
...e ? { workflows: e } : {}
|
|
173
|
+
}), u !== void 0 && y.commit(u);
|
|
174
|
+
let r = o(200, { received: !0 });
|
|
175
|
+
return x({
|
|
176
|
+
webhookId: d.id,
|
|
177
|
+
status: 200,
|
|
178
|
+
signatureOk: a,
|
|
179
|
+
idempotency: p,
|
|
180
|
+
durationMs: Date.now() - n
|
|
181
|
+
}), r;
|
|
182
|
+
} catch (e) {
|
|
183
|
+
u !== void 0 && y.release(u);
|
|
184
|
+
let t = o(500, {
|
|
185
|
+
error: "handler threw",
|
|
186
|
+
reason: e.message
|
|
187
|
+
});
|
|
188
|
+
return x({
|
|
189
|
+
webhookId: d.id,
|
|
190
|
+
status: 500,
|
|
191
|
+
signatureOk: a,
|
|
192
|
+
idempotency: p,
|
|
193
|
+
durationMs: Date.now() - n,
|
|
194
|
+
errorMessage: e.message
|
|
195
|
+
}), i({
|
|
196
|
+
error: e,
|
|
197
|
+
source: "webhook",
|
|
198
|
+
name: d.id,
|
|
199
|
+
fields: { status: 500 }
|
|
200
|
+
}), t;
|
|
201
|
+
}
|
|
202
|
+
}, { [s]: v });
|
|
203
|
+
}, f = (e) => `/webhooks/${e}`;
|
|
204
|
+
//#endregion
|
|
205
|
+
export { c as UnverifiedIncomingWebhook, s as WEBHOOK_VERIFICATION_PROPERTY, f as defaultIncomingPath, u as incomingWebhookVerification, d as mountIncomingWebhook };
|
package/dist/index.d.ts
CHANGED
|
@@ -617,6 +617,9 @@ emitter: () => Pick<WebhooksServiceShape, "emit"> | undefined) => OutboxHandlerD
|
|
|
617
617
|
*/
|
|
618
618
|
export declare const matchesFilter: (filter: WebhookFilter | Readonly<Record<string, unknown>> | null, payload: unknown) => boolean;
|
|
619
619
|
|
|
620
|
+
/** The longest overlap `rotateSecret` accepts, and its default: 30 days, unless ended earlier. */
|
|
621
|
+
export declare const MAX_SECRET_OVERLAP_SECONDS: number;
|
|
622
|
+
|
|
620
623
|
/** A mounted handler, carrying its verification declaration. */
|
|
621
624
|
export declare type MountedIncomingWebhook = ((request: IncomingRequest) => Promise<IncomingResponse>) & {
|
|
622
625
|
readonly [WEBHOOK_VERIFICATION_PROPERTY]: IncomingWebhookVerification;
|
|
@@ -822,6 +825,28 @@ export declare interface RetryPolicy {
|
|
|
822
825
|
|
|
823
826
|
export declare type RetryStrategy = 'fixed' | 'linear' | 'exponential';
|
|
824
827
|
|
|
828
|
+
/** What `rotateSecret` returns. */
|
|
829
|
+
export declare interface RotatedSecret {
|
|
830
|
+
/** The new secret — returned once; hand it to the receiver. */
|
|
831
|
+
readonly secret: string;
|
|
832
|
+
/** Until when deliveries are signed under the previous secret too. `null`:
|
|
833
|
+
* no overlap — asked for none, or a scheme with one signature per message. */
|
|
834
|
+
readonly previousSignedUntil: Date | null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** How long a rotated webhook secret keeps signing beside the new one. */
|
|
838
|
+
export declare interface RotateSecretOptions {
|
|
839
|
+
/**
|
|
840
|
+
* Seconds every delivery still carries a signature under the previous
|
|
841
|
+
* secret, so the receiver can switch when it is ready. The length is the
|
|
842
|
+
* RECEIVER's switch-over time — nothing on the sending side can measure it —
|
|
843
|
+
* so by default the overlap lasts until {@link WebhooksServiceShape.endSecretOverlap}
|
|
844
|
+
* ends it (once the receiver has switched), and at most 30 days. Pass a
|
|
845
|
+
* number to bound it yourself; `0` switches at once (after a leak).
|
|
846
|
+
*/
|
|
847
|
+
readonly overlapSeconds?: number;
|
|
848
|
+
}
|
|
849
|
+
|
|
825
850
|
export declare type SignatureScheme = HmacSignatureScheme | CustomSignatureScheme | StandardWebhooksSignatureScheme;
|
|
826
851
|
|
|
827
852
|
export declare interface SignedRequest {
|
|
@@ -1250,7 +1275,7 @@ export declare const _voltroWebhookDeliveriesTable: Table<"_voltro_webhook_deliv
|
|
|
1250
1275
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
1251
1276
|
/** Attempt counter (1-indexed). */
|
|
1252
1277
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
1253
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1278
|
+
readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
1254
1279
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
1255
1280
|
* from a referenced event row would lose the snapshot if the
|
|
1256
1281
|
* source event was deleted. */
|
|
@@ -1389,6 +1414,13 @@ export declare const _voltroWebhookTargetsTable: Table<"_voltro_webhook_targets"
|
|
|
1389
1414
|
* (a non-ciphertext value passes through); `voltro db encrypt-column
|
|
1390
1415
|
* _voltro_webhook_targets.secret` converts them in place. */
|
|
1391
1416
|
readonly secret: ColumnBuilder<string, "text", boolean>;
|
|
1417
|
+
/** During a rotation (`rotateSecret`): the secret the receiver may still
|
|
1418
|
+
* verify with. Every delivery until {@link previousSecretUntil} carries a
|
|
1419
|
+
* signature under it beside the new one (Standard Webhooks' signature list),
|
|
1420
|
+
* so the receiver switches when it is ready. Encrypted like `secret`. */
|
|
1421
|
+
readonly previousSecret: ColumnBuilder<string | null, "text", boolean>;
|
|
1422
|
+
/** When deliveries stop signing under `previousSecret`. */
|
|
1423
|
+
readonly previousSecretUntil: ColumnBuilder<Date | null, "timestamp", boolean>;
|
|
1392
1424
|
/** Serialised `SignatureScheme` discriminated union — see `signing.ts`.
|
|
1393
1425
|
* Stored as JSON so future schemes don't require a schema migration. */
|
|
1394
1426
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
@@ -1702,8 +1734,11 @@ export declare interface WebhooksServiceShape {
|
|
|
1702
1734
|
* must sign identically, and the previous way to achieve it was delete +
|
|
1703
1735
|
* re-subscribe, minting new ids and orphaning the delivery history.
|
|
1704
1736
|
*/
|
|
1705
|
-
readonly rotateSecret: (target: TargetSelector) => Promise<
|
|
1706
|
-
|
|
1737
|
+
readonly rotateSecret: (target: TargetSelector, options?: RotateSecretOptions) => Promise<RotatedSecret>;
|
|
1738
|
+
/** End a rotation's overlap once the receiver verifies with the new secret: the
|
|
1739
|
+
* previous secret stops signing at once. Returns how many targets were overlapping. */
|
|
1740
|
+
readonly endSecretOverlap: (target: TargetSelector) => Promise<{
|
|
1741
|
+
readonly ended: number;
|
|
1707
1742
|
}>;
|
|
1708
1743
|
/** Re-pin a target's `payloadVersion` to the event's current
|
|
1709
1744
|
* version. Called from the dashboard's "Re-pin" action after
|
|
@@ -1805,6 +1840,13 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
1805
1840
|
* (a non-ciphertext value passes through); `voltro db encrypt-column
|
|
1806
1841
|
* _voltro_webhook_targets.secret` converts them in place. */
|
|
1807
1842
|
readonly secret: ColumnBuilder<string, "text", boolean>;
|
|
1843
|
+
/** During a rotation (`rotateSecret`): the secret the receiver may still
|
|
1844
|
+
* verify with. Every delivery until {@link previousSecretUntil} carries a
|
|
1845
|
+
* signature under it beside the new one (Standard Webhooks' signature list),
|
|
1846
|
+
* so the receiver switches when it is ready. Encrypted like `secret`. */
|
|
1847
|
+
readonly previousSecret: ColumnBuilder<string | null, "text", boolean>;
|
|
1848
|
+
/** When deliveries stop signing under `previousSecret`. */
|
|
1849
|
+
readonly previousSecretUntil: ColumnBuilder<Date | null, "timestamp", boolean>;
|
|
1808
1850
|
/** Serialised `SignatureScheme` discriminated union — see `signing.ts`.
|
|
1809
1851
|
* Stored as JSON so future schemes don't require a schema migration. */
|
|
1810
1852
|
readonly signing: ColumnBuilder<unknown, "json", boolean>;
|
|
@@ -1910,7 +1952,7 @@ export declare const webhookTables: () => readonly [ Table<"_voltro_webhook_targ
|
|
|
1910
1952
|
readonly eventId: ColumnBuilder<string | null, "text", boolean>;
|
|
1911
1953
|
/** Attempt counter (1-indexed). */
|
|
1912
1954
|
readonly attempt: ColumnBuilder<number, "integer", boolean>;
|
|
1913
|
-
readonly status: ColumnBuilder<"failed" | "
|
|
1955
|
+
readonly status: ColumnBuilder<"failed" | "succeeded" | "pending" | "inFlight" | "retryScheduled", "text", boolean>;
|
|
1914
1956
|
/** Payload as sent over the wire. Stored verbatim — re-rendering
|
|
1915
1957
|
* from a referenced event row would lose the snapshot if the
|
|
1916
1958
|
* source event was deleted. */
|