@beignet/core 0.0.14 → 0.0.16
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 +20 -0
- package/README.md +48 -4
- package/dist/ports/index.d.ts +13 -0
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +5 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/server/hooks/error-reporting.d.ts +79 -0
- package/dist/server/hooks/error-reporting.d.ts.map +1 -0
- package/dist/server/hooks/error-reporting.js +147 -0
- package/dist/server/hooks/error-reporting.js.map +1 -0
- package/dist/server/hooks/index.d.ts +1 -0
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js +1 -0
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/server.d.ts +5 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/use-case-route.d.ts +2 -2
- package/dist/webhooks/index.d.ts +195 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +313 -0
- package/dist/webhooks/index.js.map +1 -0
- package/package.json +5 -1
- package/src/ports/index.ts +37 -0
- package/src/server/hooks/error-reporting.ts +249 -0
- package/src/server/hooks/index.ts +8 -0
- package/src/server/server.ts +5 -0
- package/src/server/use-case-route.ts +2 -2
- package/src/webhooks/index.ts +555 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/core/webhooks
|
|
3
|
+
*
|
|
4
|
+
* Provider-neutral inbound webhook primitives for Beignet applications.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Raw webhook payload. Signature verification must use this unparsed body.
|
|
11
|
+
*/
|
|
12
|
+
export type WebhookRawBody = string | Uint8Array | ArrayBuffer;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Request headers normalized to lowercase keys.
|
|
16
|
+
*/
|
|
17
|
+
export type WebhookHeaders = Record<string, string | undefined>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Input passed to webhook verifiers.
|
|
21
|
+
*/
|
|
22
|
+
export interface VerifyWebhookInput {
|
|
23
|
+
rawBody: WebhookRawBody;
|
|
24
|
+
headers?: WebhookHeaders;
|
|
25
|
+
signature?: string;
|
|
26
|
+
receivedAt?: Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Provider-neutral inbound webhook event.
|
|
31
|
+
*/
|
|
32
|
+
export interface WebhookEvent<TPayload = unknown> {
|
|
33
|
+
id: string;
|
|
34
|
+
type: string;
|
|
35
|
+
provider?: string;
|
|
36
|
+
createdAt?: Date;
|
|
37
|
+
payload: TPayload;
|
|
38
|
+
raw?: unknown;
|
|
39
|
+
metadata?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Verifies an inbound webhook and converts it into a provider-neutral event.
|
|
44
|
+
*/
|
|
45
|
+
export interface WebhookVerifier<TEvent extends WebhookEvent = WebhookEvent> {
|
|
46
|
+
verify(input: VerifyWebhookInput): Promise<TEvent>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Standard Schema payload catalog keyed by provider event type.
|
|
51
|
+
*/
|
|
52
|
+
export type WebhookEventSchemas = Record<string, StandardSchemaV1>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Infer the parsed output type from a Standard Schema.
|
|
56
|
+
*/
|
|
57
|
+
export type InferSchemaOutput<T extends StandardSchemaV1> =
|
|
58
|
+
StandardSchemaV1.InferOutput<T>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Infer a typed webhook event from an event catalog entry.
|
|
62
|
+
*/
|
|
63
|
+
export type WebhookEventForSchema<
|
|
64
|
+
Events extends WebhookEventSchemas,
|
|
65
|
+
Type extends keyof Events & string,
|
|
66
|
+
> = WebhookEvent<InferSchemaOutput<Events[Type]>> & { type: Type };
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Infer any typed event from a webhook definition's event catalog.
|
|
70
|
+
*/
|
|
71
|
+
export type InferWebhookEvent<TWebhook> =
|
|
72
|
+
TWebhook extends WebhookDef<string, infer Events>
|
|
73
|
+
? {
|
|
74
|
+
[Type in keyof Events & string]: WebhookEventForSchema<Events, Type>;
|
|
75
|
+
}[keyof Events & string]
|
|
76
|
+
: WebhookEvent;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Provider-neutral webhook definition.
|
|
80
|
+
*/
|
|
81
|
+
export interface WebhookDef<
|
|
82
|
+
Name extends string = string,
|
|
83
|
+
Events extends WebhookEventSchemas = WebhookEventSchemas,
|
|
84
|
+
> {
|
|
85
|
+
name: Name;
|
|
86
|
+
provider?: string;
|
|
87
|
+
events: Events;
|
|
88
|
+
verifier?: WebhookVerifier;
|
|
89
|
+
metadata?: Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Options accepted by `defineWebhook(...)`.
|
|
94
|
+
*/
|
|
95
|
+
export interface DefineWebhookOptions<
|
|
96
|
+
Events extends WebhookEventSchemas = WebhookEventSchemas,
|
|
97
|
+
> {
|
|
98
|
+
provider?: string;
|
|
99
|
+
events?: Events;
|
|
100
|
+
verifier?: WebhookVerifier;
|
|
101
|
+
metadata?: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Options accepted by `verifyWebhook(...)`.
|
|
106
|
+
*/
|
|
107
|
+
export interface VerifyWebhookOptions {
|
|
108
|
+
verifier?: WebhookVerifier;
|
|
109
|
+
allowUnknownEvents?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Options for the in-memory webhook verifier.
|
|
114
|
+
*/
|
|
115
|
+
export interface CreateMemoryWebhookVerifierOptions {
|
|
116
|
+
events?: readonly WebhookEvent[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* In-memory verifier exposed for tests.
|
|
121
|
+
*/
|
|
122
|
+
export interface MemoryWebhookVerifier extends WebhookVerifier {
|
|
123
|
+
readonly verifiedEvents: readonly WebhookEvent[];
|
|
124
|
+
queue(event: WebhookEvent): void;
|
|
125
|
+
reset(): void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Options for the generic HMAC webhook verifier.
|
|
130
|
+
*/
|
|
131
|
+
export interface CreateHmacWebhookVerifierOptions {
|
|
132
|
+
secret: string;
|
|
133
|
+
/**
|
|
134
|
+
* Header that carries the provider signature.
|
|
135
|
+
*
|
|
136
|
+
* @default "x-webhook-signature"
|
|
137
|
+
*/
|
|
138
|
+
signatureHeader?: string;
|
|
139
|
+
/**
|
|
140
|
+
* Web Crypto HMAC hash algorithm.
|
|
141
|
+
*
|
|
142
|
+
* @default "SHA-256"
|
|
143
|
+
*/
|
|
144
|
+
algorithm?: "SHA-256" | "SHA-384" | "SHA-512";
|
|
145
|
+
/**
|
|
146
|
+
* Optional signature prefix, such as "sha256=".
|
|
147
|
+
*/
|
|
148
|
+
signaturePrefix?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Provider name attached to verified events.
|
|
151
|
+
*/
|
|
152
|
+
provider?: string;
|
|
153
|
+
/**
|
|
154
|
+
* Dot path used to read the event ID from a JSON payload.
|
|
155
|
+
*
|
|
156
|
+
* @default "id"
|
|
157
|
+
*/
|
|
158
|
+
eventIdPath?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Dot path used to read the event type from a JSON payload.
|
|
161
|
+
*
|
|
162
|
+
* @default "type"
|
|
163
|
+
*/
|
|
164
|
+
eventTypePath?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Error thrown for invalid webhook definitions and inputs.
|
|
169
|
+
*/
|
|
170
|
+
export class WebhookOptionsError extends Error {
|
|
171
|
+
constructor(message: string) {
|
|
172
|
+
super(message);
|
|
173
|
+
this.name = "WebhookOptionsError";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Error thrown when verification fails.
|
|
179
|
+
*/
|
|
180
|
+
export class WebhookVerificationError extends Error {
|
|
181
|
+
readonly webhookName?: string;
|
|
182
|
+
readonly provider?: string;
|
|
183
|
+
readonly code: string;
|
|
184
|
+
readonly cause?: unknown;
|
|
185
|
+
|
|
186
|
+
constructor(args: {
|
|
187
|
+
message: string;
|
|
188
|
+
webhookName?: string;
|
|
189
|
+
provider?: string;
|
|
190
|
+
code: string;
|
|
191
|
+
cause?: unknown;
|
|
192
|
+
}) {
|
|
193
|
+
super(args.message);
|
|
194
|
+
this.name = "WebhookVerificationError";
|
|
195
|
+
this.webhookName = args.webhookName;
|
|
196
|
+
this.provider = args.provider;
|
|
197
|
+
this.code = args.code;
|
|
198
|
+
this.cause = args.cause;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Error thrown when a verified event fails payload validation.
|
|
204
|
+
*/
|
|
205
|
+
export class WebhookValidationError extends Error {
|
|
206
|
+
readonly webhookName: string;
|
|
207
|
+
readonly eventType: string;
|
|
208
|
+
readonly issues: readonly StandardSchemaV1.Issue[];
|
|
209
|
+
|
|
210
|
+
constructor(args: {
|
|
211
|
+
webhookName: string;
|
|
212
|
+
eventType: string;
|
|
213
|
+
issues: readonly StandardSchemaV1.Issue[];
|
|
214
|
+
}) {
|
|
215
|
+
super(
|
|
216
|
+
`Webhook "${args.webhookName}" event "${args.eventType}" payload validation failed: ${formatIssues(args.issues)}`,
|
|
217
|
+
);
|
|
218
|
+
this.name = "WebhookValidationError";
|
|
219
|
+
this.webhookName = args.webhookName;
|
|
220
|
+
this.eventType = args.eventType;
|
|
221
|
+
this.issues = args.issues;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Define a typed inbound webhook surface.
|
|
227
|
+
*/
|
|
228
|
+
export function defineWebhook<
|
|
229
|
+
Name extends string,
|
|
230
|
+
Events extends WebhookEventSchemas = WebhookEventSchemas,
|
|
231
|
+
>(
|
|
232
|
+
name: Name,
|
|
233
|
+
options: DefineWebhookOptions<Events> = {},
|
|
234
|
+
): WebhookDef<Name, Events> {
|
|
235
|
+
if (!name) throw new WebhookOptionsError("Webhook name is required.");
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
name,
|
|
239
|
+
provider: options.provider,
|
|
240
|
+
events: (options.events ?? {}) as Events,
|
|
241
|
+
verifier: options.verifier,
|
|
242
|
+
metadata: options.metadata,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Verify a raw webhook request and validate the matching event payload schema.
|
|
248
|
+
*/
|
|
249
|
+
export async function verifyWebhook<
|
|
250
|
+
TWebhook extends WebhookDef<string, WebhookEventSchemas>,
|
|
251
|
+
>(
|
|
252
|
+
webhook: TWebhook,
|
|
253
|
+
input: VerifyWebhookInput,
|
|
254
|
+
options: VerifyWebhookOptions = {},
|
|
255
|
+
): Promise<InferWebhookEvent<TWebhook> | WebhookEvent> {
|
|
256
|
+
validateWebhook(webhook);
|
|
257
|
+
const verifier = options.verifier ?? webhook.verifier;
|
|
258
|
+
if (!verifier) {
|
|
259
|
+
throw new WebhookOptionsError(
|
|
260
|
+
`Webhook "${webhook.name}" does not have a verifier.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let event: WebhookEvent;
|
|
265
|
+
try {
|
|
266
|
+
event = await verifier.verify(input);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof WebhookVerificationError) throw error;
|
|
269
|
+
throw new WebhookVerificationError({
|
|
270
|
+
message: `Webhook "${webhook.name}" verification failed.`,
|
|
271
|
+
webhookName: webhook.name,
|
|
272
|
+
provider: webhook.provider,
|
|
273
|
+
code: "verification_failed",
|
|
274
|
+
cause: error,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return parseWebhookEvent(webhook, event, options);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Validate a verified webhook event against its catalog entry.
|
|
283
|
+
*/
|
|
284
|
+
export async function parseWebhookEvent<
|
|
285
|
+
TWebhook extends WebhookDef<string, WebhookEventSchemas>,
|
|
286
|
+
>(
|
|
287
|
+
webhook: TWebhook,
|
|
288
|
+
event: WebhookEvent,
|
|
289
|
+
options: Pick<VerifyWebhookOptions, "allowUnknownEvents"> = {},
|
|
290
|
+
): Promise<InferWebhookEvent<TWebhook> | WebhookEvent> {
|
|
291
|
+
validateWebhook(webhook);
|
|
292
|
+
validateEvent(webhook, event);
|
|
293
|
+
|
|
294
|
+
const schema = webhook.events[event.type];
|
|
295
|
+
if (!schema) {
|
|
296
|
+
if (options.allowUnknownEvents ?? true) return event;
|
|
297
|
+
throw new WebhookVerificationError({
|
|
298
|
+
message: `Webhook "${webhook.name}" received unknown event type "${event.type}".`,
|
|
299
|
+
webhookName: webhook.name,
|
|
300
|
+
provider: webhook.provider,
|
|
301
|
+
code: "unknown_event_type",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const result = await schema["~standard"].validate(event.payload);
|
|
306
|
+
if (result.issues?.length) {
|
|
307
|
+
throw new WebhookValidationError({
|
|
308
|
+
webhookName: webhook.name,
|
|
309
|
+
eventType: event.type,
|
|
310
|
+
issues: result.issues,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!("value" in result)) {
|
|
315
|
+
throw new Error("Invalid Standard Schema result: missing value");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
...event,
|
|
320
|
+
payload: result.value,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Create an in-memory verifier for tests.
|
|
326
|
+
*/
|
|
327
|
+
export function createMemoryWebhookVerifier(
|
|
328
|
+
options: CreateMemoryWebhookVerifierOptions = {},
|
|
329
|
+
): MemoryWebhookVerifier {
|
|
330
|
+
const queuedEvents = [...(options.events ?? [])];
|
|
331
|
+
const verifiedEvents: WebhookEvent[] = [];
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
get verifiedEvents() {
|
|
335
|
+
return verifiedEvents;
|
|
336
|
+
},
|
|
337
|
+
async verify() {
|
|
338
|
+
const event = queuedEvents.shift();
|
|
339
|
+
if (!event) {
|
|
340
|
+
throw new WebhookVerificationError({
|
|
341
|
+
message: "No memory webhook event is queued.",
|
|
342
|
+
code: "missing_memory_event",
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
verifiedEvents.push(event);
|
|
346
|
+
return event;
|
|
347
|
+
},
|
|
348
|
+
queue(event) {
|
|
349
|
+
queuedEvents.push(event);
|
|
350
|
+
},
|
|
351
|
+
reset() {
|
|
352
|
+
queuedEvents.length = 0;
|
|
353
|
+
verifiedEvents.length = 0;
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Create a generic JSON + HMAC verifier.
|
|
360
|
+
*/
|
|
361
|
+
export function createHmacWebhookVerifier(
|
|
362
|
+
options: CreateHmacWebhookVerifierOptions,
|
|
363
|
+
): WebhookVerifier {
|
|
364
|
+
if (!options.secret) {
|
|
365
|
+
throw new WebhookOptionsError("Webhook HMAC secret is required.");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const signatureHeader = normalizeHeaderName(
|
|
369
|
+
options.signatureHeader ?? "x-webhook-signature",
|
|
370
|
+
);
|
|
371
|
+
const algorithm = options.algorithm ?? "SHA-256";
|
|
372
|
+
const eventIdPath = options.eventIdPath ?? "id";
|
|
373
|
+
const eventTypePath = options.eventTypePath ?? "type";
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
async verify(input) {
|
|
377
|
+
const signature = input.signature ?? input.headers?.[signatureHeader];
|
|
378
|
+
if (!signature) {
|
|
379
|
+
throw new WebhookVerificationError({
|
|
380
|
+
message: `Missing ${signatureHeader} header.`,
|
|
381
|
+
provider: options.provider,
|
|
382
|
+
code: "missing_signature",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const expected = await hmacHex(algorithm, options.secret, input.rawBody);
|
|
387
|
+
const actual = normalizeSignature(signature, options.signaturePrefix);
|
|
388
|
+
if (!(await timingSafeStringEqual(actual, expected))) {
|
|
389
|
+
throw new WebhookVerificationError({
|
|
390
|
+
message: "Webhook signature is invalid.",
|
|
391
|
+
provider: options.provider,
|
|
392
|
+
code: "invalid_signature",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const payload = parseJsonBody(input.rawBody);
|
|
397
|
+
const id = readStringPath(payload, eventIdPath);
|
|
398
|
+
const type = readStringPath(payload, eventTypePath);
|
|
399
|
+
|
|
400
|
+
if (!id) {
|
|
401
|
+
throw new WebhookVerificationError({
|
|
402
|
+
message: `Webhook payload is missing string event ID at "${eventIdPath}".`,
|
|
403
|
+
provider: options.provider,
|
|
404
|
+
code: "missing_event_id",
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (!type) {
|
|
408
|
+
throw new WebhookVerificationError({
|
|
409
|
+
message: `Webhook payload is missing string event type at "${eventTypePath}".`,
|
|
410
|
+
provider: options.provider,
|
|
411
|
+
code: "missing_event_type",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
id,
|
|
417
|
+
type,
|
|
418
|
+
provider: options.provider,
|
|
419
|
+
payload,
|
|
420
|
+
raw: payload,
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function validateWebhook(webhook: WebhookDef) {
|
|
427
|
+
if (!webhook.name) throw new WebhookOptionsError("Webhook name is required.");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function validateEvent(webhook: WebhookDef, event: WebhookEvent) {
|
|
431
|
+
if (!event.id) {
|
|
432
|
+
throw new WebhookVerificationError({
|
|
433
|
+
message: `Webhook "${webhook.name}" event is missing an ID.`,
|
|
434
|
+
webhookName: webhook.name,
|
|
435
|
+
provider: webhook.provider,
|
|
436
|
+
code: "missing_event_id",
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (!event.type) {
|
|
440
|
+
throw new WebhookVerificationError({
|
|
441
|
+
message: `Webhook "${webhook.name}" event is missing a type.`,
|
|
442
|
+
webhookName: webhook.name,
|
|
443
|
+
provider: webhook.provider,
|
|
444
|
+
code: "missing_event_type",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function formatPath(path: StandardSchemaV1.Issue["path"]): string {
|
|
450
|
+
if (!path?.length) return "";
|
|
451
|
+
|
|
452
|
+
return path
|
|
453
|
+
.map((segment) =>
|
|
454
|
+
typeof segment === "object" && segment !== null && "key" in segment
|
|
455
|
+
? String(segment.key)
|
|
456
|
+
: String(segment),
|
|
457
|
+
)
|
|
458
|
+
.join(".");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
|
|
462
|
+
return issues
|
|
463
|
+
.map((issue) => {
|
|
464
|
+
const path = formatPath(issue.path);
|
|
465
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
466
|
+
})
|
|
467
|
+
.join("; ");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function normalizeHeaderName(name: string): string {
|
|
471
|
+
return name.toLowerCase();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function normalizeSignature(signature: string, prefix: string | undefined) {
|
|
475
|
+
const trimmed = signature.trim();
|
|
476
|
+
if (!prefix) return trimmed;
|
|
477
|
+
return trimmed.startsWith(prefix) ? trimmed.slice(prefix.length) : trimmed;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function rawBodyBytes(rawBody: WebhookRawBody): Uint8Array {
|
|
481
|
+
if (typeof rawBody === "string") return new TextEncoder().encode(rawBody);
|
|
482
|
+
if (rawBody instanceof Uint8Array) return rawBody;
|
|
483
|
+
return new Uint8Array(rawBody);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function rawBodyText(rawBody: WebhookRawBody): string {
|
|
487
|
+
if (typeof rawBody === "string") return rawBody;
|
|
488
|
+
return new TextDecoder().decode(rawBodyBytes(rawBody));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseJsonBody(rawBody: WebhookRawBody): unknown {
|
|
492
|
+
try {
|
|
493
|
+
return JSON.parse(rawBodyText(rawBody));
|
|
494
|
+
} catch (error) {
|
|
495
|
+
throw new WebhookVerificationError({
|
|
496
|
+
message: "Webhook payload must be valid JSON.",
|
|
497
|
+
code: "invalid_json",
|
|
498
|
+
cause: error,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function readStringPath(input: unknown, path: string): string | undefined {
|
|
504
|
+
let value = input;
|
|
505
|
+
for (const segment of path.split(".")) {
|
|
506
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
value = (value as Record<string, unknown>)[segment];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function hmacHex(
|
|
516
|
+
algorithm: CreateHmacWebhookVerifierOptions["algorithm"],
|
|
517
|
+
secret: string,
|
|
518
|
+
rawBody: WebhookRawBody,
|
|
519
|
+
): Promise<string> {
|
|
520
|
+
const key = await crypto.subtle.importKey(
|
|
521
|
+
"raw",
|
|
522
|
+
new TextEncoder().encode(secret),
|
|
523
|
+
{ name: "HMAC", hash: algorithm ?? "SHA-256" },
|
|
524
|
+
false,
|
|
525
|
+
["sign"],
|
|
526
|
+
);
|
|
527
|
+
const bytes = rawBodyBytes(rawBody);
|
|
528
|
+
const data = bytes.buffer.slice(
|
|
529
|
+
bytes.byteOffset,
|
|
530
|
+
bytes.byteOffset + bytes.byteLength,
|
|
531
|
+
) as ArrayBuffer;
|
|
532
|
+
const signature = await crypto.subtle.sign("HMAC", key, data);
|
|
533
|
+
return bytesToHex(new Uint8Array(signature));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function bytesToHex(bytes: Uint8Array): string {
|
|
537
|
+
return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function timingSafeStringEqual(a: string, b: string): Promise<boolean> {
|
|
541
|
+
const encoder = new TextEncoder();
|
|
542
|
+
const [digestA, digestB] = await Promise.all([
|
|
543
|
+
crypto.subtle.digest("SHA-256", encoder.encode(a)),
|
|
544
|
+
crypto.subtle.digest("SHA-256", encoder.encode(b)),
|
|
545
|
+
]);
|
|
546
|
+
const bytesA = new Uint8Array(digestA);
|
|
547
|
+
const bytesB = new Uint8Array(digestB);
|
|
548
|
+
|
|
549
|
+
let mismatch = 0;
|
|
550
|
+
for (let index = 0; index < bytesA.length; index++) {
|
|
551
|
+
mismatch |= (bytesA[index] ?? 0) ^ (bytesB[index] ?? 0);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return mismatch === 0;
|
|
555
|
+
}
|