@amirabaris/webhook-kit 0.1.1 → 0.1.2
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/README.md +37 -20
- package/bin/webhook-kit.mjs +2 -0
- package/dist/index.cjs +404 -311
- package/dist/index.d.cts +168 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +168 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +168 -0
- package/dist/index.js +404 -311
- package/dist/index.mjs +382 -312
- package/dist/index.mjs.map +1 -1
- package/docs/agent-rules.md +10 -2
- package/package.json +14 -11
package/dist/index.mjs
CHANGED
|
@@ -62,18 +62,78 @@ var ReplayAttackError = class extends WebhookKitError {
|
|
|
62
62
|
this.eventId = eventId;
|
|
63
63
|
}
|
|
64
64
|
};
|
|
65
|
-
// src/
|
|
65
|
+
// src/utils/headers.ts
|
|
66
|
+
/**
|
|
67
|
+
* Extracts a header value from a plain headers record case-insensitively.
|
|
68
|
+
*
|
|
69
|
+
* @param record - Plain headers record object.
|
|
70
|
+
* @param lowerTarget - Pre-lowercased target header name.
|
|
71
|
+
* @returns Header value string or undefined.
|
|
72
|
+
*/
|
|
73
|
+
function extractFromRecord(record, lowerTarget) {
|
|
74
|
+
for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
|
|
75
|
+
const value = record[key];
|
|
76
|
+
return Array.isArray(value) ? value[0] : value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Case-insensitively extracts a header value from a `Headers` instance or plain record.
|
|
81
|
+
*
|
|
82
|
+
* @param headers - Web standard `Headers` instance or record.
|
|
83
|
+
* @param targetName - Lowercase target header name.
|
|
84
|
+
* @returns Header value string or undefined.
|
|
85
|
+
*/
|
|
86
|
+
function extractHeaderValue$1(headers, targetName) {
|
|
87
|
+
if (!headers || typeof headers !== "object") return;
|
|
88
|
+
if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
|
|
89
|
+
return extractFromRecord(headers, targetName.toLowerCase());
|
|
90
|
+
}
|
|
91
|
+
// src/store/store.ts
|
|
66
92
|
const DEFAULT_REPLAY_STORE_MAX_SIZE = 1e4;
|
|
67
93
|
const DEFAULT_REPLAY_STORE_TTL_SECONDS = 300;
|
|
68
94
|
const GENERIC_EVENT_ID_HEADERS = [
|
|
69
95
|
"x-webhook-id",
|
|
70
96
|
"webhook-id",
|
|
97
|
+
"svix-id",
|
|
71
98
|
"x-github-delivery",
|
|
72
99
|
"x-shopify-webhook-id",
|
|
73
100
|
"x-event-id",
|
|
74
101
|
"x-idempotency-key"
|
|
75
102
|
];
|
|
76
103
|
/**
|
|
104
|
+
* Tracks in-flight replay check promises per store instance to prevent TOCTOU race conditions
|
|
105
|
+
* during concurrent duplicate deliveries across stores that only implement `has` and `set`.
|
|
106
|
+
*/
|
|
107
|
+
const inFlightPerStore = /* @__PURE__ */ new WeakMap();
|
|
108
|
+
/**
|
|
109
|
+
* Sanitizes and bounds the maximum capacity for `InMemoryReplayStore`.
|
|
110
|
+
*
|
|
111
|
+
* @param size - Candidate capacity option.
|
|
112
|
+
* @returns Positive bounded integer capacity.
|
|
113
|
+
*/
|
|
114
|
+
function sanitizeCapacity(size) {
|
|
115
|
+
return typeof size === "number" && Number.isFinite(size) && size > 0 ? Math.floor(size) : DEFAULT_REPLAY_STORE_MAX_SIZE;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Sanitizes and bounds the default TTL for `InMemoryReplayStore`.
|
|
119
|
+
*
|
|
120
|
+
* @param ttl - Candidate default TTL option.
|
|
121
|
+
* @returns Positive finite TTL in seconds.
|
|
122
|
+
*/
|
|
123
|
+
function sanitizeTtl(ttl) {
|
|
124
|
+
return typeof ttl === "number" && Number.isFinite(ttl) && ttl > 0 ? ttl : 300;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolves the effective TTL for a store insert from explicit TTL or default fallback.
|
|
128
|
+
*
|
|
129
|
+
* @param ttl - Explicit TTL in seconds.
|
|
130
|
+
* @param defaultTtl - Default TTL fallback.
|
|
131
|
+
* @returns Resolved finite TTL in seconds.
|
|
132
|
+
*/
|
|
133
|
+
function resolveInsertTtl(ttl, defaultTtl) {
|
|
134
|
+
return typeof ttl === "number" && Number.isFinite(ttl) ? ttl : defaultTtl;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
77
137
|
* Default in-memory TTL replay store backed by a JavaScript `Map`.
|
|
78
138
|
* Automatically evicts expired entries on lookup and enforces an upper capacity bound
|
|
79
139
|
* with LRU (Least Recently Used) eviction.
|
|
@@ -83,9 +143,10 @@ var InMemoryReplayStore = class {
|
|
|
83
143
|
defaultTtlSeconds;
|
|
84
144
|
now;
|
|
85
145
|
entries = /* @__PURE__ */ new Map();
|
|
146
|
+
lastPruneMs = 0;
|
|
86
147
|
constructor(options = {}) {
|
|
87
|
-
this.maxSize = options.maxSize
|
|
88
|
-
this.defaultTtlSeconds = options.defaultTtlSeconds
|
|
148
|
+
this.maxSize = sanitizeCapacity(options.maxSize);
|
|
149
|
+
this.defaultTtlSeconds = sanitizeTtl(options.defaultTtlSeconds);
|
|
89
150
|
this.now = options.now ?? Date.now;
|
|
90
151
|
}
|
|
91
152
|
/**
|
|
@@ -95,6 +156,7 @@ var InMemoryReplayStore = class {
|
|
|
95
156
|
* @returns `true` if `eventId` is present and unexpired, `false` otherwise.
|
|
96
157
|
*/
|
|
97
158
|
has(eventId) {
|
|
159
|
+
if (typeof eventId !== "string" || eventId.length === 0) return false;
|
|
98
160
|
const expiresAt = this.entries.get(eventId);
|
|
99
161
|
if (expiresAt === void 0) return false;
|
|
100
162
|
if (this.now() >= expiresAt) {
|
|
@@ -110,22 +172,35 @@ var InMemoryReplayStore = class {
|
|
|
110
172
|
* @param ttlSeconds - Time-to-live in seconds before expiration.
|
|
111
173
|
*/
|
|
112
174
|
set(eventId, ttlSeconds = this.defaultTtlSeconds) {
|
|
113
|
-
|
|
175
|
+
if (typeof eventId !== "string" || eventId.length === 0) return;
|
|
176
|
+
const effectiveTtl = resolveInsertTtl(ttlSeconds, this.defaultTtlSeconds);
|
|
114
177
|
if (effectiveTtl <= 0) {
|
|
115
178
|
this.entries.delete(eventId);
|
|
116
179
|
return;
|
|
117
180
|
}
|
|
118
|
-
|
|
119
|
-
else if (this.entries.size >= this.maxSize) this.evictExpiredOrOldest();
|
|
181
|
+
this.prepareInsertSlot(eventId);
|
|
120
182
|
this.entries.set(eventId, this.now() + effectiveTtl * 1e3);
|
|
121
183
|
}
|
|
122
184
|
/**
|
|
185
|
+
* Atomically records `eventId` with TTL if it does not already exist in the store (set-if-not-exists).
|
|
186
|
+
*
|
|
187
|
+
* @param eventId - Unique webhook event or delivery identifier.
|
|
188
|
+
* @param ttlSeconds - Time-to-live in seconds before expiration.
|
|
189
|
+
* @returns `true` if newly recorded, `false` if already present in the store.
|
|
190
|
+
*/
|
|
191
|
+
setNx(eventId, ttlSeconds = this.defaultTtlSeconds) {
|
|
192
|
+
if (this.has(eventId)) return false;
|
|
193
|
+
this.set(eventId, ttlSeconds);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
123
197
|
* Removes a specific `eventId` from the store.
|
|
124
198
|
*
|
|
125
199
|
* @param eventId - Unique webhook event or delivery identifier.
|
|
126
200
|
* @returns `true` if the entry existed and was removed.
|
|
127
201
|
*/
|
|
128
202
|
delete(eventId) {
|
|
203
|
+
if (typeof eventId !== "string" || eventId.length === 0) return false;
|
|
129
204
|
return this.entries.delete(eventId);
|
|
130
205
|
}
|
|
131
206
|
/**
|
|
@@ -160,8 +235,36 @@ var InMemoryReplayStore = class {
|
|
|
160
235
|
get capacity() {
|
|
161
236
|
return this.maxSize;
|
|
162
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Prepares a map slot for insertion by evicting an existing key or pruning capacity.
|
|
240
|
+
*
|
|
241
|
+
* @param eventId - Candidate event ID key.
|
|
242
|
+
*/
|
|
243
|
+
prepareInsertSlot(eventId) {
|
|
244
|
+
if (this.entries.has(eventId)) {
|
|
245
|
+
this.entries.delete(eventId);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (this.entries.size >= this.maxSize) this.evictExpiredOrOldest();
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Fast-path check from the head of the insertion-ordered Map to purge expired entries.
|
|
252
|
+
*
|
|
253
|
+
* @param currentMs - Current time in milliseconds.
|
|
254
|
+
*/
|
|
255
|
+
evictHeadExpired(currentMs) {
|
|
256
|
+
for (const [key, expiresAt] of this.entries) if (currentMs >= expiresAt) {
|
|
257
|
+
this.entries.delete(key);
|
|
258
|
+
if (this.entries.size < this.maxSize) return;
|
|
259
|
+
} else break;
|
|
260
|
+
}
|
|
163
261
|
evictExpiredOrOldest() {
|
|
164
|
-
this.
|
|
262
|
+
const currentMs = this.now();
|
|
263
|
+
this.evictHeadExpired(currentMs);
|
|
264
|
+
if (this.entries.size >= this.maxSize && (currentMs - this.lastPruneMs >= 1e3 || currentMs < this.lastPruneMs)) {
|
|
265
|
+
this.lastPruneMs = currentMs;
|
|
266
|
+
this.prune();
|
|
267
|
+
}
|
|
165
268
|
if (this.entries.size < this.maxSize) return;
|
|
166
269
|
const oldestKey = this.entries.keys().next().value;
|
|
167
270
|
if (oldestKey !== void 0) this.entries.delete(oldestKey);
|
|
@@ -177,22 +280,6 @@ function createInMemoryReplayStore(options) {
|
|
|
177
280
|
return new InMemoryReplayStore(options);
|
|
178
281
|
}
|
|
179
282
|
/**
|
|
180
|
-
* Case-insensitively extracts a single header value from a `Headers` instance or plain record.
|
|
181
|
-
*
|
|
182
|
-
* @param headers - Web standard `Headers` instance or record.
|
|
183
|
-
* @param targetName - Lowercase target header name.
|
|
184
|
-
* @returns Header value string or undefined.
|
|
185
|
-
*/
|
|
186
|
-
function getHeaderCaseInsensitive(headers, targetName) {
|
|
187
|
-
if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
|
|
188
|
-
const record = headers;
|
|
189
|
-
const lowerTarget = targetName.toLowerCase();
|
|
190
|
-
for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
|
|
191
|
-
const value = record[key];
|
|
192
|
-
return Array.isArray(value) ? value[0] : value;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
283
|
* Searches candidate header names in a `Headers` or record object for a non-empty value.
|
|
197
284
|
*
|
|
198
285
|
* @param headers - Headers source.
|
|
@@ -202,11 +289,46 @@ function getHeaderCaseInsensitive(headers, targetName) {
|
|
|
202
289
|
function findHeaderValue(headers, candidateNames) {
|
|
203
290
|
if (!headers) return;
|
|
204
291
|
for (const name of candidateNames) {
|
|
205
|
-
const trimmed =
|
|
292
|
+
const trimmed = extractHeaderValue$1(headers, name)?.trim();
|
|
206
293
|
if (trimmed) return trimmed;
|
|
207
294
|
}
|
|
208
295
|
}
|
|
209
296
|
/**
|
|
297
|
+
* Determines whether a candidate value is a non-null, non-array object record.
|
|
298
|
+
*
|
|
299
|
+
* @param value - Candidate value.
|
|
300
|
+
* @returns `true` if value is a plain object record.
|
|
301
|
+
*/
|
|
302
|
+
function isPlainRecord(value) {
|
|
303
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Parses a raw JSON string into a record object, ignoring non-object outputs.
|
|
307
|
+
*
|
|
308
|
+
* @param rawBody - Raw body text.
|
|
309
|
+
* @returns Parsed object record or null.
|
|
310
|
+
*/
|
|
311
|
+
function parseJsonRecord(rawBody) {
|
|
312
|
+
if (typeof rawBody !== "string" || rawBody.length === 0) return null;
|
|
313
|
+
try {
|
|
314
|
+
const parsed = JSON.parse(rawBody);
|
|
315
|
+
return isPlainRecord(parsed) ? parsed : null;
|
|
316
|
+
} catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Resolves a record object from either an already-parsed payload or a raw JSON string.
|
|
322
|
+
*
|
|
323
|
+
* @param payload - Parsed payload value.
|
|
324
|
+
* @param rawBody - Raw body string.
|
|
325
|
+
* @returns Object record or null.
|
|
326
|
+
*/
|
|
327
|
+
function resolvePayloadObject(payload, rawBody) {
|
|
328
|
+
if (isPlainRecord(payload)) return payload;
|
|
329
|
+
return parseJsonRecord(rawBody);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
210
332
|
* Extracts a string or finite number `id` property from a parsed payload or raw JSON body.
|
|
211
333
|
*
|
|
212
334
|
* @param payload - Already parsed payload object if available.
|
|
@@ -221,23 +343,6 @@ function extractIdFromPayload(payload, rawBody) {
|
|
|
221
343
|
if (typeof idValue === "number" && Number.isFinite(idValue)) return String(idValue);
|
|
222
344
|
}
|
|
223
345
|
/**
|
|
224
|
-
* Resolves a record object from either an already-parsed payload or a raw JSON string.
|
|
225
|
-
*
|
|
226
|
-
* @param payload - Parsed payload value.
|
|
227
|
-
* @param rawBody - Raw body string.
|
|
228
|
-
* @returns Object record or null.
|
|
229
|
-
*/
|
|
230
|
-
function resolvePayloadObject(payload, rawBody) {
|
|
231
|
-
if (typeof payload === "object" && payload !== null) return payload;
|
|
232
|
-
if (!rawBody) return null;
|
|
233
|
-
try {
|
|
234
|
-
const parsed = JSON.parse(rawBody);
|
|
235
|
-
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
236
|
-
} catch {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
346
|
* Extracts a matching header value from a Web Standard `Request` body if provided.
|
|
242
347
|
*
|
|
243
348
|
* @param body - Candidate raw body input.
|
|
@@ -248,6 +353,19 @@ function findHeaderInRequest(body, headerCandidates) {
|
|
|
248
353
|
if (typeof Request !== "undefined" && body instanceof Request) return findHeaderValue(body.headers, headerCandidates);
|
|
249
354
|
}
|
|
250
355
|
/**
|
|
356
|
+
* Sanitizes an explicitly provided `eventId` option into a non-empty string.
|
|
357
|
+
*
|
|
358
|
+
* @param eventId - Candidate event ID.
|
|
359
|
+
* @returns Trimmed event ID or undefined.
|
|
360
|
+
*/
|
|
361
|
+
function resolveExplicitEventId(eventId) {
|
|
362
|
+
if (typeof eventId === "string") {
|
|
363
|
+
const trimmed = eventId.trim();
|
|
364
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
365
|
+
}
|
|
366
|
+
if (typeof eventId === "number" && Number.isFinite(eventId)) return String(eventId);
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
251
369
|
* Resolves the unique webhook event ID from explicit options, delivery headers, or payload `id`.
|
|
252
370
|
*
|
|
253
371
|
* @param options - Source options containing explicit `eventId`, `headers`, `body`, `payload`, and `rawBody`.
|
|
@@ -255,7 +373,7 @@ function findHeaderInRequest(body, headerCandidates) {
|
|
|
255
373
|
* @returns Resolved non-empty event ID string or `undefined`.
|
|
256
374
|
*/
|
|
257
375
|
function resolveWebhookEventId(options, headerCandidates = GENERIC_EVENT_ID_HEADERS) {
|
|
258
|
-
const explicitId = options.eventId
|
|
376
|
+
const explicitId = resolveExplicitEventId(options.eventId);
|
|
259
377
|
if (explicitId) return explicitId;
|
|
260
378
|
const fromHeaders = findHeaderValue(options.headers, headerCandidates) ?? findHeaderInRequest(options.body, headerCandidates);
|
|
261
379
|
if (fromHeaders) return fromHeaders;
|
|
@@ -269,26 +387,78 @@ function resolveWebhookEventId(options, headerCandidates = GENERIC_EVENT_ID_HEAD
|
|
|
269
387
|
* @returns Positive TTL in seconds (defaults to 300s).
|
|
270
388
|
*/
|
|
271
389
|
function resolveEffectiveTtl(storeTtlSeconds, toleranceSeconds) {
|
|
272
|
-
if (storeTtlSeconds
|
|
273
|
-
if (toleranceSeconds
|
|
274
|
-
return
|
|
390
|
+
if (typeof storeTtlSeconds === "number" && Number.isFinite(storeTtlSeconds) && storeTtlSeconds > 0) return storeTtlSeconds;
|
|
391
|
+
if (typeof toleranceSeconds === "number" && Number.isFinite(toleranceSeconds) && toleranceSeconds > 0) return toleranceSeconds;
|
|
392
|
+
return 300;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Checks and records an event ID using a store's basic `has` and `set` primitives.
|
|
396
|
+
*
|
|
397
|
+
* @param store - Webhook replay store.
|
|
398
|
+
* @param eventId - Resolved event identifier.
|
|
399
|
+
* @param ttlSeconds - Time-to-live in seconds.
|
|
400
|
+
* @returns `ReplayAttackError` if already seen, or `null` on success.
|
|
401
|
+
*/
|
|
402
|
+
async function executeStoreCheckAndRecord(store, eventId, ttlSeconds) {
|
|
403
|
+
if (await store.has(eventId)) return new ReplayAttackError(eventId);
|
|
404
|
+
await store.set(eventId, ttlSeconds);
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Synchronizes concurrent in-flight checks for the same `eventId` across stores lacking atomic operations.
|
|
409
|
+
*
|
|
410
|
+
* @param store - Webhook replay store.
|
|
411
|
+
* @param eventId - Resolved event identifier.
|
|
412
|
+
* @param ttlSeconds - Time-to-live in seconds.
|
|
413
|
+
* @returns `ReplayAttackError` if duplicate or already tracked, or `null` on success.
|
|
414
|
+
*/
|
|
415
|
+
async function handleInFlightReplayCheck(store, eventId, ttlSeconds) {
|
|
416
|
+
let storeInFlight = inFlightPerStore.get(store);
|
|
417
|
+
if (!storeInFlight) {
|
|
418
|
+
storeInFlight = /* @__PURE__ */ new Map();
|
|
419
|
+
inFlightPerStore.set(store, storeInFlight);
|
|
420
|
+
}
|
|
421
|
+
const existing = storeInFlight.get(eventId);
|
|
422
|
+
if (existing) {
|
|
423
|
+
const result = await existing;
|
|
424
|
+
return result === null ? new ReplayAttackError(eventId) : result;
|
|
425
|
+
}
|
|
426
|
+
const checkPromise = executeStoreCheckAndRecord(store, eventId, ttlSeconds);
|
|
427
|
+
storeInFlight.set(eventId, checkPromise);
|
|
428
|
+
try {
|
|
429
|
+
return await checkPromise;
|
|
430
|
+
} finally {
|
|
431
|
+
storeInFlight.delete(eventId);
|
|
432
|
+
}
|
|
275
433
|
}
|
|
276
434
|
/**
|
|
277
435
|
* Verifies that `eventId` has not already been seen in `store`, and records it with a TTL if unseen.
|
|
436
|
+
* Automatically leverages atomic `setNx` when supported, or serializes in-flight checks to prevent
|
|
437
|
+
* TOCTOU race conditions under high concurrency.
|
|
278
438
|
*
|
|
279
439
|
* @param options - Store instance, resolved `eventId`, and TTL configuration.
|
|
280
440
|
* @returns `ReplayAttackError` if `eventId` is already present in `store`, or `null` if unseen / no store.
|
|
281
441
|
*/
|
|
282
442
|
async function checkAndRecordReplay(options) {
|
|
283
|
-
const { store
|
|
284
|
-
if (!store
|
|
285
|
-
|
|
443
|
+
const { store } = options;
|
|
444
|
+
if (!store) return null;
|
|
445
|
+
const resolvedId = resolveExplicitEventId(options.eventId);
|
|
446
|
+
if (!resolvedId) return null;
|
|
286
447
|
const ttlSeconds = resolveEffectiveTtl(options.storeTtlSeconds, options.toleranceSeconds);
|
|
287
|
-
await store.
|
|
288
|
-
return
|
|
448
|
+
if (typeof store.setNx === "function") return await store.setNx(resolvedId, ttlSeconds) ? null : new ReplayAttackError(resolvedId);
|
|
449
|
+
return handleInFlightReplayCheck(store, resolvedId, ttlSeconds);
|
|
289
450
|
}
|
|
290
451
|
// src/crypto/subtle.ts
|
|
291
452
|
/**
|
|
453
|
+
* Sanitizes and bounds the maximum capacity for `CryptoKeyLruCache`.
|
|
454
|
+
*
|
|
455
|
+
* @param size - Candidate capacity option.
|
|
456
|
+
* @returns Positive bounded integer capacity.
|
|
457
|
+
*/
|
|
458
|
+
function sanitizeCacheCapacity(size) {
|
|
459
|
+
return typeof size === "number" && Number.isFinite(size) && size > 0 ? Math.floor(size) : 256;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
292
462
|
* Lightweight LRU (Least Recently Used) cache backed by a JavaScript `Map`.
|
|
293
463
|
* Reuses compiled WebCrypto keys across concurrent requests and evicts the
|
|
294
464
|
* oldest entries when capacity is reached.
|
|
@@ -297,7 +467,7 @@ var CryptoKeyLruCache = class {
|
|
|
297
467
|
maxSize;
|
|
298
468
|
map = /* @__PURE__ */ new Map();
|
|
299
469
|
constructor(maxSize = 256) {
|
|
300
|
-
this.maxSize = maxSize;
|
|
470
|
+
this.maxSize = sanitizeCacheCapacity(maxSize);
|
|
301
471
|
}
|
|
302
472
|
get(key) {
|
|
303
473
|
const item = this.map.get(key);
|
|
@@ -384,7 +554,7 @@ function getOrCreateCryptoKey(subtle, normalizedHash, secretBytes, cache = defau
|
|
|
384
554
|
if (cachedPromise) return cachedPromise;
|
|
385
555
|
const keyPromise = importHmacKey(subtle, normalizedHash, secretBytes);
|
|
386
556
|
keyPromise.catch(() => {
|
|
387
|
-
cache.delete(cacheKey);
|
|
557
|
+
if (cache.get(cacheKey) === keyPromise) cache.delete(cacheKey);
|
|
388
558
|
});
|
|
389
559
|
cache.set(cacheKey, keyPromise);
|
|
390
560
|
return keyPromise;
|
|
@@ -513,6 +683,7 @@ function bytesToHex(bytes) {
|
|
|
513
683
|
*/
|
|
514
684
|
function hexToBytes(hex) {
|
|
515
685
|
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
686
|
+
if (!/^[0-9a-fA-F]*$/.test(cleanHex)) throw new TypeError("Invalid hexadecimal string: contains non-hexadecimal characters");
|
|
516
687
|
const normalizedHex = cleanHex.length % 2 === 1 ? `0${cleanHex}` : cleanHex;
|
|
517
688
|
const bytes = new Uint8Array(normalizedHex.length / 2);
|
|
518
689
|
for (let i = 0; i < normalizedHex.length; i += 2) bytes[i / 2] = parseInt(normalizedHex.slice(i, i + 2), 16);
|
|
@@ -546,6 +717,75 @@ function base64ToBytes(base64) {
|
|
|
546
717
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
547
718
|
return bytes;
|
|
548
719
|
}
|
|
720
|
+
// src/providers/common/assertions.ts
|
|
721
|
+
/**
|
|
722
|
+
* Asserts that a header value is a non-empty string.
|
|
723
|
+
*
|
|
724
|
+
* @param header - Candidate header value.
|
|
725
|
+
* @param errorMessage - Error message to throw if header is invalid or missing.
|
|
726
|
+
*/
|
|
727
|
+
function assertNonEmptyHeader(header, errorMessage) {
|
|
728
|
+
if (typeof header !== "string" || !header.trim()) throw new InvalidSignatureHeaderError(errorMessage);
|
|
729
|
+
}
|
|
730
|
+
// src/providers/github/signature.ts
|
|
731
|
+
const GITHUB_SIGNATURE_PREFIX = "sha256=";
|
|
732
|
+
const GITHUB_SIGNATURE_HEX_REGEX = /^[0-9a-fA-F]{64}$/;
|
|
733
|
+
const GITHUB_EVENT_ID_HEADERS = [
|
|
734
|
+
"x-github-delivery",
|
|
735
|
+
"x-webhook-id",
|
|
736
|
+
"webhook-id"
|
|
737
|
+
];
|
|
738
|
+
/**
|
|
739
|
+
* Parses an `X-Hub-Signature-256` header, validating and extracting the hex signature.
|
|
740
|
+
*
|
|
741
|
+
* @param header - Raw `X-Hub-Signature-256` header string (e.g. `sha256=d57c68d1...`).
|
|
742
|
+
* @returns Hexadecimal signature string after the `sha256=` prefix.
|
|
743
|
+
* @throws {InvalidSignatureHeaderError} If header is missing, empty, or lacks a valid sha256= prefix.
|
|
744
|
+
*/
|
|
745
|
+
function parseGithubSignature(header) {
|
|
746
|
+
assertNonEmptyHeader(header, "Missing or empty X-Hub-Signature-256 header");
|
|
747
|
+
const trimmed = header.trim();
|
|
748
|
+
if (!trimmed.toLowerCase().startsWith("sha256=")) throw new InvalidSignatureHeaderError("Invalid X-Hub-Signature-256 header format: expected 'sha256=' prefix");
|
|
749
|
+
const signature = trimmed.slice(7).trim();
|
|
750
|
+
if (!signature || !GITHUB_SIGNATURE_HEX_REGEX.test(signature)) throw new InvalidSignatureHeaderError("Invalid X-Hub-Signature-256 header: missing or malformed 64-character hex signature after prefix");
|
|
751
|
+
return signature;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Resolves the raw signature header string from options, headers record, or request.
|
|
755
|
+
*
|
|
756
|
+
* @param options - GitHub verification options.
|
|
757
|
+
* @returns GitHub signature header string or undefined if not found.
|
|
758
|
+
*/
|
|
759
|
+
function resolveGithubSignatureHeader(options) {
|
|
760
|
+
if (options.signature) return options.signature;
|
|
761
|
+
if (options.headers) return extractHeaderValue$1(options.headers, "x-hub-signature-256");
|
|
762
|
+
if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("x-hub-signature-256") ?? void 0;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Validates preconditions for GitHub webhook verification.
|
|
766
|
+
*
|
|
767
|
+
* @param options - GitHub verification options.
|
|
768
|
+
* @param rawHeader - Resolved header string.
|
|
769
|
+
* @returns Error if preconditions fail, null otherwise.
|
|
770
|
+
*/
|
|
771
|
+
function validateGithubPreconditions(options, rawHeader) {
|
|
772
|
+
if (!options.secret) return new SignatureVerificationError("Missing GitHub webhook signing secret");
|
|
773
|
+
if (!rawHeader) return new InvalidSignatureHeaderError("Missing X-Hub-Signature-256 header");
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Safely parses the raw signature header without uncaught exceptions.
|
|
778
|
+
*
|
|
779
|
+
* @param rawHeader - Raw header string.
|
|
780
|
+
* @returns Extracted signature string or error object.
|
|
781
|
+
*/
|
|
782
|
+
function safeParseGithubHeader(rawHeader) {
|
|
783
|
+
try {
|
|
784
|
+
return { signature: parseGithubSignature(rawHeader) };
|
|
785
|
+
} catch (err) {
|
|
786
|
+
return { error: err instanceof WebhookKitError ? err : new InvalidSignatureHeaderError(String(err)) };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
549
789
|
// src/utils/extract-body.ts
|
|
550
790
|
/**
|
|
551
791
|
* Checks whether an unknown value is a binary BufferSource (ArrayBuffer or ArrayBufferView).
|
|
@@ -641,101 +881,23 @@ async function extractRawBody(body) {
|
|
|
641
881
|
if (isReadableStream(body)) return readReadableStream(body);
|
|
642
882
|
throw new PayloadExtractionError("Unsupported body type. Expected string, Uint8Array, ArrayBuffer, ReadableStream, or Request.");
|
|
643
883
|
}
|
|
644
|
-
// src/providers/
|
|
645
|
-
const GITHUB_SIGNATURE_PREFIX = "sha256=";
|
|
646
|
-
const GITHUB_EVENT_ID_HEADERS = [
|
|
647
|
-
"x-github-delivery",
|
|
648
|
-
"x-webhook-id",
|
|
649
|
-
"webhook-id"
|
|
650
|
-
];
|
|
651
|
-
/**
|
|
652
|
-
* Asserts that a header value is a non-empty string.
|
|
653
|
-
*
|
|
654
|
-
* @param header - Candidate header value.
|
|
655
|
-
*/
|
|
656
|
-
function assertNonEmptyHeader$2(header) {
|
|
657
|
-
if (typeof header !== "string" || !header.trim()) throw new InvalidSignatureHeaderError("Missing or empty X-Hub-Signature-256 header");
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* Parses an `X-Hub-Signature-256` header, validating and extracting the hex signature.
|
|
661
|
-
*
|
|
662
|
-
* @param header - Raw `X-Hub-Signature-256` header string (e.g. `sha256=d57c68d1...`).
|
|
663
|
-
* @returns Hexadecimal signature string after the `sha256=` prefix.
|
|
664
|
-
* @throws {InvalidSignatureHeaderError} If header is missing, empty, or lacks a valid sha256= prefix.
|
|
665
|
-
*/
|
|
666
|
-
function parseGithubSignature(header) {
|
|
667
|
-
assertNonEmptyHeader$2(header);
|
|
668
|
-
const trimmed = header.trim();
|
|
669
|
-
if (!trimmed.toLowerCase().startsWith(GITHUB_SIGNATURE_PREFIX)) throw new InvalidSignatureHeaderError("Invalid X-Hub-Signature-256 header format: expected 'sha256=' prefix");
|
|
670
|
-
const signature = trimmed.slice(7).trim();
|
|
671
|
-
if (!signature) throw new InvalidSignatureHeaderError("Invalid X-Hub-Signature-256 header: missing signature hash after prefix");
|
|
672
|
-
return signature;
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* Case-insensitively extracts a header value from a `Headers` instance or plain record.
|
|
676
|
-
*
|
|
677
|
-
* @param headers - Web standard `Headers` instance or record.
|
|
678
|
-
* @param targetName - Lowercase target header name.
|
|
679
|
-
* @returns Header value string or undefined.
|
|
680
|
-
*/
|
|
681
|
-
function extractHeaderValue$3(headers, targetName) {
|
|
682
|
-
if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
|
|
683
|
-
const record = headers;
|
|
684
|
-
const lowerTarget = targetName.toLowerCase();
|
|
685
|
-
for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
|
|
686
|
-
const value = record[key];
|
|
687
|
-
return Array.isArray(value) ? value[0] : value;
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Resolves the raw signature header string from options, headers record, or request.
|
|
692
|
-
*
|
|
693
|
-
* @param options - GitHub verification options.
|
|
694
|
-
* @returns GitHub signature header string or undefined if not found.
|
|
695
|
-
*/
|
|
696
|
-
function resolveSignatureHeader$2(options) {
|
|
697
|
-
if (options.signature) return options.signature;
|
|
698
|
-
if (options.headers) return extractHeaderValue$3(options.headers, "x-hub-signature-256");
|
|
699
|
-
if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("x-hub-signature-256") ?? void 0;
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Validates preconditions for GitHub webhook verification.
|
|
703
|
-
*
|
|
704
|
-
* @param options - GitHub verification options.
|
|
705
|
-
* @param rawHeader - Resolved header string.
|
|
706
|
-
* @returns Error if preconditions fail, null otherwise.
|
|
707
|
-
*/
|
|
708
|
-
function validatePreconditions$2(options, rawHeader) {
|
|
709
|
-
if (!options.secret) return new SignatureVerificationError("Missing GitHub webhook signing secret");
|
|
710
|
-
if (!rawHeader) return new InvalidSignatureHeaderError("Missing X-Hub-Signature-256 header");
|
|
711
|
-
return null;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* Safely parses the raw signature header without uncaught exceptions.
|
|
715
|
-
*
|
|
716
|
-
* @param rawHeader - Raw header string.
|
|
717
|
-
* @returns Extracted signature string or error object.
|
|
718
|
-
*/
|
|
719
|
-
function safeParseHeader$2(rawHeader) {
|
|
720
|
-
try {
|
|
721
|
-
return { signature: parseGithubSignature(rawHeader) };
|
|
722
|
-
} catch (err) {
|
|
723
|
-
return { error: err instanceof WebhookKitError ? err : new InvalidSignatureHeaderError(String(err)) };
|
|
724
|
-
}
|
|
725
|
-
}
|
|
884
|
+
// src/providers/common/helpers.ts
|
|
726
885
|
/**
|
|
727
886
|
* Constructs a failed WebhookVerificationResult or throws if throwOnError is set.
|
|
728
887
|
*
|
|
729
888
|
* @param rawBody - Raw body string.
|
|
730
889
|
* @param error - WebhookKitError instance.
|
|
731
890
|
* @param throwOnError - Optional flag to throw immediately.
|
|
891
|
+
* @param timestamp - Optional parsed timestamp.
|
|
892
|
+
* @param eventId - Optional webhook event ID.
|
|
732
893
|
* @returns Failed verification result.
|
|
733
894
|
*/
|
|
734
|
-
function failVerification
|
|
895
|
+
function failVerification(rawBody, error, throwOnError, timestamp, eventId) {
|
|
735
896
|
if (throwOnError) throw error;
|
|
736
897
|
return {
|
|
737
898
|
valid: false,
|
|
738
899
|
rawBody,
|
|
900
|
+
timestamp,
|
|
739
901
|
eventId,
|
|
740
902
|
error
|
|
741
903
|
};
|
|
@@ -746,13 +908,14 @@ function failVerification$3(rawBody, error, throwOnError, eventId) {
|
|
|
746
908
|
* @param rawBody - Raw body payload string.
|
|
747
909
|
* @returns Parsed payload or undefined.
|
|
748
910
|
*/
|
|
749
|
-
function tryParseJson
|
|
911
|
+
function tryParseJson(rawBody) {
|
|
750
912
|
try {
|
|
751
913
|
return JSON.parse(rawBody);
|
|
752
914
|
} catch {
|
|
753
915
|
return;
|
|
754
916
|
}
|
|
755
917
|
}
|
|
918
|
+
// src/providers/github/verify.ts
|
|
756
919
|
/**
|
|
757
920
|
* Verifies a GitHub webhook payload signature against `X-Hub-Signature-256`.
|
|
758
921
|
*
|
|
@@ -763,13 +926,13 @@ function tryParseJson$3(rawBody) {
|
|
|
763
926
|
*/
|
|
764
927
|
async function verifyGithubWebhook(options) {
|
|
765
928
|
const rawBody = await extractRawBody(options.body);
|
|
766
|
-
const rawHeader =
|
|
767
|
-
const preconditionError =
|
|
768
|
-
if (preconditionError) return failVerification
|
|
769
|
-
const parsed =
|
|
770
|
-
if ("error" in parsed) return failVerification
|
|
771
|
-
if (!timingSafeEqual((await computeSubtleHmac("SHA-256", options.secret, rawBody, "hex")).toLowerCase(), parsed.signature.toLowerCase())) return failVerification
|
|
772
|
-
const payload = options.parseJson !== false ? tryParseJson
|
|
929
|
+
const rawHeader = resolveGithubSignatureHeader(options);
|
|
930
|
+
const preconditionError = validateGithubPreconditions(options, rawHeader);
|
|
931
|
+
if (preconditionError) return failVerification(rawBody, preconditionError, options.throwOnError);
|
|
932
|
+
const parsed = safeParseGithubHeader(rawHeader);
|
|
933
|
+
if ("error" in parsed) return failVerification(rawBody, parsed.error, options.throwOnError);
|
|
934
|
+
if (!timingSafeEqual((await computeSubtleHmac("SHA-256", options.secret, rawBody, "hex")).toLowerCase(), parsed.signature.toLowerCase())) return failVerification(rawBody, new SignatureVerificationError("GitHub signature verification failed: signature does not match expected HMAC"), options.throwOnError);
|
|
935
|
+
const payload = options.parseJson !== false ? tryParseJson(rawBody) : void 0;
|
|
773
936
|
const eventId = resolveWebhookEventId({
|
|
774
937
|
eventId: options.eventId,
|
|
775
938
|
headers: options.headers,
|
|
@@ -783,7 +946,7 @@ async function verifyGithubWebhook(options) {
|
|
|
783
946
|
storeTtlSeconds: options.storeTtlSeconds,
|
|
784
947
|
toleranceSeconds: options.toleranceSeconds
|
|
785
948
|
});
|
|
786
|
-
if (replayError) return failVerification
|
|
949
|
+
if (replayError) return failVerification(rawBody, replayError, options.throwOnError, void 0, eventId);
|
|
787
950
|
return {
|
|
788
951
|
valid: true,
|
|
789
952
|
rawBody,
|
|
@@ -812,7 +975,7 @@ const githubPlugin = {
|
|
|
812
975
|
name: "github",
|
|
813
976
|
verify: verifyGithubWebhook
|
|
814
977
|
};
|
|
815
|
-
// src/providers/shopify.ts
|
|
978
|
+
// src/providers/shopify/signature.ts
|
|
816
979
|
const BASE64_REGEX = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
817
980
|
const SHOPIFY_EVENT_ID_HEADERS = [
|
|
818
981
|
"x-shopify-webhook-id",
|
|
@@ -820,14 +983,6 @@ const SHOPIFY_EVENT_ID_HEADERS = [
|
|
|
820
983
|
"webhook-id"
|
|
821
984
|
];
|
|
822
985
|
/**
|
|
823
|
-
* Asserts that a header value is a non-empty string.
|
|
824
|
-
*
|
|
825
|
-
* @param header - Candidate header value.
|
|
826
|
-
*/
|
|
827
|
-
function assertNonEmptyHeader$1(header) {
|
|
828
|
-
if (typeof header !== "string" || !header.trim()) throw new InvalidSignatureHeaderError("Missing or empty X-Shopify-Hmac-Sha256 header");
|
|
829
|
-
}
|
|
830
|
-
/**
|
|
831
986
|
* Parses an `X-Shopify-Hmac-Sha256` header, validating and normalizing the Base64 signature.
|
|
832
987
|
*
|
|
833
988
|
* @param header - Raw `X-Shopify-Hmac-Sha256` Base64 header string.
|
|
@@ -835,36 +990,20 @@ function assertNonEmptyHeader$1(header) {
|
|
|
835
990
|
* @throws {InvalidSignatureHeaderError} If header is missing, empty, or not valid Base64.
|
|
836
991
|
*/
|
|
837
992
|
function parseShopifySignature(header) {
|
|
838
|
-
assertNonEmptyHeader
|
|
993
|
+
assertNonEmptyHeader(header, "Missing or empty X-Shopify-Hmac-Sha256 header");
|
|
839
994
|
const trimmed = header.trim();
|
|
840
|
-
if (!BASE64_REGEX.test(trimmed)) throw new InvalidSignatureHeaderError("Invalid X-Shopify-Hmac-Sha256 header format: expected Base64-encoded
|
|
995
|
+
if (trimmed.length !== 44 || !BASE64_REGEX.test(trimmed)) throw new InvalidSignatureHeaderError("Invalid X-Shopify-Hmac-Sha256 header format: expected 44-character Base64-encoded SHA-256 digest");
|
|
841
996
|
return trimmed;
|
|
842
997
|
}
|
|
843
998
|
/**
|
|
844
|
-
* Case-insensitively extracts a header value from a `Headers` instance or plain record.
|
|
845
|
-
*
|
|
846
|
-
* @param headers - Web standard `Headers` instance or record.
|
|
847
|
-
* @param targetName - Lowercase target header name.
|
|
848
|
-
* @returns Header value string or undefined.
|
|
849
|
-
*/
|
|
850
|
-
function extractHeaderValue$2(headers, targetName) {
|
|
851
|
-
if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
|
|
852
|
-
const record = headers;
|
|
853
|
-
const lowerTarget = targetName.toLowerCase();
|
|
854
|
-
for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
|
|
855
|
-
const value = record[key];
|
|
856
|
-
return Array.isArray(value) ? value[0] : value;
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
/**
|
|
860
999
|
* Resolves the raw signature header string from options, headers record, or request.
|
|
861
1000
|
*
|
|
862
1001
|
* @param options - Shopify verification options.
|
|
863
1002
|
* @returns Shopify signature header string or undefined if not found.
|
|
864
1003
|
*/
|
|
865
|
-
function
|
|
1004
|
+
function resolveShopifySignatureHeader(options) {
|
|
866
1005
|
if (options.signature) return options.signature;
|
|
867
|
-
if (options.headers) return extractHeaderValue$
|
|
1006
|
+
if (options.headers) return extractHeaderValue$1(options.headers, "x-shopify-hmac-sha256");
|
|
868
1007
|
if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("x-shopify-hmac-sha256") ?? void 0;
|
|
869
1008
|
}
|
|
870
1009
|
/**
|
|
@@ -874,7 +1013,7 @@ function resolveSignatureHeader$1(options) {
|
|
|
874
1013
|
* @param rawHeader - Resolved header string.
|
|
875
1014
|
* @returns Error if preconditions fail, null otherwise.
|
|
876
1015
|
*/
|
|
877
|
-
function
|
|
1016
|
+
function validateShopifyPreconditions(options, rawHeader) {
|
|
878
1017
|
if (!options.secret) return new SignatureVerificationError("Missing Shopify webhook signing secret");
|
|
879
1018
|
if (!rawHeader) return new InvalidSignatureHeaderError("Missing X-Shopify-Hmac-Sha256 header");
|
|
880
1019
|
return null;
|
|
@@ -885,43 +1024,14 @@ function validatePreconditions$1(options, rawHeader) {
|
|
|
885
1024
|
* @param rawHeader - Raw header string.
|
|
886
1025
|
* @returns Extracted Base64 signature string or error object.
|
|
887
1026
|
*/
|
|
888
|
-
function
|
|
1027
|
+
function safeParseShopifyHeader(rawHeader) {
|
|
889
1028
|
try {
|
|
890
1029
|
return { signature: parseShopifySignature(rawHeader) };
|
|
891
1030
|
} catch (err) {
|
|
892
1031
|
return { error: err instanceof WebhookKitError ? err : new InvalidSignatureHeaderError(String(err)) };
|
|
893
1032
|
}
|
|
894
1033
|
}
|
|
895
|
-
|
|
896
|
-
* Constructs a failed WebhookVerificationResult or throws if throwOnError is set.
|
|
897
|
-
*
|
|
898
|
-
* @param rawBody - Raw body string.
|
|
899
|
-
* @param error - WebhookKitError instance.
|
|
900
|
-
* @param throwOnError - Optional flag to throw immediately.
|
|
901
|
-
* @returns Failed verification result.
|
|
902
|
-
*/
|
|
903
|
-
function failVerification$2(rawBody, error, throwOnError, eventId) {
|
|
904
|
-
if (throwOnError) throw error;
|
|
905
|
-
return {
|
|
906
|
-
valid: false,
|
|
907
|
-
rawBody,
|
|
908
|
-
eventId,
|
|
909
|
-
error
|
|
910
|
-
};
|
|
911
|
-
}
|
|
912
|
-
/**
|
|
913
|
-
* Attempts to parse the raw body as JSON.
|
|
914
|
-
*
|
|
915
|
-
* @param rawBody - Raw body payload string.
|
|
916
|
-
* @returns Parsed payload or undefined.
|
|
917
|
-
*/
|
|
918
|
-
function tryParseJson$2(rawBody) {
|
|
919
|
-
try {
|
|
920
|
-
return JSON.parse(rawBody);
|
|
921
|
-
} catch {
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
1034
|
+
// src/providers/shopify/verify.ts
|
|
925
1035
|
/**
|
|
926
1036
|
* Verifies a Shopify webhook payload signature against `X-Shopify-Hmac-Sha256`.
|
|
927
1037
|
*
|
|
@@ -932,13 +1042,13 @@ function tryParseJson$2(rawBody) {
|
|
|
932
1042
|
*/
|
|
933
1043
|
async function verifyShopifyWebhook(options) {
|
|
934
1044
|
const rawBody = await extractRawBody(options.body);
|
|
935
|
-
const rawHeader =
|
|
936
|
-
const preconditionError =
|
|
937
|
-
if (preconditionError) return failVerification
|
|
938
|
-
const parsed =
|
|
939
|
-
if ("error" in parsed) return failVerification
|
|
940
|
-
if (!timingSafeEqual(await computeSubtleHmac("SHA-256", options.secret, rawBody, "base64"), parsed.signature)) return failVerification
|
|
941
|
-
const payload = options.parseJson !== false ? tryParseJson
|
|
1045
|
+
const rawHeader = resolveShopifySignatureHeader(options);
|
|
1046
|
+
const preconditionError = validateShopifyPreconditions(options, rawHeader);
|
|
1047
|
+
if (preconditionError) return failVerification(rawBody, preconditionError, options.throwOnError);
|
|
1048
|
+
const parsed = safeParseShopifyHeader(rawHeader);
|
|
1049
|
+
if ("error" in parsed) return failVerification(rawBody, parsed.error, options.throwOnError);
|
|
1050
|
+
if (!timingSafeEqual(await computeSubtleHmac("SHA-256", options.secret, rawBody, "base64"), parsed.signature)) return failVerification(rawBody, new SignatureVerificationError("Shopify signature verification failed: signature does not match expected HMAC"), options.throwOnError);
|
|
1051
|
+
const payload = options.parseJson !== false ? tryParseJson(rawBody) : void 0;
|
|
942
1052
|
const eventId = resolveWebhookEventId({
|
|
943
1053
|
eventId: options.eventId,
|
|
944
1054
|
headers: options.headers,
|
|
@@ -952,7 +1062,7 @@ async function verifyShopifyWebhook(options) {
|
|
|
952
1062
|
storeTtlSeconds: options.storeTtlSeconds,
|
|
953
1063
|
toleranceSeconds: options.toleranceSeconds
|
|
954
1064
|
});
|
|
955
|
-
if (replayError) return failVerification
|
|
1065
|
+
if (replayError) return failVerification(rawBody, replayError, options.throwOnError, void 0, eventId);
|
|
956
1066
|
return {
|
|
957
1067
|
valid: true,
|
|
958
1068
|
rawBody,
|
|
@@ -981,40 +1091,13 @@ const shopifyPlugin = {
|
|
|
981
1091
|
name: "shopify",
|
|
982
1092
|
verify: verifyShopifyWebhook
|
|
983
1093
|
};
|
|
984
|
-
// src/providers/stripe.ts
|
|
1094
|
+
// src/providers/stripe/signature.ts
|
|
985
1095
|
const STRIPE_EVENT_ID_HEADERS = [
|
|
986
1096
|
"x-stripe-webhook-id",
|
|
987
1097
|
"x-webhook-id",
|
|
988
1098
|
"webhook-id"
|
|
989
1099
|
];
|
|
990
1100
|
/**
|
|
991
|
-
* Case-insensitively extracts a header value from a `Headers` instance or plain record.
|
|
992
|
-
*
|
|
993
|
-
* @param headers - Web standard `Headers` instance or record.
|
|
994
|
-
* @param targetName - Lowercase target header name.
|
|
995
|
-
* @returns Header value string or undefined.
|
|
996
|
-
*/
|
|
997
|
-
function extractHeaderValue$1(headers, targetName) {
|
|
998
|
-
if (typeof Headers !== "undefined" && headers instanceof Headers) return headers.get(targetName) ?? void 0;
|
|
999
|
-
const record = headers;
|
|
1000
|
-
const lowerTarget = targetName.toLowerCase();
|
|
1001
|
-
for (const key of Object.keys(record)) if (key.toLowerCase() === lowerTarget) {
|
|
1002
|
-
const value = record[key];
|
|
1003
|
-
return Array.isArray(value) ? value[0] : value;
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
/**
|
|
1007
|
-
* Resolves the raw signature header string from options, headers record, or request.
|
|
1008
|
-
*
|
|
1009
|
-
* @param options - Stripe verification options.
|
|
1010
|
-
* @returns Stripe signature header string or undefined if not found.
|
|
1011
|
-
*/
|
|
1012
|
-
function resolveSignatureHeader(options) {
|
|
1013
|
-
if (options.signature) return options.signature;
|
|
1014
|
-
if (options.headers) return extractHeaderValue$1(options.headers, "stripe-signature");
|
|
1015
|
-
if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("stripe-signature") ?? void 0;
|
|
1016
|
-
}
|
|
1017
|
-
/**
|
|
1018
1101
|
* Splits a header tag pair into key and value.
|
|
1019
1102
|
*
|
|
1020
1103
|
* @param item - Header entry (e.g. `t=1492774577` or `v1=...`).
|
|
@@ -1049,14 +1132,6 @@ function processHeaderTag(key, value, state) {
|
|
|
1049
1132
|
}
|
|
1050
1133
|
}
|
|
1051
1134
|
/**
|
|
1052
|
-
* Asserts that a header value is a non-empty string.
|
|
1053
|
-
*
|
|
1054
|
-
* @param header - Candidate header value.
|
|
1055
|
-
*/
|
|
1056
|
-
function assertNonEmptyHeader(header) {
|
|
1057
|
-
if (typeof header !== "string" || !header.trim()) throw new InvalidSignatureHeaderError("Missing or empty Stripe-Signature header");
|
|
1058
|
-
}
|
|
1059
|
-
/**
|
|
1060
1135
|
* Collects timestamp and signature schemes across comma-separated header entries.
|
|
1061
1136
|
*
|
|
1062
1137
|
* @param header - Non-empty header string.
|
|
@@ -1092,7 +1167,7 @@ function validateParsedState(timestamp, signaturesCount) {
|
|
|
1092
1167
|
* @throws {InvalidSignatureHeaderError} If header format is invalid or missing required tags.
|
|
1093
1168
|
*/
|
|
1094
1169
|
function parseStripeSignature(header) {
|
|
1095
|
-
assertNonEmptyHeader(header);
|
|
1170
|
+
assertNonEmptyHeader(header, "Missing or empty Stripe-Signature header");
|
|
1096
1171
|
const state = collectHeaderState(header);
|
|
1097
1172
|
validateParsedState(state.timestamp, state.signatures.length);
|
|
1098
1173
|
return {
|
|
@@ -1102,6 +1177,17 @@ function parseStripeSignature(header) {
|
|
|
1102
1177
|
};
|
|
1103
1178
|
}
|
|
1104
1179
|
/**
|
|
1180
|
+
* Asserts that a timestamp number is a positive finite number.
|
|
1181
|
+
*
|
|
1182
|
+
* @param val - Candidate timestamp value.
|
|
1183
|
+
* @param name - Descriptive parameter name for error messages.
|
|
1184
|
+
* @param eventTimestamp - Event timestamp for error attribution.
|
|
1185
|
+
* @param tolerance - Tolerance window for error attribution.
|
|
1186
|
+
*/
|
|
1187
|
+
function assertPositiveFiniteTimestamp(val, name, eventTimestamp, tolerance) {
|
|
1188
|
+
if (val !== void 0 && (!Number.isFinite(val) || val <= 0)) throw new TimestampToleranceError(eventTimestamp, tolerance, `Invalid ${name}: expected a positive finite number`);
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1105
1191
|
* Validates that a webhook timestamp falls within the allowed tolerance window.
|
|
1106
1192
|
*
|
|
1107
1193
|
* @param timestamp - Unix timestamp in seconds from the signature header.
|
|
@@ -1110,34 +1196,23 @@ function parseStripeSignature(header) {
|
|
|
1110
1196
|
* @throws {TimestampToleranceError} If timestamp falls outside the tolerance window.
|
|
1111
1197
|
*/
|
|
1112
1198
|
function validateTimestampTolerance(timestamp, toleranceSeconds = 300, currentTimestamp) {
|
|
1199
|
+
if (!Number.isFinite(toleranceSeconds)) throw new TimestampToleranceError(timestamp, 0, "Invalid timestamp tolerance value: expected a finite number");
|
|
1113
1200
|
if (toleranceSeconds <= 0) return;
|
|
1201
|
+
assertPositiveFiniteTimestamp(timestamp, "webhook timestamp", timestamp, toleranceSeconds);
|
|
1202
|
+
assertPositiveFiniteTimestamp(currentTimestamp, "current timestamp", timestamp, toleranceSeconds);
|
|
1114
1203
|
const now = currentTimestamp ?? Math.floor(Date.now() / 1e3);
|
|
1115
1204
|
if (Math.abs(now - timestamp) > toleranceSeconds) throw new TimestampToleranceError(timestamp, toleranceSeconds);
|
|
1116
1205
|
}
|
|
1117
1206
|
/**
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
* @param expectedSignature - Computed hex signature.
|
|
1121
|
-
* @param candidates - List of candidate hex signatures from the header.
|
|
1122
|
-
* @returns `true` if any candidate matches the expected signature.
|
|
1123
|
-
*/
|
|
1124
|
-
function matchesAnySignature(expectedSignature, candidates) {
|
|
1125
|
-
let matched = false;
|
|
1126
|
-
for (const candidate of candidates) if (timingSafeEqual(expectedSignature, candidate)) matched = true;
|
|
1127
|
-
return matched;
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* Attempts to parse the raw body as JSON.
|
|
1207
|
+
* Resolves the raw signature header string from options, headers record, or request.
|
|
1131
1208
|
*
|
|
1132
|
-
* @param
|
|
1133
|
-
* @returns
|
|
1209
|
+
* @param options - Stripe verification options.
|
|
1210
|
+
* @returns Stripe signature header string or undefined if not found.
|
|
1134
1211
|
*/
|
|
1135
|
-
function
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1212
|
+
function resolveStripeSignatureHeader(options) {
|
|
1213
|
+
if (options.signature) return options.signature;
|
|
1214
|
+
if (options.headers) return extractHeaderValue$1(options.headers, "stripe-signature");
|
|
1215
|
+
if (typeof Request !== "undefined" && options.body instanceof Request) return options.body.headers.get("stripe-signature") ?? void 0;
|
|
1141
1216
|
}
|
|
1142
1217
|
/**
|
|
1143
1218
|
* Validates preconditions for Stripe webhook verification.
|
|
@@ -1146,7 +1221,7 @@ function tryParseJson$1(rawBody) {
|
|
|
1146
1221
|
* @param rawHeader - Resolved header string.
|
|
1147
1222
|
* @returns Error if preconditions fail, null otherwise.
|
|
1148
1223
|
*/
|
|
1149
|
-
function
|
|
1224
|
+
function validateStripePreconditions(options, rawHeader) {
|
|
1150
1225
|
if (!options.secret) return new SignatureVerificationError("Missing Stripe webhook signing secret");
|
|
1151
1226
|
if (!rawHeader) return new InvalidSignatureHeaderError("Missing Stripe-Signature header");
|
|
1152
1227
|
return null;
|
|
@@ -1157,7 +1232,7 @@ function validatePreconditions(options, rawHeader) {
|
|
|
1157
1232
|
* @param rawHeader - Raw header string.
|
|
1158
1233
|
* @returns Parsed header or error object.
|
|
1159
1234
|
*/
|
|
1160
|
-
function
|
|
1235
|
+
function safeParseStripeHeader(rawHeader) {
|
|
1161
1236
|
try {
|
|
1162
1237
|
return parseStripeSignature(rawHeader);
|
|
1163
1238
|
} catch (err) {
|
|
@@ -1171,7 +1246,7 @@ function safeParseHeader(rawHeader) {
|
|
|
1171
1246
|
* @param options - Stripe verification options.
|
|
1172
1247
|
* @returns TimestampToleranceError if exceeded, null otherwise.
|
|
1173
1248
|
*/
|
|
1174
|
-
function
|
|
1249
|
+
function checkStripeTolerance(timestamp, options) {
|
|
1175
1250
|
const tolerance = options.toleranceSeconds ?? 300;
|
|
1176
1251
|
try {
|
|
1177
1252
|
validateTimestampTolerance(timestamp, tolerance, options.currentTimestamp);
|
|
@@ -1181,24 +1256,19 @@ function checkTolerance(timestamp, options) {
|
|
|
1181
1256
|
}
|
|
1182
1257
|
}
|
|
1183
1258
|
/**
|
|
1184
|
-
*
|
|
1259
|
+
* Compares an expected HMAC hex signature against candidate signatures in constant time.
|
|
1185
1260
|
*
|
|
1186
|
-
* @param
|
|
1187
|
-
* @param
|
|
1188
|
-
* @
|
|
1189
|
-
* @param timestamp - Optional parsed timestamp.
|
|
1190
|
-
* @returns Failed verification result.
|
|
1261
|
+
* @param expectedSignature - Computed hex signature.
|
|
1262
|
+
* @param candidates - List of candidate hex signatures from the header.
|
|
1263
|
+
* @returns `true` if any candidate matches the expected signature.
|
|
1191
1264
|
*/
|
|
1192
|
-
function
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
timestamp,
|
|
1198
|
-
eventId,
|
|
1199
|
-
error
|
|
1200
|
-
};
|
|
1265
|
+
function matchesAnyStripeSignature(expectedSignature, candidates) {
|
|
1266
|
+
let matched = false;
|
|
1267
|
+
const expectedLower = expectedSignature.trim().toLowerCase();
|
|
1268
|
+
for (const candidate of candidates) if (timingSafeEqual(expectedLower, candidate.trim().toLowerCase())) matched = true;
|
|
1269
|
+
return matched;
|
|
1201
1270
|
}
|
|
1271
|
+
// src/providers/stripe/verify.ts
|
|
1202
1272
|
/**
|
|
1203
1273
|
* Verifies a Stripe webhook event signature.
|
|
1204
1274
|
*
|
|
@@ -1210,17 +1280,17 @@ function failVerification$1(rawBody, error, throwOnError, timestamp, eventId) {
|
|
|
1210
1280
|
*/
|
|
1211
1281
|
async function verifyStripeWebhook(options) {
|
|
1212
1282
|
const rawBody = await extractRawBody(options.body);
|
|
1213
|
-
const rawHeader =
|
|
1214
|
-
const preconditionError =
|
|
1215
|
-
if (preconditionError) return failVerification
|
|
1216
|
-
const parsedHeader =
|
|
1217
|
-
if ("error" in parsedHeader) return failVerification
|
|
1283
|
+
const rawHeader = resolveStripeSignatureHeader(options);
|
|
1284
|
+
const preconditionError = validateStripePreconditions(options, rawHeader);
|
|
1285
|
+
if (preconditionError) return failVerification(rawBody, preconditionError, options.throwOnError);
|
|
1286
|
+
const parsedHeader = safeParseStripeHeader(rawHeader);
|
|
1287
|
+
if ("error" in parsedHeader) return failVerification(rawBody, parsedHeader.error, options.throwOnError);
|
|
1218
1288
|
const { timestamp, signatures } = parsedHeader;
|
|
1219
|
-
const toleranceError =
|
|
1220
|
-
if (toleranceError) return failVerification
|
|
1289
|
+
const toleranceError = checkStripeTolerance(timestamp, options);
|
|
1290
|
+
if (toleranceError) return failVerification(rawBody, toleranceError, options.throwOnError, timestamp);
|
|
1221
1291
|
const signedPayload = `${timestamp}.${rawBody}`;
|
|
1222
|
-
if (!
|
|
1223
|
-
const payload = options.parseJson !== false ? tryParseJson
|
|
1292
|
+
if (!matchesAnyStripeSignature(await computeSubtleHmac("SHA-256", options.secret, signedPayload, "hex"), signatures)) return failVerification(rawBody, new SignatureVerificationError("Stripe signature verification failed: signature does not match expected HMAC"), options.throwOnError, timestamp);
|
|
1293
|
+
const payload = options.parseJson !== false ? tryParseJson(rawBody) : void 0;
|
|
1224
1294
|
const eventId = resolveWebhookEventId({
|
|
1225
1295
|
eventId: options.eventId,
|
|
1226
1296
|
headers: options.headers,
|
|
@@ -1234,7 +1304,7 @@ async function verifyStripeWebhook(options) {
|
|
|
1234
1304
|
storeTtlSeconds: options.storeTtlSeconds,
|
|
1235
1305
|
toleranceSeconds: options.toleranceSeconds
|
|
1236
1306
|
});
|
|
1237
|
-
if (replayError) return failVerification
|
|
1307
|
+
if (replayError) return failVerification(rawBody, replayError, options.throwOnError, timestamp, eventId);
|
|
1238
1308
|
return {
|
|
1239
1309
|
valid: true,
|
|
1240
1310
|
rawBody,
|
|
@@ -1348,8 +1418,8 @@ function resolveRawTimestamp(options) {
|
|
|
1348
1418
|
* @returns Positive integer timestamp or `null` if invalid.
|
|
1349
1419
|
*/
|
|
1350
1420
|
function parseTimestampValue(rawTimestamp) {
|
|
1351
|
-
const parsed = typeof rawTimestamp === "number" ? rawTimestamp : parseInt(String(rawTimestamp).trim(), 10);
|
|
1352
|
-
if (!Number.
|
|
1421
|
+
const parsed = typeof rawTimestamp === "number" ? Math.floor(rawTimestamp) : parseInt(String(rawTimestamp).trim(), 10);
|
|
1422
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return null;
|
|
1353
1423
|
return parsed;
|
|
1354
1424
|
}
|
|
1355
1425
|
/**
|
|
@@ -1433,7 +1503,7 @@ async function matchesGenericHmac(options, rawBody, rawSignature, timestamp) {
|
|
|
1433
1503
|
* @param timestamp - Optional parsed timestamp.
|
|
1434
1504
|
* @returns Failed verification result.
|
|
1435
1505
|
*/
|
|
1436
|
-
function failVerification(rawBody, error, throwOnError, timestamp, eventId) {
|
|
1506
|
+
function failVerification$1(rawBody, error, throwOnError, timestamp, eventId) {
|
|
1437
1507
|
if (throwOnError) throw error;
|
|
1438
1508
|
return {
|
|
1439
1509
|
valid: false,
|
|
@@ -1449,7 +1519,7 @@ function failVerification(rawBody, error, throwOnError, timestamp, eventId) {
|
|
|
1449
1519
|
* @param rawBody - Raw body string.
|
|
1450
1520
|
* @returns Parsed JSON value or `undefined`.
|
|
1451
1521
|
*/
|
|
1452
|
-
function tryParseJson(rawBody) {
|
|
1522
|
+
function tryParseJson$1(rawBody) {
|
|
1453
1523
|
try {
|
|
1454
1524
|
return JSON.parse(rawBody);
|
|
1455
1525
|
} catch {
|
|
@@ -1484,7 +1554,7 @@ async function runPluginVerification(plugin, options, enforceStoreAfterPlugin =
|
|
|
1484
1554
|
storeTtlSeconds: options.storeTtlSeconds,
|
|
1485
1555
|
toleranceSeconds: options.toleranceSeconds
|
|
1486
1556
|
});
|
|
1487
|
-
if (replayError) return failVerification(result.rawBody, replayError, options.throwOnError, result.timestamp, eventId);
|
|
1557
|
+
if (replayError) return failVerification$1(result.rawBody, replayError, options.throwOnError, result.timestamp, eventId);
|
|
1488
1558
|
return {
|
|
1489
1559
|
...result,
|
|
1490
1560
|
eventId
|
|
@@ -1501,7 +1571,7 @@ async function delegateToProvider(options) {
|
|
|
1501
1571
|
if (typeof provider === "object") return runPluginVerification(provider, options, true);
|
|
1502
1572
|
const normalizedName = String(provider).toLowerCase().trim();
|
|
1503
1573
|
const builtin = PROVIDER_REGISTRY[normalizedName];
|
|
1504
|
-
if (!builtin) return failVerification(await extractRawBody(options.body), new WebhookKitError(`Unsupported webhook provider: "${String(provider)}"`, "UNSUPPORTED_PROVIDER"), options.throwOnError);
|
|
1574
|
+
if (!builtin) return failVerification$1(await extractRawBody(options.body), new WebhookKitError(`Unsupported webhook provider: "${String(provider)}"`, "UNSUPPORTED_PROVIDER"), options.throwOnError);
|
|
1505
1575
|
return runPluginVerification(builtin, options);
|
|
1506
1576
|
}
|
|
1507
1577
|
/**
|
|
@@ -1513,11 +1583,11 @@ async function delegateToProvider(options) {
|
|
|
1513
1583
|
async function verifyGenericWebhook(options) {
|
|
1514
1584
|
const rawBody = await extractRawBody(options.body);
|
|
1515
1585
|
const rawSignature = resolveGenericSignature(options);
|
|
1516
|
-
if (!hasSecret(options.secret) || !rawSignature) return failVerification(rawBody, new SignatureVerificationError("Missing signature or secret"), options.throwOnError);
|
|
1586
|
+
if (!hasSecret(options.secret) || !rawSignature) return failVerification$1(rawBody, new SignatureVerificationError("Missing signature or secret"), options.throwOnError);
|
|
1517
1587
|
const tsResult = verifyGenericTimestamp(options);
|
|
1518
|
-
if (tsResult.error) return failVerification(rawBody, tsResult.error, options.throwOnError);
|
|
1519
|
-
if (!await matchesGenericHmac(options, rawBody, rawSignature, tsResult.timestamp)) return failVerification(rawBody, new SignatureVerificationError(), options.throwOnError, tsResult.timestamp);
|
|
1520
|
-
const payload = options.parseJson !== false ? tryParseJson(rawBody) : void 0;
|
|
1588
|
+
if (tsResult.error) return failVerification$1(rawBody, tsResult.error, options.throwOnError);
|
|
1589
|
+
if (!await matchesGenericHmac(options, rawBody, rawSignature, tsResult.timestamp)) return failVerification$1(rawBody, new SignatureVerificationError(), options.throwOnError, tsResult.timestamp);
|
|
1590
|
+
const payload = options.parseJson !== false ? tryParseJson$1(rawBody) : void 0;
|
|
1521
1591
|
const eventId = resolveWebhookEventId({
|
|
1522
1592
|
eventId: options.eventId,
|
|
1523
1593
|
headers: options.headers,
|
|
@@ -1531,7 +1601,7 @@ async function verifyGenericWebhook(options) {
|
|
|
1531
1601
|
storeTtlSeconds: options.storeTtlSeconds,
|
|
1532
1602
|
toleranceSeconds: options.toleranceSeconds
|
|
1533
1603
|
});
|
|
1534
|
-
if (replayError) return failVerification(rawBody, replayError, options.throwOnError, tsResult.timestamp, eventId);
|
|
1604
|
+
if (replayError) return failVerification$1(rawBody, replayError, options.throwOnError, tsResult.timestamp, eventId);
|
|
1535
1605
|
return {
|
|
1536
1606
|
valid: true,
|
|
1537
1607
|
rawBody,
|
|
@@ -1552,7 +1622,7 @@ async function assertWebhookSignature(options) {
|
|
|
1552
1622
|
if (!result.valid) throw result.error;
|
|
1553
1623
|
return result.payload;
|
|
1554
1624
|
}
|
|
1555
|
-
// src/handler.ts
|
|
1625
|
+
// src/handler/handler.ts
|
|
1556
1626
|
const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
|
|
1557
1627
|
const DEFAULT_SUCCESS_BODY = JSON.stringify({ received: true });
|
|
1558
1628
|
/**
|
|
@@ -1733,6 +1803,6 @@ function createWebhookHandler(options) {
|
|
|
1733
1803
|
}));
|
|
1734
1804
|
};
|
|
1735
1805
|
}
|
|
1736
|
-
export { CryptoKeyLruCache, InMemoryReplayStore, InvalidSignatureHeaderError, PROVIDER_REGISTRY, PayloadExtractionError, ReplayAttackError, SignatureVerificationError, TimestampToleranceError, WebhookKitError, assertGithubWebhook, assertShopifyWebhook, assertStripeWebhook, assertWebhookSignature, base64ToBytes, bytesToBase64, bytesToHex, checkAndRecordReplay, clearCryptoKeyCache, computeSubtleHmac, createInMemoryReplayStore, createWebhookHandler, extractRawBody, getCachedCryptoKey, getCryptoKeyCacheSize, githubPlugin, hexToBytes, parseGithubSignature, parseShopifySignature, parseStripeSignature, resolveWebhookEventId, shopifyPlugin, stripePlugin, timingSafeEqual, validateTimestampTolerance, verifyGithubWebhook, verifyShopifyWebhook, verifyStripeWebhook, verifyWebhookSignature };
|
|
1806
|
+
export { BASE64_REGEX, CryptoKeyLruCache, DEFAULT_REPLAY_STORE_MAX_SIZE, DEFAULT_REPLAY_STORE_TTL_SECONDS, GENERIC_EVENT_ID_HEADERS, GITHUB_EVENT_ID_HEADERS, GITHUB_SIGNATURE_HEX_REGEX, GITHUB_SIGNATURE_PREFIX, InMemoryReplayStore, InvalidSignatureHeaderError, PROVIDER_REGISTRY, PayloadExtractionError, ReplayAttackError, SHOPIFY_EVENT_ID_HEADERS, STRIPE_EVENT_ID_HEADERS, SignatureVerificationError, TimestampToleranceError, WebhookKitError, assertGithubWebhook, assertNonEmptyHeader, assertShopifyWebhook, assertStripeWebhook, assertWebhookSignature, base64ToBytes, bytesToBase64, bytesToHex, checkAndRecordReplay, checkStripeTolerance, clearCryptoKeyCache, computeSubtleHmac, createInMemoryReplayStore, createWebhookHandler, extractRawBody, failVerification, getCachedCryptoKey, getCryptoKeyCacheSize, githubPlugin, hexToBytes, matchesAnyStripeSignature, parseGithubSignature, parseShopifySignature, parseStripeSignature, resolveGithubSignatureHeader, resolveShopifySignatureHeader, resolveStripeSignatureHeader, resolveWebhookEventId, safeParseGithubHeader, safeParseShopifyHeader, safeParseStripeHeader, shopifyPlugin, stripePlugin, timingSafeEqual, tryParseJson, validateGithubPreconditions, validateShopifyPreconditions, validateStripePreconditions, validateTimestampTolerance, verifyGithubWebhook, verifyShopifyWebhook, verifyStripeWebhook, verifyWebhookSignature };
|
|
1737
1807
|
|
|
1738
1808
|
//# sourceMappingURL=index.mjs.map
|