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