@voltro/plugin-webhooks 0.85.0 → 0.86.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.
@@ -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 };