@webhooks-cc/sdk 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,15 +31,19 @@ __export(src_exports, {
31
31
  WebhooksCCError: () => WebhooksCCError,
32
32
  diffRequests: () => diffRequests,
33
33
  extractJsonField: () => extractJsonField,
34
+ isClerkWebhook: () => isClerkWebhook,
34
35
  isDiscordWebhook: () => isDiscordWebhook,
35
36
  isGitHubWebhook: () => isGitHubWebhook,
37
+ isGitLabWebhook: () => isGitLabWebhook,
36
38
  isLinearWebhook: () => isLinearWebhook,
37
39
  isPaddleWebhook: () => isPaddleWebhook,
40
+ isSendGridWebhook: () => isSendGridWebhook,
38
41
  isShopifyWebhook: () => isShopifyWebhook,
39
42
  isSlackWebhook: () => isSlackWebhook,
40
43
  isStandardWebhook: () => isStandardWebhook,
41
44
  isStripeWebhook: () => isStripeWebhook,
42
45
  isTwilioWebhook: () => isTwilioWebhook,
46
+ isVercelWebhook: () => isVercelWebhook,
43
47
  matchAll: () => matchAll,
44
48
  matchAny: () => matchAny,
45
49
  matchBodyPath: () => matchBodyPath,
@@ -55,8 +59,10 @@ __export(src_exports, {
55
59
  parseFormBody: () => parseFormBody,
56
60
  parseJsonBody: () => parseJsonBody,
57
61
  parseSSE: () => parseSSE,
62
+ verifyClerkSignature: () => verifyClerkSignature,
58
63
  verifyDiscordSignature: () => verifyDiscordSignature,
59
64
  verifyGitHubSignature: () => verifyGitHubSignature,
65
+ verifyGitLabSignature: () => verifyGitLabSignature,
60
66
  verifyLinearSignature: () => verifyLinearSignature,
61
67
  verifyPaddleSignature: () => verifyPaddleSignature,
62
68
  verifyShopifySignature: () => verifyShopifySignature,
@@ -64,7 +70,8 @@ __export(src_exports, {
64
70
  verifySlackSignature: () => verifySlackSignature,
65
71
  verifyStandardWebhookSignature: () => verifyStandardWebhookSignature,
66
72
  verifyStripeSignature: () => verifyStripeSignature,
67
- verifyTwilioSignature: () => verifyTwilioSignature
73
+ verifyTwilioSignature: () => verifyTwilioSignature,
74
+ verifyVercelSignature: () => verifyVercelSignature
68
75
  });
69
76
  module.exports = __toCommonJS(src_exports);
70
77
 
@@ -225,7 +232,12 @@ var DEFAULT_TEMPLATE_BY_PROVIDER = {
225
232
  twilio: "messaging.inbound",
226
233
  slack: "event_callback",
227
234
  paddle: "transaction.completed",
228
- linear: "issue.create"
235
+ linear: "issue.create",
236
+ sendgrid: "delivered",
237
+ clerk: "user.created",
238
+ discord: "interaction_create",
239
+ vercel: "deployment.created",
240
+ gitlab: "push"
229
241
  };
230
242
  var PROVIDER_TEMPLATES = {
231
243
  stripe: ["payment_intent.succeeded", "checkout.session.completed", "invoice.paid"],
@@ -234,7 +246,12 @@ var PROVIDER_TEMPLATES = {
234
246
  twilio: ["messaging.inbound", "messaging.status_callback", "voice.incoming_call"],
235
247
  slack: ["event_callback", "slash_command", "url_verification"],
236
248
  paddle: ["transaction.completed", "subscription.created", "subscription.updated"],
237
- linear: ["issue.create", "issue.update", "comment.create"]
249
+ linear: ["issue.create", "issue.update", "comment.create"],
250
+ sendgrid: ["delivered", "open", "bounce", "spam_report"],
251
+ clerk: ["user.created", "user.updated", "user.deleted", "session.created"],
252
+ discord: ["interaction_create", "message_component", "ping"],
253
+ vercel: ["deployment.created", "deployment.succeeded", "deployment.error"],
254
+ gitlab: ["push", "merge_request"]
238
255
  };
239
256
  var TEMPLATE_PROVIDERS = [
240
257
  "stripe",
@@ -244,6 +261,11 @@ var TEMPLATE_PROVIDERS = [
244
261
  "slack",
245
262
  "paddle",
246
263
  "linear",
264
+ "sendgrid",
265
+ "clerk",
266
+ "discord",
267
+ "vercel",
268
+ "gitlab",
247
269
  "standard-webhooks"
248
270
  ];
249
271
  var TEMPLATE_METADATA = Object.freeze({
@@ -303,6 +325,42 @@ var TEMPLATE_METADATA = Object.freeze({
303
325
  signatureHeader: "linear-signature",
304
326
  signatureAlgorithm: "hmac-sha256"
305
327
  }),
328
+ sendgrid: Object.freeze({
329
+ provider: "sendgrid",
330
+ templates: Object.freeze([...PROVIDER_TEMPLATES.sendgrid]),
331
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.sendgrid,
332
+ secretRequired: false
333
+ }),
334
+ clerk: Object.freeze({
335
+ provider: "clerk",
336
+ templates: Object.freeze([...PROVIDER_TEMPLATES.clerk]),
337
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.clerk,
338
+ secretRequired: true,
339
+ signatureHeader: "webhook-signature",
340
+ signatureAlgorithm: "hmac-sha256"
341
+ }),
342
+ discord: Object.freeze({
343
+ provider: "discord",
344
+ templates: Object.freeze([...PROVIDER_TEMPLATES.discord]),
345
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.discord,
346
+ secretRequired: false
347
+ }),
348
+ vercel: Object.freeze({
349
+ provider: "vercel",
350
+ templates: Object.freeze([...PROVIDER_TEMPLATES.vercel]),
351
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.vercel,
352
+ secretRequired: true,
353
+ signatureHeader: "x-vercel-signature",
354
+ signatureAlgorithm: "hmac-sha1"
355
+ }),
356
+ gitlab: Object.freeze({
357
+ provider: "gitlab",
358
+ templates: Object.freeze([...PROVIDER_TEMPLATES.gitlab]),
359
+ defaultTemplate: DEFAULT_TEMPLATE_BY_PROVIDER.gitlab,
360
+ secretRequired: true,
361
+ signatureHeader: "x-gitlab-token",
362
+ signatureAlgorithm: "token"
363
+ }),
306
364
  "standard-webhooks": Object.freeze({
307
365
  provider: "standard-webhooks",
308
366
  templates: Object.freeze([]),
@@ -888,6 +946,228 @@ function buildTemplatePayload(provider, template, event, now, bodyOverride) {
888
946
  }
889
947
  };
890
948
  }
949
+ if (provider === "clerk") {
950
+ const userId = `user_${randomHex(24)}`;
951
+ const payloadByTemplate = {
952
+ "user.created": {
953
+ data: {
954
+ id: userId,
955
+ object: "user",
956
+ email_addresses: [
957
+ {
958
+ id: `idn_${randomHex(24)}`,
959
+ email_address: "user@example.com",
960
+ verification: { status: "verified", strategy: "email_code" }
961
+ }
962
+ ],
963
+ first_name: "Jane",
964
+ last_name: "Doe",
965
+ created_at: nowSec * 1e3,
966
+ updated_at: nowSec * 1e3
967
+ },
968
+ object: "event",
969
+ type: "user.created",
970
+ timestamp: nowSec * 1e3
971
+ },
972
+ "user.updated": {
973
+ data: {
974
+ id: userId,
975
+ object: "user",
976
+ email_addresses: [
977
+ {
978
+ id: `idn_${randomHex(24)}`,
979
+ email_address: "user@example.com",
980
+ verification: { status: "verified", strategy: "email_code" }
981
+ }
982
+ ],
983
+ first_name: "Jane",
984
+ last_name: "Smith",
985
+ created_at: nowSec * 1e3,
986
+ updated_at: nowSec * 1e3
987
+ },
988
+ object: "event",
989
+ type: "user.updated",
990
+ timestamp: nowSec * 1e3
991
+ },
992
+ "user.deleted": {
993
+ data: {
994
+ id: userId,
995
+ object: "user",
996
+ deleted: true
997
+ },
998
+ object: "event",
999
+ type: "user.deleted",
1000
+ timestamp: nowSec * 1e3
1001
+ },
1002
+ "session.created": {
1003
+ data: {
1004
+ id: `sess_${randomHex(24)}`,
1005
+ object: "session",
1006
+ user_id: userId,
1007
+ status: "active",
1008
+ created_at: nowSec * 1e3,
1009
+ updated_at: nowSec * 1e3,
1010
+ expire_at: (nowSec + 86400) * 1e3
1011
+ },
1012
+ object: "event",
1013
+ type: "session.created",
1014
+ timestamp: nowSec * 1e3
1015
+ }
1016
+ };
1017
+ const payload = bodyOverride ?? payloadByTemplate[template];
1018
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
1019
+ return {
1020
+ body,
1021
+ contentType: "application/json",
1022
+ headers: {
1023
+ "user-agent": "Svix-Webhooks/1.0"
1024
+ }
1025
+ };
1026
+ }
1027
+ if (provider === "vercel") {
1028
+ const deploymentId = `dpl_${randomHex(20)}`;
1029
+ const projectId = `prj_${randomHex(20)}`;
1030
+ const teamId = `team_${randomHex(20)}`;
1031
+ const payloadByTemplate = {
1032
+ "deployment.created": {
1033
+ id: randomUuid(),
1034
+ type: "deployment.created",
1035
+ createdAt: nowSec * 1e3,
1036
+ payload: {
1037
+ deployment: {
1038
+ id: deploymentId,
1039
+ name: "webhooks-cc-web",
1040
+ url: `webhooks-cc-web-${randomHex(8)}.vercel.app`,
1041
+ meta: {
1042
+ githubCommitRef: "main",
1043
+ githubCommitSha: randomHex(40),
1044
+ githubCommitMessage: "Update webhook templates"
1045
+ }
1046
+ },
1047
+ project: { id: projectId, name: "webhooks-cc-web" },
1048
+ team: { id: teamId, name: "webhooks-cc" }
1049
+ }
1050
+ },
1051
+ "deployment.succeeded": {
1052
+ id: randomUuid(),
1053
+ type: "deployment.succeeded",
1054
+ createdAt: nowSec * 1e3,
1055
+ payload: {
1056
+ deployment: {
1057
+ id: deploymentId,
1058
+ name: "webhooks-cc-web",
1059
+ url: `webhooks-cc-web-${randomHex(8)}.vercel.app`,
1060
+ readyState: "READY"
1061
+ },
1062
+ project: { id: projectId, name: "webhooks-cc-web" },
1063
+ team: { id: teamId, name: "webhooks-cc" }
1064
+ }
1065
+ },
1066
+ "deployment.error": {
1067
+ id: randomUuid(),
1068
+ type: "deployment.error",
1069
+ createdAt: nowSec * 1e3,
1070
+ payload: {
1071
+ deployment: {
1072
+ id: deploymentId,
1073
+ name: "webhooks-cc-web",
1074
+ url: `webhooks-cc-web-${randomHex(8)}.vercel.app`,
1075
+ readyState: "ERROR",
1076
+ errorMessage: "Build failed: exit code 1"
1077
+ },
1078
+ project: { id: projectId, name: "webhooks-cc-web" },
1079
+ team: { id: teamId, name: "webhooks-cc" }
1080
+ }
1081
+ }
1082
+ };
1083
+ const payload = bodyOverride ?? payloadByTemplate[template];
1084
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
1085
+ return {
1086
+ body,
1087
+ contentType: "application/json",
1088
+ headers: {
1089
+ "user-agent": "Vercel/1.0"
1090
+ }
1091
+ };
1092
+ }
1093
+ if (provider === "gitlab") {
1094
+ const projectId = Number(randomDigits(7));
1095
+ const payloadByTemplate = {
1096
+ push: {
1097
+ object_kind: "push",
1098
+ event_name: "push",
1099
+ before: randomHex(40),
1100
+ after: randomHex(40),
1101
+ ref: "refs/heads/main",
1102
+ checkout_sha: randomHex(40),
1103
+ user_id: Number(randomDigits(5)),
1104
+ user_name: "webhooks-cc-bot",
1105
+ user_email: "bot@webhooks.cc",
1106
+ project_id: projectId,
1107
+ project: {
1108
+ id: projectId,
1109
+ name: "demo-repo",
1110
+ web_url: "https://gitlab.com/webhooks-cc/demo-repo",
1111
+ namespace: "webhooks-cc",
1112
+ default_branch: "main"
1113
+ },
1114
+ commits: [
1115
+ {
1116
+ id: randomHex(40),
1117
+ message: "Update webhook integration tests",
1118
+ title: "Update webhook integration tests",
1119
+ timestamp: nowIso,
1120
+ url: `https://gitlab.com/webhooks-cc/demo-repo/-/commit/${randomHex(40)}`,
1121
+ author: { name: "webhooks-cc-bot", email: "bot@webhooks.cc" },
1122
+ added: [],
1123
+ modified: ["src/webhooks.ts"],
1124
+ removed: []
1125
+ }
1126
+ ],
1127
+ total_commits_count: 1
1128
+ },
1129
+ merge_request: {
1130
+ object_kind: "merge_request",
1131
+ event_type: "merge_request",
1132
+ user: {
1133
+ id: Number(randomDigits(5)),
1134
+ name: "webhooks-cc-bot",
1135
+ username: "webhooks-cc-bot",
1136
+ email: "bot@webhooks.cc"
1137
+ },
1138
+ project: {
1139
+ id: projectId,
1140
+ name: "demo-repo",
1141
+ web_url: "https://gitlab.com/webhooks-cc/demo-repo",
1142
+ namespace: "webhooks-cc",
1143
+ default_branch: "main"
1144
+ },
1145
+ object_attributes: {
1146
+ id: Number(randomDigits(7)),
1147
+ iid: 42,
1148
+ title: "Add webhook retry logic",
1149
+ description: "This MR improves retry handling for inbound webhooks.",
1150
+ state: "opened",
1151
+ action: "open",
1152
+ source_branch: "feature/webhook-retries",
1153
+ target_branch: "main",
1154
+ created_at: nowIso,
1155
+ updated_at: nowIso,
1156
+ url: `https://gitlab.com/webhooks-cc/demo-repo/-/merge_requests/42`
1157
+ }
1158
+ }
1159
+ };
1160
+ const payload = bodyOverride ?? payloadByTemplate[template];
1161
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
1162
+ return {
1163
+ body,
1164
+ contentType: "application/json",
1165
+ headers: {
1166
+ "x-gitlab-event": template === "merge_request" ? "Merge Request Hook" : "Push Hook",
1167
+ "user-agent": "GitLab/1.0"
1168
+ }
1169
+ };
1170
+ }
891
1171
  throw new Error(`Unsupported provider: ${provider}`);
892
1172
  }
893
1173
  const defaultTwilioParamsByTemplate = {
@@ -1073,6 +1353,142 @@ async function buildTemplateSendOptions(endpointUrl, options) {
1073
1353
  body
1074
1354
  };
1075
1355
  }
1356
+ if (options.provider === "sendgrid") {
1357
+ const method2 = (options.method ?? "POST").toUpperCase();
1358
+ const supported = PROVIDER_TEMPLATES.sendgrid;
1359
+ const template2 = options.template ?? DEFAULT_TEMPLATE_BY_PROVIDER.sendgrid;
1360
+ if (!supported.some((item) => item === template2)) {
1361
+ throw new Error(
1362
+ `Unsupported template "${template2}" for provider "sendgrid". Supported templates: ${supported.join(", ")}`
1363
+ );
1364
+ }
1365
+ const nowSec = Math.floor(Date.now() / 1e3);
1366
+ const payloadByTemplate = {
1367
+ delivered: [
1368
+ {
1369
+ sg_event_id: randomHex(22),
1370
+ sg_message_id: `${randomHex(20)}.${randomDigits(4)}`,
1371
+ email: "recipient@example.com",
1372
+ timestamp: nowSec,
1373
+ event: "delivered",
1374
+ smtp_id: `<${randomHex(20)}@example.com>`,
1375
+ ip: "168.1.1.1",
1376
+ response: "250 OK",
1377
+ category: ["webhooks-cc-test"]
1378
+ }
1379
+ ],
1380
+ open: [
1381
+ {
1382
+ sg_event_id: randomHex(22),
1383
+ sg_message_id: `${randomHex(20)}.${randomDigits(4)}`,
1384
+ email: "recipient@example.com",
1385
+ timestamp: nowSec,
1386
+ event: "open",
1387
+ ip: "72.14.199.28",
1388
+ useragent: "Mozilla/5.0",
1389
+ category: ["webhooks-cc-test"]
1390
+ }
1391
+ ],
1392
+ bounce: [
1393
+ {
1394
+ sg_event_id: randomHex(22),
1395
+ sg_message_id: `${randomHex(20)}.${randomDigits(4)}`,
1396
+ email: "bounced@example.com",
1397
+ timestamp: nowSec,
1398
+ event: "bounce",
1399
+ type: "bounce",
1400
+ status: "5.1.1",
1401
+ reason: "550 5.1.1 The email account does not exist.",
1402
+ ip: "168.1.1.1",
1403
+ category: ["webhooks-cc-test"]
1404
+ }
1405
+ ],
1406
+ spam_report: [
1407
+ {
1408
+ sg_event_id: randomHex(22),
1409
+ sg_message_id: `${randomHex(20)}.${randomDigits(4)}`,
1410
+ email: "complainant@example.com",
1411
+ timestamp: nowSec,
1412
+ event: "spamreport",
1413
+ category: ["webhooks-cc-test"]
1414
+ }
1415
+ ]
1416
+ };
1417
+ const payload = options.body ?? payloadByTemplate[template2];
1418
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
1419
+ return {
1420
+ method: method2,
1421
+ headers: {
1422
+ "content-type": "application/json",
1423
+ "user-agent": "SendGrid/1.0",
1424
+ "x-webhooks-cc-template-provider": "sendgrid",
1425
+ "x-webhooks-cc-template-template": template2,
1426
+ "x-webhooks-cc-template-event": template2,
1427
+ ...options.headers ?? {}
1428
+ },
1429
+ body
1430
+ };
1431
+ }
1432
+ if (options.provider === "discord") {
1433
+ const method2 = (options.method ?? "POST").toUpperCase();
1434
+ const supported = PROVIDER_TEMPLATES.discord;
1435
+ const template2 = options.template ?? DEFAULT_TEMPLATE_BY_PROVIDER.discord;
1436
+ if (!supported.some((item) => item === template2)) {
1437
+ throw new Error(
1438
+ `Unsupported template "${template2}" for provider "discord". Supported templates: ${supported.join(", ")}`
1439
+ );
1440
+ }
1441
+ const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
1442
+ const payloadByTemplate = {
1443
+ interaction_create: {
1444
+ id: randomHex(18),
1445
+ application_id: randomHex(18),
1446
+ type: 2,
1447
+ data: {
1448
+ id: randomHex(18),
1449
+ name: "webhook-test",
1450
+ type: 1
1451
+ },
1452
+ guild_id: randomHex(18),
1453
+ channel_id: randomHex(18),
1454
+ token: randomHex(40),
1455
+ version: 1
1456
+ },
1457
+ message_component: {
1458
+ id: randomHex(18),
1459
+ application_id: randomHex(18),
1460
+ type: 3,
1461
+ data: {
1462
+ custom_id: "click_me",
1463
+ component_type: 2
1464
+ },
1465
+ guild_id: randomHex(18),
1466
+ channel_id: randomHex(18),
1467
+ token: randomHex(40),
1468
+ version: 1
1469
+ },
1470
+ ping: {
1471
+ id: randomHex(18),
1472
+ application_id: randomHex(18),
1473
+ type: 1,
1474
+ version: 1
1475
+ }
1476
+ };
1477
+ const payload = options.body ?? payloadByTemplate[template2];
1478
+ const body = typeof payload === "string" ? payload : JSON.stringify(payload);
1479
+ return {
1480
+ method: method2,
1481
+ headers: {
1482
+ "content-type": "application/json",
1483
+ "x-signature-timestamp": String(timestamp),
1484
+ "x-webhooks-cc-template-provider": "discord",
1485
+ "x-webhooks-cc-template-template": template2,
1486
+ "x-webhooks-cc-template-event": template2,
1487
+ ...options.headers ?? {}
1488
+ },
1489
+ body
1490
+ };
1491
+ }
1076
1492
  const provider = options.provider;
1077
1493
  const method = (options.method ?? "POST").toUpperCase();
1078
1494
  const template = ensureTemplate(provider, options.template);
@@ -1122,6 +1538,29 @@ async function buildTemplateSendOptions(endpointUrl, options) {
1122
1538
  const signature = await hmacSign("SHA-256", options.secret, built.body);
1123
1539
  headers["linear-signature"] = `sha256=${toHex(signature)}`;
1124
1540
  }
1541
+ if (provider === "clerk") {
1542
+ const msgId = `msg_${randomHex(16)}`;
1543
+ const timestamp = options.timestamp ?? Math.floor(Date.now() / 1e3);
1544
+ const signingInput = `${msgId}.${timestamp}.${built.body}`;
1545
+ const secretBytes = decodeStandardWebhookSecret(options.secret);
1546
+ const signature = await hmacSignRaw("SHA-256", secretBytes, signingInput);
1547
+ const sig = `v1,${toBase64(signature)}`;
1548
+ headers["webhook-id"] = msgId;
1549
+ headers["webhook-timestamp"] = String(timestamp);
1550
+ headers["webhook-signature"] = sig;
1551
+ headers["svix-id"] = msgId;
1552
+ headers["svix-timestamp"] = String(timestamp);
1553
+ headers["svix-signature"] = sig;
1554
+ }
1555
+ if (provider === "vercel") {
1556
+ const signature = await hmacSign("SHA-1", options.secret, built.body);
1557
+ headers["x-vercel-signature"] = toHex(signature);
1558
+ }
1559
+ if (provider === "gitlab") {
1560
+ headers["x-gitlab-token"] = options.secret;
1561
+ const gitlabEvent = template === "merge_request" ? "Merge Request Hook" : "Push Hook";
1562
+ headers["x-gitlab-event"] = gitlabEvent;
1563
+ }
1125
1564
  return {
1126
1565
  method,
1127
1566
  headers: {
@@ -1467,6 +1906,32 @@ async function verifyLinearSignature(body, signatureHeader, secret) {
1467
1906
  const expected = toHex(await hmacSign("SHA-256", secret, normalizeBody(body))).toLowerCase();
1468
1907
  return timingSafeEqual(match[1].toLowerCase(), expected);
1469
1908
  }
1909
+ async function verifyVercelSignature(body, signatureHeader, secret) {
1910
+ requireSecret(secret, "verifyVercelSignature");
1911
+ if (!signatureHeader) {
1912
+ return false;
1913
+ }
1914
+ const expected = toHex(await hmacSign("SHA-1", secret, normalizeBody(body))).toLowerCase();
1915
+ return timingSafeEqual(signatureHeader.trim().toLowerCase(), expected);
1916
+ }
1917
+ async function verifyGitLabSignature(_body, tokenHeader, secret) {
1918
+ requireSecret(secret, "verifyGitLabSignature");
1919
+ if (!tokenHeader) {
1920
+ return false;
1921
+ }
1922
+ return timingSafeEqual(tokenHeader, secret);
1923
+ }
1924
+ async function verifyClerkSignature(body, headers, secret) {
1925
+ const normalized = { ...headers };
1926
+ const svixId = getHeader(headers, "svix-id");
1927
+ const svixTs = getHeader(headers, "svix-timestamp");
1928
+ const svixSig = getHeader(headers, "svix-signature");
1929
+ if (svixId && !getHeader(headers, "webhook-id")) normalized["webhook-id"] = svixId;
1930
+ if (svixTs && !getHeader(headers, "webhook-timestamp")) normalized["webhook-timestamp"] = svixTs;
1931
+ if (svixSig && !getHeader(headers, "webhook-signature"))
1932
+ normalized["webhook-signature"] = svixSig;
1933
+ return verifyStandardWebhookSignature(body, normalized, secret);
1934
+ }
1470
1935
  async function verifyDiscordSignature(body, headers, publicKey) {
1471
1936
  if (!publicKey || typeof publicKey !== "string") {
1472
1937
  throw new Error("verifyDiscordSignature requires a non-empty public key");
@@ -1573,6 +2038,28 @@ async function verifySignature(request, options) {
1573
2038
  options.secret
1574
2039
  );
1575
2040
  }
2041
+ if (options.provider === "clerk") {
2042
+ valid = await verifyClerkSignature(request.body, request.headers, options.secret);
2043
+ }
2044
+ if (options.provider === "vercel") {
2045
+ valid = await verifyVercelSignature(
2046
+ request.body,
2047
+ getHeader(request.headers, "x-vercel-signature"),
2048
+ options.secret
2049
+ );
2050
+ }
2051
+ if (options.provider === "gitlab") {
2052
+ valid = await verifyGitLabSignature(
2053
+ request.body,
2054
+ getHeader(request.headers, "x-gitlab-token"),
2055
+ options.secret
2056
+ );
2057
+ }
2058
+ if (options.provider === "sendgrid") {
2059
+ throw new Error(
2060
+ "SendGrid does not use signature verification. SendGrid webhooks are verified via IP allowlisting."
2061
+ );
2062
+ }
1576
2063
  if (options.provider === "discord") {
1577
2064
  valid = await verifyDiscordSignature(request.body, request.headers, options.publicKey);
1578
2065
  }
@@ -1949,10 +2436,13 @@ async function collectMatchingRequests(fetchRequests, options) {
1949
2436
  throw new TimeoutError(timeout);
1950
2437
  }
1951
2438
  function validateMockResponse(mockResponse, fieldName) {
1952
- const { status } = mockResponse;
2439
+ const { status, delay } = mockResponse;
1953
2440
  if (!Number.isInteger(status) || status < 100 || status > 599) {
1954
2441
  throw new Error(`Invalid ${fieldName} status: ${status}. Must be an integer 100-599.`);
1955
2442
  }
2443
+ if (delay !== void 0 && (!Number.isInteger(delay) || delay < 0 || delay > 3e4)) {
2444
+ throw new Error(`Invalid ${fieldName} delay: ${delay}. Must be an integer 0-30000.`);
2445
+ }
1956
2446
  }
1957
2447
  var WebhooksCC = class {
1958
2448
  constructor(options) {
@@ -2890,6 +3380,26 @@ function isDiscordWebhook(request) {
2890
3380
  const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
2891
3381
  return keys.includes("x-signature-ed25519") && keys.includes("x-signature-timestamp");
2892
3382
  }
3383
+ function isSendGridWebhook(request) {
3384
+ if (!request.body) return false;
3385
+ try {
3386
+ const parsed = JSON.parse(request.body);
3387
+ return Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "object" && parsed[0] !== null && "sg_event_id" in parsed[0];
3388
+ } catch {
3389
+ return false;
3390
+ }
3391
+ }
3392
+ function isClerkWebhook(request) {
3393
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "svix-id");
3394
+ }
3395
+ function isVercelWebhook(request) {
3396
+ return Object.keys(request.headers).some((k) => k.toLowerCase() === "x-vercel-signature");
3397
+ }
3398
+ function isGitLabWebhook(request) {
3399
+ return Object.keys(request.headers).some(
3400
+ (k) => k.toLowerCase() === "x-gitlab-event" || k.toLowerCase() === "x-gitlab-token"
3401
+ );
3402
+ }
2893
3403
  function isStandardWebhook(request) {
2894
3404
  const keys = Object.keys(request.headers).map((k) => k.toLowerCase());
2895
3405
  return keys.includes("webhook-id") && keys.includes("webhook-timestamp") && keys.includes("webhook-signature");
@@ -3180,15 +3690,19 @@ function diffRequests(left, right, options = {}) {
3180
3690
  WebhooksCCError,
3181
3691
  diffRequests,
3182
3692
  extractJsonField,
3693
+ isClerkWebhook,
3183
3694
  isDiscordWebhook,
3184
3695
  isGitHubWebhook,
3696
+ isGitLabWebhook,
3185
3697
  isLinearWebhook,
3186
3698
  isPaddleWebhook,
3699
+ isSendGridWebhook,
3187
3700
  isShopifyWebhook,
3188
3701
  isSlackWebhook,
3189
3702
  isStandardWebhook,
3190
3703
  isStripeWebhook,
3191
3704
  isTwilioWebhook,
3705
+ isVercelWebhook,
3192
3706
  matchAll,
3193
3707
  matchAny,
3194
3708
  matchBodyPath,
@@ -3204,8 +3718,10 @@ function diffRequests(left, right, options = {}) {
3204
3718
  parseFormBody,
3205
3719
  parseJsonBody,
3206
3720
  parseSSE,
3721
+ verifyClerkSignature,
3207
3722
  verifyDiscordSignature,
3208
3723
  verifyGitHubSignature,
3724
+ verifyGitLabSignature,
3209
3725
  verifyLinearSignature,
3210
3726
  verifyPaddleSignature,
3211
3727
  verifyShopifySignature,
@@ -3213,5 +3729,6 @@ function diffRequests(left, right, options = {}) {
3213
3729
  verifySlackSignature,
3214
3730
  verifyStandardWebhookSignature,
3215
3731
  verifyStripeSignature,
3216
- verifyTwilioSignature
3732
+ verifyTwilioSignature,
3733
+ verifyVercelSignature
3217
3734
  });