@astrasyncai/verification-gateway 5.18.1 → 5.20.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.
Files changed (129) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/README.md +217 -4
  3. package/dist/adapter-interface/interface.d.mts +2 -2
  4. package/dist/adapter-interface/interface.d.ts +2 -2
  5. package/dist/adapters/express.d.mts +9 -1
  6. package/dist/adapters/express.d.ts +9 -1
  7. package/dist/adapters/express.js +160 -52
  8. package/dist/adapters/express.js.map +1 -1
  9. package/dist/adapters/express.mjs +160 -52
  10. package/dist/adapters/express.mjs.map +1 -1
  11. package/dist/adapters/http-pdlss.d.mts +7 -1
  12. package/dist/adapters/http-pdlss.d.ts +7 -1
  13. package/dist/adapters/http-pdlss.js +18 -2
  14. package/dist/adapters/http-pdlss.js.map +1 -1
  15. package/dist/adapters/http-pdlss.mjs +16 -1
  16. package/dist/adapters/http-pdlss.mjs.map +1 -1
  17. package/dist/adapters/kit-express.d.mts +93 -1
  18. package/dist/adapters/kit-express.d.ts +93 -1
  19. package/dist/adapters/kit-express.js +181 -2
  20. package/dist/adapters/kit-express.js.map +1 -1
  21. package/dist/adapters/kit-express.mjs +180 -2
  22. package/dist/adapters/kit-express.mjs.map +1 -1
  23. package/dist/adapters/mcp.d.mts +102 -9
  24. package/dist/adapters/mcp.d.ts +102 -9
  25. package/dist/adapters/mcp.js +242 -19
  26. package/dist/adapters/mcp.js.map +1 -1
  27. package/dist/adapters/mcp.mjs +240 -19
  28. package/dist/adapters/mcp.mjs.map +1 -1
  29. package/dist/adapters/nextjs.d.mts +1 -1
  30. package/dist/adapters/nextjs.d.ts +1 -1
  31. package/dist/adapters/nextjs.js +97 -14
  32. package/dist/adapters/nextjs.js.map +1 -1
  33. package/dist/adapters/nextjs.mjs +97 -14
  34. package/dist/adapters/nextjs.mjs.map +1 -1
  35. package/dist/adapters/sdk.d.mts +101 -2
  36. package/dist/adapters/sdk.d.ts +101 -2
  37. package/dist/adapters/sdk.js +199 -12
  38. package/dist/adapters/sdk.js.map +1 -1
  39. package/dist/adapters/sdk.mjs +199 -12
  40. package/dist/adapters/sdk.mjs.map +1 -1
  41. package/dist/agent/index.d.mts +1 -1
  42. package/dist/agent/index.d.ts +1 -1
  43. package/dist/agent/index.js +1 -1
  44. package/dist/agent/index.js.map +1 -1
  45. package/dist/agent/index.mjs +1 -1
  46. package/dist/agent/index.mjs.map +1 -1
  47. package/dist/bin/astrasync-claude-hook.js +94 -16
  48. package/dist/bin/astrasync-codex-hook.js +94 -16
  49. package/dist/bin/astrasync-guard.js +94 -16
  50. package/dist/bin/astrasync.js +127 -16
  51. package/dist/browser/background.js +94 -16
  52. package/dist/browser/background.js.map +1 -1
  53. package/dist/browser/background.mjs +94 -16
  54. package/dist/browser/background.mjs.map +1 -1
  55. package/dist/cli/index.js +1 -1
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/index.mjs +1 -1
  58. package/dist/cli/index.mjs.map +1 -1
  59. package/dist/codex/index.js +94 -16
  60. package/dist/codex/index.js.map +1 -1
  61. package/dist/codex/index.mjs +94 -16
  62. package/dist/codex/index.mjs.map +1 -1
  63. package/dist/cursor/extension.js +94 -16
  64. package/dist/cursor/extension.js.map +1 -1
  65. package/dist/cursor/extension.mjs +94 -16
  66. package/dist/cursor/extension.mjs.map +1 -1
  67. package/dist/edge-config.d.mts +1 -1
  68. package/dist/edge-config.d.ts +1 -1
  69. package/dist/edge-config.js +1 -1
  70. package/dist/edge-config.js.map +1 -1
  71. package/dist/edge-config.mjs +1 -1
  72. package/dist/edge-config.mjs.map +1 -1
  73. package/dist/edge-core/index.d.mts +1 -1
  74. package/dist/edge-core/index.d.ts +1 -1
  75. package/dist/edge-core/index.js +69 -12
  76. package/dist/edge-core/index.js.map +1 -1
  77. package/dist/edge-core/index.mjs +69 -12
  78. package/dist/edge-core/index.mjs.map +1 -1
  79. package/dist/gateway/gateway.d.mts +2 -2
  80. package/dist/gateway/gateway.d.ts +2 -2
  81. package/dist/gateway/gateway.js +94 -16
  82. package/dist/gateway/gateway.js.map +1 -1
  83. package/dist/gateway/gateway.mjs +94 -16
  84. package/dist/gateway/gateway.mjs.map +1 -1
  85. package/dist/git-trigger/git-hooks.d.mts +1 -1
  86. package/dist/git-trigger/git-hooks.d.ts +1 -1
  87. package/dist/index.d.mts +706 -34
  88. package/dist/index.d.ts +706 -34
  89. package/dist/index.js +1391 -205
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +1374 -195
  92. package/dist/index.mjs.map +1 -1
  93. package/dist/local-evaluator/evaluator.d.mts +2 -2
  94. package/dist/local-evaluator/evaluator.d.ts +2 -2
  95. package/dist/registration/index.d.mts +11 -2
  96. package/dist/registration/index.d.ts +11 -2
  97. package/dist/registration/index.js +1 -1
  98. package/dist/registration/index.js.map +1 -1
  99. package/dist/registration/index.mjs +1 -1
  100. package/dist/registration/index.mjs.map +1 -1
  101. package/dist/transport/index.d.mts +1 -1
  102. package/dist/transport/index.d.ts +1 -1
  103. package/dist/transport/index.js +3 -2
  104. package/dist/transport/index.js.map +1 -1
  105. package/dist/transport/index.mjs +3 -2
  106. package/dist/transport/index.mjs.map +1 -1
  107. package/dist/{types-BRz2U0Pn.d.ts → types-B_HLhZ9n.d.ts} +1 -1
  108. package/dist/{types-BfILnheI.d.mts → types-CU17R4Nr.d.mts} +11 -0
  109. package/dist/{types-BfILnheI.d.ts → types-CU17R4Nr.d.ts} +11 -0
  110. package/dist/{types-Cns4l8gX.d.mts → types-CxjqmPl-.d.mts} +115 -13
  111. package/dist/{types-Cns4l8gX.d.ts → types-CxjqmPl-.d.ts} +115 -13
  112. package/dist/{types-3ObBpsnf.d.mts → types-D37aSQ9c.d.mts} +106 -3
  113. package/dist/{types-CfBpm3w5.d.mts → types-Djpz-L0w.d.mts} +1 -1
  114. package/dist/{types-BoxZEYPS.d.ts → types-Dn8tXnei.d.ts} +106 -3
  115. package/dist/ui/index.d.mts +1 -1
  116. package/dist/ui/index.d.ts +1 -1
  117. package/dist/verify.d.mts +50 -4
  118. package/dist/verify.d.ts +50 -4
  119. package/dist/verify.js +93 -14
  120. package/dist/verify.js.map +1 -1
  121. package/dist/verify.mjs +91 -14
  122. package/dist/verify.mjs.map +1 -1
  123. package/dist/webhooks.d.mts +16 -3
  124. package/dist/webhooks.d.ts +16 -3
  125. package/dist/webhooks.js +24 -13
  126. package/dist/webhooks.js.map +1 -1
  127. package/dist/webhooks.mjs +24 -13
  128. package/dist/webhooks.mjs.map +1 -1
  129. package/package.json +1 -1
@@ -10,6 +10,13 @@
10
10
  *
11
11
  * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.
12
12
  *
13
+ * 5.19.0 — secret rotation: for 24 hours after you rotate the endpoint's
14
+ * webhook secret, AstraSync also signs with the PREVIOUS secret, sent as a
15
+ * `v1old` value: `t=<unix-ts>,v1=<hex>,v1old=<hex>`. A delivery verifies when
16
+ * ANY value matches ANY secret you pass, so pass both while you roll over:
17
+ * `verifyAstraSyncWebhook(raw, headers, [newSecret, oldSecret])`. Queued
18
+ * events signed before the rotation still verify on either side.
19
+ *
13
20
  * Usage:
14
21
  * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';
15
22
  *
@@ -40,18 +47,24 @@ interface VerifyWebhookResult {
40
47
  * @param rawBody - The raw request body as a string (NOT the parsed JSON).
41
48
  * Use `express.raw({type:'application/json'})` to preserve bytes.
42
49
  * @param headers - Incoming request headers (case-insensitive).
43
- * @param secret - The merchant's webhook secret from endpoint registration.
50
+ * @param secret - The merchant's webhook secret from endpoint registration,
51
+ * or (5.19.0) several — e.g. `[current, previous]` during a
52
+ * rotation. Any secret matching any `v1` / `v1old` value passes.
44
53
  * @param options - Optional tolerance overrides.
45
54
  * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.
46
55
  */
47
- declare function verifyAstraSyncWebhook(rawBody: string, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyWebhookOptions): VerifyWebhookResult;
56
+ declare function verifyAstraSyncWebhook(rawBody: string, headers: Record<string, string | string[] | undefined>, secret: string | readonly string[], options?: VerifyWebhookOptions): VerifyWebhookResult;
48
57
  /**
49
58
  * Server-side companion: produce an `X-AstraSync-Signature` header value for
50
59
  * an outbound webhook delivery. Exposed for completeness and for test
51
60
  * harnesses that want to verify the verifier; the AstraSync platform itself
52
61
  * uses an internal version of the same logic.
53
62
  */
54
- declare function signAstraSyncWebhook(rawBody: string, secret: string, nowMs?: number): {
63
+ declare function signAstraSyncWebhook(rawBody: string, secret: string, nowMs?: number,
64
+ /** 5.19.0: also sign with the previous secret (rotation) as `v1old`. */
65
+ options?: {
66
+ previousSecret?: string;
67
+ }): {
55
68
  header: string;
56
69
  timestamp: number;
57
70
  };
@@ -10,6 +10,13 @@
10
10
  *
11
11
  * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.
12
12
  *
13
+ * 5.19.0 — secret rotation: for 24 hours after you rotate the endpoint's
14
+ * webhook secret, AstraSync also signs with the PREVIOUS secret, sent as a
15
+ * `v1old` value: `t=<unix-ts>,v1=<hex>,v1old=<hex>`. A delivery verifies when
16
+ * ANY value matches ANY secret you pass, so pass both while you roll over:
17
+ * `verifyAstraSyncWebhook(raw, headers, [newSecret, oldSecret])`. Queued
18
+ * events signed before the rotation still verify on either side.
19
+ *
13
20
  * Usage:
14
21
  * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';
15
22
  *
@@ -40,18 +47,24 @@ interface VerifyWebhookResult {
40
47
  * @param rawBody - The raw request body as a string (NOT the parsed JSON).
41
48
  * Use `express.raw({type:'application/json'})` to preserve bytes.
42
49
  * @param headers - Incoming request headers (case-insensitive).
43
- * @param secret - The merchant's webhook secret from endpoint registration.
50
+ * @param secret - The merchant's webhook secret from endpoint registration,
51
+ * or (5.19.0) several — e.g. `[current, previous]` during a
52
+ * rotation. Any secret matching any `v1` / `v1old` value passes.
44
53
  * @param options - Optional tolerance overrides.
45
54
  * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.
46
55
  */
47
- declare function verifyAstraSyncWebhook(rawBody: string, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyWebhookOptions): VerifyWebhookResult;
56
+ declare function verifyAstraSyncWebhook(rawBody: string, headers: Record<string, string | string[] | undefined>, secret: string | readonly string[], options?: VerifyWebhookOptions): VerifyWebhookResult;
48
57
  /**
49
58
  * Server-side companion: produce an `X-AstraSync-Signature` header value for
50
59
  * an outbound webhook delivery. Exposed for completeness and for test
51
60
  * harnesses that want to verify the verifier; the AstraSync platform itself
52
61
  * uses an internal version of the same logic.
53
62
  */
54
- declare function signAstraSyncWebhook(rawBody: string, secret: string, nowMs?: number): {
63
+ declare function signAstraSyncWebhook(rawBody: string, secret: string, nowMs?: number,
64
+ /** 5.19.0: also sign with the previous secret (rotation) as `v1old`. */
65
+ options?: {
66
+ previousSecret?: string;
67
+ }): {
55
68
  header: string;
56
69
  timestamp: number;
57
70
  };
package/dist/webhooks.js CHANGED
@@ -27,12 +27,17 @@ module.exports = __toCommonJS(webhooks_exports);
27
27
  var import_crypto = require("crypto");
28
28
  var DEFAULT_TOLERANCE_SECONDS = 300;
29
29
  function parseSignatureHeader(header) {
30
- const parts = {};
30
+ let t;
31
+ const signatures = [];
31
32
  for (const segment of header.split(",")) {
32
- const [k, v] = segment.split("=");
33
- if (k === "t" || k === "v1") parts[k] = v;
33
+ const eq = segment.indexOf("=");
34
+ if (eq < 0) continue;
35
+ const k = segment.slice(0, eq).trim();
36
+ const v = segment.slice(eq + 1).trim();
37
+ if (k === "t") t = v;
38
+ else if ((k === "v1" || k === "v1old") && v) signatures.push(v);
34
39
  }
35
- return parts;
40
+ return { t, signatures };
36
41
  }
37
42
  function computeHmac(secret, signedPayload) {
38
43
  return (0, import_crypto.createHmac)("sha256", secret).update(signedPayload, "utf8").digest("hex");
@@ -44,12 +49,15 @@ function constantTimeEquals(a, b) {
44
49
  return (0, import_crypto.timingSafeEqual)(aBuf, bBuf);
45
50
  }
46
51
  function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
47
- if (!secret) return { ok: false, reason: "no_secret_provided" };
52
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(
53
+ (x) => typeof x === "string" && x.length > 0
54
+ );
55
+ if (!secrets.length) return { ok: false, reason: "no_secret_provided" };
48
56
  const rawHeader = headers["x-astrasync-signature"] ?? headers["X-Astrasync-Signature"] ?? headers["X-AstraSync-Signature"] ?? headers["X-ASTRASYNC-SIGNATURE"];
49
57
  if (!rawHeader) return { ok: false, reason: "missing_signature_header" };
50
58
  const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
51
- const { t: timestamp, v1: receivedSignature } = parseSignatureHeader(headerValue);
52
- if (!timestamp || !receivedSignature) {
59
+ const { t: timestamp, signatures } = parseSignatureHeader(headerValue);
60
+ if (!timestamp || !signatures.length) {
53
61
  return { ok: false, reason: "malformed_signature_header" };
54
62
  }
55
63
  const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
@@ -62,16 +70,19 @@ function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
62
70
  if (ageSeconds > tolerance) {
63
71
  return { ok: false, reason: "timestamp_outside_tolerance" };
64
72
  }
65
- const expectedSignature = computeHmac(secret, `${timestamp}.${rawBody}`);
66
- if (!constantTimeEquals(expectedSignature, receivedSignature)) {
67
- return { ok: false, reason: "signature_mismatch" };
73
+ for (const key of secrets) {
74
+ const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);
75
+ if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {
76
+ return { ok: true };
77
+ }
68
78
  }
69
- return { ok: true };
79
+ return { ok: false, reason: "signature_mismatch" };
70
80
  }
71
- function signAstraSyncWebhook(rawBody, secret, nowMs = Date.now()) {
81
+ function signAstraSyncWebhook(rawBody, secret, nowMs = Date.now(), options = {}) {
72
82
  const timestamp = Math.floor(nowMs / 1e3);
73
83
  const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);
74
- return { header: `t=${timestamp},v1=${v1}`, timestamp };
84
+ const old = options.previousSecret ? `,v1old=${computeHmac(options.previousSecret, `${timestamp}.${rawBody}`)}` : "";
85
+ return { header: `t=${timestamp},v1=${v1}${old}`, timestamp };
75
86
  }
76
87
  // Annotate the CommonJS export names for ESM import in node:
77
88
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/webhooks.ts"],"sourcesContent":["/**\n * AstraSync webhook signature verification.\n *\n * AstraSync signs every webhook delivery with HMAC-SHA256 over\n * `${unix_timestamp}.${rawBody}` using the merchant's webhook secret\n * (returned ONCE at endpoint registration). The signature is sent in\n * the `X-AstraSync-Signature` header in the form:\n *\n * X-AstraSync-Signature: t=<unix-ts>,v1=<hex-hmac>\n *\n * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.\n *\n * Usage:\n * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';\n *\n * app.post('/webhooks/astrasync', express.raw({type:'application/json'}), (req, res) => {\n * const result = verifyAstraSyncWebhook(req.body.toString('utf8'), req.headers, secret);\n * if (!result.ok) return res.status(401).json({error: result.reason});\n * // ... process event\n * });\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto';\n\nexport interface VerifyWebhookOptions {\n /**\n * Maximum age (seconds) for the signature timestamp. Older deliveries\n * are rejected as replays. Default 300 (5 minutes) — matches Stripe.\n */\n toleranceSeconds?: number;\n /**\n * Override \"now\" for tests. Defaults to Date.now().\n */\n nowMs?: number;\n}\n\nexport interface VerifyWebhookResult {\n ok: boolean;\n reason?: string;\n}\n\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction parseSignatureHeader(header: string): { t?: string; v1?: string } {\n const parts: { t?: string; v1?: string } = {};\n for (const segment of header.split(',')) {\n const [k, v] = segment.split('=');\n if (k === 't' || k === 'v1') parts[k] = v;\n }\n return parts;\n}\n\nfunction computeHmac(secret: string, signedPayload: string): string {\n return createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex');\n}\n\nfunction constantTimeEquals(a: string, b: string): boolean {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n}\n\n/**\n * Verify an AstraSync-issued webhook delivery.\n *\n * @param rawBody - The raw request body as a string (NOT the parsed JSON).\n * Use `express.raw({type:'application/json'})` to preserve bytes.\n * @param headers - Incoming request headers (case-insensitive).\n * @param secret - The merchant's webhook secret from endpoint registration.\n * @param options - Optional tolerance overrides.\n * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.\n */\nexport function verifyAstraSyncWebhook(\n rawBody: string,\n headers: Record<string, string | string[] | undefined>,\n secret: string,\n options: VerifyWebhookOptions = {}\n): VerifyWebhookResult {\n if (!secret) return { ok: false, reason: 'no_secret_provided' };\n\n // Header lookup is case-insensitive — support common variants.\n const rawHeader =\n headers['x-astrasync-signature'] ??\n headers['X-Astrasync-Signature'] ??\n headers['X-AstraSync-Signature'] ??\n headers['X-ASTRASYNC-SIGNATURE'];\n if (!rawHeader) return { ok: false, reason: 'missing_signature_header' };\n\n const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;\n const { t: timestamp, v1: receivedSignature } = parseSignatureHeader(headerValue);\n if (!timestamp || !receivedSignature) {\n return { ok: false, reason: 'malformed_signature_header' };\n }\n\n const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.nowMs ?? Date.now();\n const tsSeconds = Number(timestamp);\n if (!Number.isFinite(tsSeconds)) {\n return { ok: false, reason: 'invalid_timestamp' };\n }\n const ageSeconds = Math.abs(now / 1000 - tsSeconds);\n if (ageSeconds > tolerance) {\n return { ok: false, reason: 'timestamp_outside_tolerance' };\n }\n\n const expectedSignature = computeHmac(secret, `${timestamp}.${rawBody}`);\n if (!constantTimeEquals(expectedSignature, receivedSignature)) {\n return { ok: false, reason: 'signature_mismatch' };\n }\n\n return { ok: true };\n}\n\n/**\n * Server-side companion: produce an `X-AstraSync-Signature` header value for\n * an outbound webhook delivery. Exposed for completeness and for test\n * harnesses that want to verify the verifier; the AstraSync platform itself\n * uses an internal version of the same logic.\n */\nexport function signAstraSyncWebhook(\n rawBody: string,\n secret: string,\n nowMs: number = Date.now()\n): { header: string; timestamp: number } {\n const timestamp = Math.floor(nowMs / 1000);\n const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);\n return { header: `t=${timestamp},v1=${v1}`, timestamp };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBA,oBAA4C;AAmB5C,IAAM,4BAA4B;AAElC,SAAS,qBAAqB,QAA6C;AACzE,QAAM,QAAqC,CAAC;AAC5C,aAAW,WAAW,OAAO,MAAM,GAAG,GAAG;AACvC,UAAM,CAAC,GAAG,CAAC,IAAI,QAAQ,MAAM,GAAG;AAChC,QAAI,MAAM,OAAO,MAAM,KAAM,OAAM,CAAC,IAAI;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,SAAS,YAAY,QAAgB,eAA+B;AAClE,aAAO,0BAAW,UAAU,MAAM,EAAE,OAAO,eAAe,MAAM,EAAE,OAAO,KAAK;AAChF;AAEA,SAAS,mBAAmB,GAAW,GAAoB;AACzD,QAAM,OAAO,OAAO,KAAK,GAAG,MAAM;AAClC,QAAM,OAAO,OAAO,KAAK,GAAG,MAAM;AAClC,MAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,aAAO,+BAAgB,MAAM,IAAI;AACnC;AAYO,SAAS,uBACd,SACA,SACA,QACA,UAAgC,CAAC,GACZ;AACrB,MAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,OAAO,QAAQ,qBAAqB;AAG9D,QAAM,YACJ,QAAQ,uBAAuB,KAC/B,QAAQ,uBAAuB,KAC/B,QAAQ,uBAAuB,KAC/B,QAAQ,uBAAuB;AACjC,MAAI,CAAC,UAAW,QAAO,EAAE,IAAI,OAAO,QAAQ,2BAA2B;AAEvE,QAAM,cAAc,MAAM,QAAQ,SAAS,IAAI,UAAU,CAAC,IAAI;AAC9D,QAAM,EAAE,GAAG,WAAW,IAAI,kBAAkB,IAAI,qBAAqB,WAAW;AAChF,MAAI,CAAC,aAAa,CAAC,mBAAmB;AACpC,WAAO,EAAE,IAAI,OAAO,QAAQ,6BAA6B;AAAA,EAC3D;AAEA,QAAM,YAAY,QAAQ,oBAAoB;AAC9C,QAAM,MAAM,QAAQ,SAAS,KAAK,IAAI;AACtC,QAAM,YAAY,OAAO,SAAS;AAClC,MAAI,CAAC,OAAO,SAAS,SAAS,GAAG;AAC/B,WAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB;AAAA,EAClD;AACA,QAAM,aAAa,KAAK,IAAI,MAAM,MAAO,SAAS;AAClD,MAAI,aAAa,WAAW;AAC1B,WAAO,EAAE,IAAI,OAAO,QAAQ,8BAA8B;AAAA,EAC5D;AAEA,QAAM,oBAAoB,YAAY,QAAQ,GAAG,SAAS,IAAI,OAAO,EAAE;AACvE,MAAI,CAAC,mBAAmB,mBAAmB,iBAAiB,GAAG;AAC7D,WAAO,EAAE,IAAI,OAAO,QAAQ,qBAAqB;AAAA,EACnD;AAEA,SAAO,EAAE,IAAI,KAAK;AACpB;AAQO,SAAS,qBACd,SACA,QACA,QAAgB,KAAK,IAAI,GACc;AACvC,QAAM,YAAY,KAAK,MAAM,QAAQ,GAAI;AACzC,QAAM,KAAK,YAAY,QAAQ,GAAG,SAAS,IAAI,OAAO,EAAE;AACxD,SAAO,EAAE,QAAQ,KAAK,SAAS,OAAO,EAAE,IAAI,UAAU;AACxD;","names":[]}
1
+ {"version":3,"sources":["../src/webhooks.ts"],"sourcesContent":["/**\n * AstraSync webhook signature verification.\n *\n * AstraSync signs every webhook delivery with HMAC-SHA256 over\n * `${unix_timestamp}.${rawBody}` using the merchant's webhook secret\n * (returned ONCE at endpoint registration). The signature is sent in\n * the `X-AstraSync-Signature` header in the form:\n *\n * X-AstraSync-Signature: t=<unix-ts>,v1=<hex-hmac>\n *\n * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.\n *\n * 5.19.0 — secret rotation: for 24 hours after you rotate the endpoint's\n * webhook secret, AstraSync also signs with the PREVIOUS secret, sent as a\n * `v1old` value: `t=<unix-ts>,v1=<hex>,v1old=<hex>`. A delivery verifies when\n * ANY value matches ANY secret you pass, so pass both while you roll over:\n * `verifyAstraSyncWebhook(raw, headers, [newSecret, oldSecret])`. Queued\n * events signed before the rotation still verify on either side.\n *\n * Usage:\n * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';\n *\n * app.post('/webhooks/astrasync', express.raw({type:'application/json'}), (req, res) => {\n * const result = verifyAstraSyncWebhook(req.body.toString('utf8'), req.headers, secret);\n * if (!result.ok) return res.status(401).json({error: result.reason});\n * // ... process event\n * });\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto';\n\nexport interface VerifyWebhookOptions {\n /**\n * Maximum age (seconds) for the signature timestamp. Older deliveries\n * are rejected as replays. Default 300 (5 minutes) — matches Stripe.\n */\n toleranceSeconds?: number;\n /**\n * Override \"now\" for tests. Defaults to Date.now().\n */\n nowMs?: number;\n}\n\nexport interface VerifyWebhookResult {\n ok: boolean;\n reason?: string;\n}\n\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction parseSignatureHeader(header: string): { t?: string; signatures: string[] } {\n let t: string | undefined;\n const signatures: string[] = [];\n for (const segment of header.split(',')) {\n const eq = segment.indexOf('=');\n if (eq < 0) continue;\n const k = segment.slice(0, eq).trim();\n const v = segment.slice(eq + 1).trim();\n if (k === 't') t = v;\n // 5.19.0: `v1old` = the same HMAC under the previous secret (rotation).\n else if ((k === 'v1' || k === 'v1old') && v) signatures.push(v);\n }\n return { t, signatures };\n}\n\nfunction computeHmac(secret: string, signedPayload: string): string {\n return createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex');\n}\n\nfunction constantTimeEquals(a: string, b: string): boolean {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n}\n\n/**\n * Verify an AstraSync-issued webhook delivery.\n *\n * @param rawBody - The raw request body as a string (NOT the parsed JSON).\n * Use `express.raw({type:'application/json'})` to preserve bytes.\n * @param headers - Incoming request headers (case-insensitive).\n * @param secret - The merchant's webhook secret from endpoint registration,\n * or (5.19.0) several — e.g. `[current, previous]` during a\n * rotation. Any secret matching any `v1` / `v1old` value passes.\n * @param options - Optional tolerance overrides.\n * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.\n */\nexport function verifyAstraSyncWebhook(\n rawBody: string,\n headers: Record<string, string | string[] | undefined>,\n secret: string | readonly string[],\n options: VerifyWebhookOptions = {}\n): VerifyWebhookResult {\n const secrets = (Array.isArray(secret) ? secret : [secret]).filter(\n (x): x is string => typeof x === 'string' && x.length > 0\n );\n if (!secrets.length) return { ok: false, reason: 'no_secret_provided' };\n\n // Header lookup is case-insensitive — support common variants.\n const rawHeader =\n headers['x-astrasync-signature'] ??\n headers['X-Astrasync-Signature'] ??\n headers['X-AstraSync-Signature'] ??\n headers['X-ASTRASYNC-SIGNATURE'];\n if (!rawHeader) return { ok: false, reason: 'missing_signature_header' };\n\n const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;\n const { t: timestamp, signatures } = parseSignatureHeader(headerValue);\n if (!timestamp || !signatures.length) {\n return { ok: false, reason: 'malformed_signature_header' };\n }\n\n const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.nowMs ?? Date.now();\n const tsSeconds = Number(timestamp);\n if (!Number.isFinite(tsSeconds)) {\n return { ok: false, reason: 'invalid_timestamp' };\n }\n const ageSeconds = Math.abs(now / 1000 - tsSeconds);\n if (ageSeconds > tolerance) {\n return { ok: false, reason: 'timestamp_outside_tolerance' };\n }\n\n for (const key of secrets) {\n const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);\n if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {\n return { ok: true };\n }\n }\n return { ok: false, reason: 'signature_mismatch' };\n}\n\n/**\n * Server-side companion: produce an `X-AstraSync-Signature` header value for\n * an outbound webhook delivery. Exposed for completeness and for test\n * harnesses that want to verify the verifier; the AstraSync platform itself\n * uses an internal version of the same logic.\n */\nexport function signAstraSyncWebhook(\n rawBody: string,\n secret: string,\n nowMs: number = Date.now(),\n /** 5.19.0: also sign with the previous secret (rotation) as `v1old`. */\n options: { previousSecret?: string } = {}\n): { header: string; timestamp: number } {\n const timestamp = Math.floor(nowMs / 1000);\n const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);\n const old = options.previousSecret\n ? `,v1old=${computeHmac(options.previousSecret, `${timestamp}.${rawBody}`)}`\n : '';\n return { header: `t=${timestamp},v1=${v1}${old}`, timestamp };\n}\n"],"mappings":"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","names":[]}
package/dist/webhooks.mjs CHANGED
@@ -2,12 +2,17 @@
2
2
  import { createHmac, timingSafeEqual } from "crypto";
3
3
  var DEFAULT_TOLERANCE_SECONDS = 300;
4
4
  function parseSignatureHeader(header) {
5
- const parts = {};
5
+ let t;
6
+ const signatures = [];
6
7
  for (const segment of header.split(",")) {
7
- const [k, v] = segment.split("=");
8
- if (k === "t" || k === "v1") parts[k] = v;
8
+ const eq = segment.indexOf("=");
9
+ if (eq < 0) continue;
10
+ const k = segment.slice(0, eq).trim();
11
+ const v = segment.slice(eq + 1).trim();
12
+ if (k === "t") t = v;
13
+ else if ((k === "v1" || k === "v1old") && v) signatures.push(v);
9
14
  }
10
- return parts;
15
+ return { t, signatures };
11
16
  }
12
17
  function computeHmac(secret, signedPayload) {
13
18
  return createHmac("sha256", secret).update(signedPayload, "utf8").digest("hex");
@@ -19,12 +24,15 @@ function constantTimeEquals(a, b) {
19
24
  return timingSafeEqual(aBuf, bBuf);
20
25
  }
21
26
  function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
22
- if (!secret) return { ok: false, reason: "no_secret_provided" };
27
+ const secrets = (Array.isArray(secret) ? secret : [secret]).filter(
28
+ (x) => typeof x === "string" && x.length > 0
29
+ );
30
+ if (!secrets.length) return { ok: false, reason: "no_secret_provided" };
23
31
  const rawHeader = headers["x-astrasync-signature"] ?? headers["X-Astrasync-Signature"] ?? headers["X-AstraSync-Signature"] ?? headers["X-ASTRASYNC-SIGNATURE"];
24
32
  if (!rawHeader) return { ok: false, reason: "missing_signature_header" };
25
33
  const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
26
- const { t: timestamp, v1: receivedSignature } = parseSignatureHeader(headerValue);
27
- if (!timestamp || !receivedSignature) {
34
+ const { t: timestamp, signatures } = parseSignatureHeader(headerValue);
35
+ if (!timestamp || !signatures.length) {
28
36
  return { ok: false, reason: "malformed_signature_header" };
29
37
  }
30
38
  const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
@@ -37,16 +45,19 @@ function verifyAstraSyncWebhook(rawBody, headers, secret, options = {}) {
37
45
  if (ageSeconds > tolerance) {
38
46
  return { ok: false, reason: "timestamp_outside_tolerance" };
39
47
  }
40
- const expectedSignature = computeHmac(secret, `${timestamp}.${rawBody}`);
41
- if (!constantTimeEquals(expectedSignature, receivedSignature)) {
42
- return { ok: false, reason: "signature_mismatch" };
48
+ for (const key of secrets) {
49
+ const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);
50
+ if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {
51
+ return { ok: true };
52
+ }
43
53
  }
44
- return { ok: true };
54
+ return { ok: false, reason: "signature_mismatch" };
45
55
  }
46
- function signAstraSyncWebhook(rawBody, secret, nowMs = Date.now()) {
56
+ function signAstraSyncWebhook(rawBody, secret, nowMs = Date.now(), options = {}) {
47
57
  const timestamp = Math.floor(nowMs / 1e3);
48
58
  const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);
49
- return { header: `t=${timestamp},v1=${v1}`, timestamp };
59
+ const old = options.previousSecret ? `,v1old=${computeHmac(options.previousSecret, `${timestamp}.${rawBody}`)}` : "";
60
+ return { header: `t=${timestamp},v1=${v1}${old}`, timestamp };
50
61
  }
51
62
  export {
52
63
  signAstraSyncWebhook,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/webhooks.ts"],"sourcesContent":["/**\n * AstraSync webhook signature verification.\n *\n * AstraSync signs every webhook delivery with HMAC-SHA256 over\n * `${unix_timestamp}.${rawBody}` using the merchant's webhook secret\n * (returned ONCE at endpoint registration). The signature is sent in\n * the `X-AstraSync-Signature` header in the form:\n *\n * X-AstraSync-Signature: t=<unix-ts>,v1=<hex-hmac>\n *\n * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.\n *\n * Usage:\n * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';\n *\n * app.post('/webhooks/astrasync', express.raw({type:'application/json'}), (req, res) => {\n * const result = verifyAstraSyncWebhook(req.body.toString('utf8'), req.headers, secret);\n * if (!result.ok) return res.status(401).json({error: result.reason});\n * // ... process event\n * });\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto';\n\nexport interface VerifyWebhookOptions {\n /**\n * Maximum age (seconds) for the signature timestamp. Older deliveries\n * are rejected as replays. Default 300 (5 minutes) — matches Stripe.\n */\n toleranceSeconds?: number;\n /**\n * Override \"now\" for tests. Defaults to Date.now().\n */\n nowMs?: number;\n}\n\nexport interface VerifyWebhookResult {\n ok: boolean;\n reason?: string;\n}\n\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction parseSignatureHeader(header: string): { t?: string; v1?: string } {\n const parts: { t?: string; v1?: string } = {};\n for (const segment of header.split(',')) {\n const [k, v] = segment.split('=');\n if (k === 't' || k === 'v1') parts[k] = v;\n }\n return parts;\n}\n\nfunction computeHmac(secret: string, signedPayload: string): string {\n return createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex');\n}\n\nfunction constantTimeEquals(a: string, b: string): boolean {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n}\n\n/**\n * Verify an AstraSync-issued webhook delivery.\n *\n * @param rawBody - The raw request body as a string (NOT the parsed JSON).\n * Use `express.raw({type:'application/json'})` to preserve bytes.\n * @param headers - Incoming request headers (case-insensitive).\n * @param secret - The merchant's webhook secret from endpoint registration.\n * @param options - Optional tolerance overrides.\n * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.\n */\nexport function verifyAstraSyncWebhook(\n rawBody: string,\n headers: Record<string, string | string[] | undefined>,\n secret: string,\n options: VerifyWebhookOptions = {}\n): VerifyWebhookResult {\n if (!secret) return { ok: false, reason: 'no_secret_provided' };\n\n // Header lookup is case-insensitive — support common variants.\n const rawHeader =\n headers['x-astrasync-signature'] ??\n headers['X-Astrasync-Signature'] ??\n headers['X-AstraSync-Signature'] ??\n headers['X-ASTRASYNC-SIGNATURE'];\n if (!rawHeader) return { ok: false, reason: 'missing_signature_header' };\n\n const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;\n const { t: timestamp, v1: receivedSignature } = parseSignatureHeader(headerValue);\n if (!timestamp || !receivedSignature) {\n return { ok: false, reason: 'malformed_signature_header' };\n }\n\n const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.nowMs ?? Date.now();\n const tsSeconds = Number(timestamp);\n if (!Number.isFinite(tsSeconds)) {\n return { ok: false, reason: 'invalid_timestamp' };\n }\n const ageSeconds = Math.abs(now / 1000 - tsSeconds);\n if (ageSeconds > tolerance) {\n return { ok: false, reason: 'timestamp_outside_tolerance' };\n }\n\n const expectedSignature = computeHmac(secret, `${timestamp}.${rawBody}`);\n if (!constantTimeEquals(expectedSignature, receivedSignature)) {\n return { ok: false, reason: 'signature_mismatch' };\n }\n\n return { ok: true };\n}\n\n/**\n * Server-side companion: produce an `X-AstraSync-Signature` header value for\n * an outbound webhook delivery. Exposed for completeness and for test\n * harnesses that want to verify the verifier; the AstraSync platform itself\n * uses an internal version of the same logic.\n */\nexport function signAstraSyncWebhook(\n rawBody: string,\n secret: string,\n nowMs: number = Date.now()\n): { header: string; timestamp: number } {\n const timestamp = Math.floor(nowMs / 1000);\n const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);\n return { header: `t=${timestamp},v1=${v1}`, timestamp };\n}\n"],"mappings":";AAsBA,SAAS,YAAY,uBAAuB;AAmB5C,IAAM,4BAA4B;AAElC,SAAS,qBAAqB,QAA6C;AACzE,QAAM,QAAqC,CAAC;AAC5C,aAAW,WAAW,OAAO,MAAM,GAAG,GAAG;AACvC,UAAM,CAAC,GAAG,CAAC,IAAI,QAAQ,MAAM,GAAG;AAChC,QAAI,MAAM,OAAO,MAAM,KAAM,OAAM,CAAC,IAAI;AAAA,EAC1C;AACA,SAAO;AACT;AAEA,SAAS,YAAY,QAAgB,eAA+B;AAClE,SAAO,WAAW,UAAU,MAAM,EAAE,OAAO,eAAe,MAAM,EAAE,OAAO,KAAK;AAChF;AAEA,SAAS,mBAAmB,GAAW,GAAoB;AACzD,QAAM,OAAO,OAAO,KAAK,GAAG,MAAM;AAClC,QAAM,OAAO,OAAO,KAAK,GAAG,MAAM;AAClC,MAAI,KAAK,WAAW,KAAK,OAAQ,QAAO;AACxC,SAAO,gBAAgB,MAAM,IAAI;AACnC;AAYO,SAAS,uBACd,SACA,SACA,QACA,UAAgC,CAAC,GACZ;AACrB,MAAI,CAAC,OAAQ,QAAO,EAAE,IAAI,OAAO,QAAQ,qBAAqB;AAG9D,QAAM,YACJ,QAAQ,uBAAuB,KAC/B,QAAQ,uBAAuB,KAC/B,QAAQ,uBAAuB,KAC/B,QAAQ,uBAAuB;AACjC,MAAI,CAAC,UAAW,QAAO,EAAE,IAAI,OAAO,QAAQ,2BAA2B;AAEvE,QAAM,cAAc,MAAM,QAAQ,SAAS,IAAI,UAAU,CAAC,IAAI;AAC9D,QAAM,EAAE,GAAG,WAAW,IAAI,kBAAkB,IAAI,qBAAqB,WAAW;AAChF,MAAI,CAAC,aAAa,CAAC,mBAAmB;AACpC,WAAO,EAAE,IAAI,OAAO,QAAQ,6BAA6B;AAAA,EAC3D;AAEA,QAAM,YAAY,QAAQ,oBAAoB;AAC9C,QAAM,MAAM,QAAQ,SAAS,KAAK,IAAI;AACtC,QAAM,YAAY,OAAO,SAAS;AAClC,MAAI,CAAC,OAAO,SAAS,SAAS,GAAG;AAC/B,WAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB;AAAA,EAClD;AACA,QAAM,aAAa,KAAK,IAAI,MAAM,MAAO,SAAS;AAClD,MAAI,aAAa,WAAW;AAC1B,WAAO,EAAE,IAAI,OAAO,QAAQ,8BAA8B;AAAA,EAC5D;AAEA,QAAM,oBAAoB,YAAY,QAAQ,GAAG,SAAS,IAAI,OAAO,EAAE;AACvE,MAAI,CAAC,mBAAmB,mBAAmB,iBAAiB,GAAG;AAC7D,WAAO,EAAE,IAAI,OAAO,QAAQ,qBAAqB;AAAA,EACnD;AAEA,SAAO,EAAE,IAAI,KAAK;AACpB;AAQO,SAAS,qBACd,SACA,QACA,QAAgB,KAAK,IAAI,GACc;AACvC,QAAM,YAAY,KAAK,MAAM,QAAQ,GAAI;AACzC,QAAM,KAAK,YAAY,QAAQ,GAAG,SAAS,IAAI,OAAO,EAAE;AACxD,SAAO,EAAE,QAAQ,KAAK,SAAS,OAAO,EAAE,IAAI,UAAU;AACxD;","names":[]}
1
+ {"version":3,"sources":["../src/webhooks.ts"],"sourcesContent":["/**\n * AstraSync webhook signature verification.\n *\n * AstraSync signs every webhook delivery with HMAC-SHA256 over\n * `${unix_timestamp}.${rawBody}` using the merchant's webhook secret\n * (returned ONCE at endpoint registration). The signature is sent in\n * the `X-AstraSync-Signature` header in the form:\n *\n * X-AstraSync-Signature: t=<unix-ts>,v1=<hex-hmac>\n *\n * This pattern mirrors Stripe's `Stripe-Signature` to ease adoption.\n *\n * 5.19.0 — secret rotation: for 24 hours after you rotate the endpoint's\n * webhook secret, AstraSync also signs with the PREVIOUS secret, sent as a\n * `v1old` value: `t=<unix-ts>,v1=<hex>,v1old=<hex>`. A delivery verifies when\n * ANY value matches ANY secret you pass, so pass both while you roll over:\n * `verifyAstraSyncWebhook(raw, headers, [newSecret, oldSecret])`. Queued\n * events signed before the rotation still verify on either side.\n *\n * Usage:\n * import { verifyAstraSyncWebhook } from '@astrasyncai/verification-gateway/webhooks';\n *\n * app.post('/webhooks/astrasync', express.raw({type:'application/json'}), (req, res) => {\n * const result = verifyAstraSyncWebhook(req.body.toString('utf8'), req.headers, secret);\n * if (!result.ok) return res.status(401).json({error: result.reason});\n * // ... process event\n * });\n */\n\nimport { createHmac, timingSafeEqual } from 'crypto';\n\nexport interface VerifyWebhookOptions {\n /**\n * Maximum age (seconds) for the signature timestamp. Older deliveries\n * are rejected as replays. Default 300 (5 minutes) — matches Stripe.\n */\n toleranceSeconds?: number;\n /**\n * Override \"now\" for tests. Defaults to Date.now().\n */\n nowMs?: number;\n}\n\nexport interface VerifyWebhookResult {\n ok: boolean;\n reason?: string;\n}\n\nconst DEFAULT_TOLERANCE_SECONDS = 300;\n\nfunction parseSignatureHeader(header: string): { t?: string; signatures: string[] } {\n let t: string | undefined;\n const signatures: string[] = [];\n for (const segment of header.split(',')) {\n const eq = segment.indexOf('=');\n if (eq < 0) continue;\n const k = segment.slice(0, eq).trim();\n const v = segment.slice(eq + 1).trim();\n if (k === 't') t = v;\n // 5.19.0: `v1old` = the same HMAC under the previous secret (rotation).\n else if ((k === 'v1' || k === 'v1old') && v) signatures.push(v);\n }\n return { t, signatures };\n}\n\nfunction computeHmac(secret: string, signedPayload: string): string {\n return createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex');\n}\n\nfunction constantTimeEquals(a: string, b: string): boolean {\n const aBuf = Buffer.from(a, 'utf8');\n const bBuf = Buffer.from(b, 'utf8');\n if (aBuf.length !== bBuf.length) return false;\n return timingSafeEqual(aBuf, bBuf);\n}\n\n/**\n * Verify an AstraSync-issued webhook delivery.\n *\n * @param rawBody - The raw request body as a string (NOT the parsed JSON).\n * Use `express.raw({type:'application/json'})` to preserve bytes.\n * @param headers - Incoming request headers (case-insensitive).\n * @param secret - The merchant's webhook secret from endpoint registration,\n * or (5.19.0) several — e.g. `[current, previous]` during a\n * rotation. Any secret matching any `v1` / `v1old` value passes.\n * @param options - Optional tolerance overrides.\n * @returns - `{ok: true}` on success, `{ok: false, reason}` on failure.\n */\nexport function verifyAstraSyncWebhook(\n rawBody: string,\n headers: Record<string, string | string[] | undefined>,\n secret: string | readonly string[],\n options: VerifyWebhookOptions = {}\n): VerifyWebhookResult {\n const secrets = (Array.isArray(secret) ? secret : [secret]).filter(\n (x): x is string => typeof x === 'string' && x.length > 0\n );\n if (!secrets.length) return { ok: false, reason: 'no_secret_provided' };\n\n // Header lookup is case-insensitive — support common variants.\n const rawHeader =\n headers['x-astrasync-signature'] ??\n headers['X-Astrasync-Signature'] ??\n headers['X-AstraSync-Signature'] ??\n headers['X-ASTRASYNC-SIGNATURE'];\n if (!rawHeader) return { ok: false, reason: 'missing_signature_header' };\n\n const headerValue = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;\n const { t: timestamp, signatures } = parseSignatureHeader(headerValue);\n if (!timestamp || !signatures.length) {\n return { ok: false, reason: 'malformed_signature_header' };\n }\n\n const tolerance = options.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;\n const now = options.nowMs ?? Date.now();\n const tsSeconds = Number(timestamp);\n if (!Number.isFinite(tsSeconds)) {\n return { ok: false, reason: 'invalid_timestamp' };\n }\n const ageSeconds = Math.abs(now / 1000 - tsSeconds);\n if (ageSeconds > tolerance) {\n return { ok: false, reason: 'timestamp_outside_tolerance' };\n }\n\n for (const key of secrets) {\n const expectedSignature = computeHmac(key, `${timestamp}.${rawBody}`);\n if (signatures.some((received) => constantTimeEquals(expectedSignature, received))) {\n return { ok: true };\n }\n }\n return { ok: false, reason: 'signature_mismatch' };\n}\n\n/**\n * Server-side companion: produce an `X-AstraSync-Signature` header value for\n * an outbound webhook delivery. Exposed for completeness and for test\n * harnesses that want to verify the verifier; the AstraSync platform itself\n * uses an internal version of the same logic.\n */\nexport function signAstraSyncWebhook(\n rawBody: string,\n secret: string,\n nowMs: number = Date.now(),\n /** 5.19.0: also sign with the previous secret (rotation) as `v1old`. */\n options: { previousSecret?: string } = {}\n): { header: string; timestamp: number } {\n const timestamp = Math.floor(nowMs / 1000);\n const v1 = computeHmac(secret, `${timestamp}.${rawBody}`);\n const old = options.previousSecret\n ? `,v1old=${computeHmac(options.previousSecret, `${timestamp}.${rawBody}`)}`\n : '';\n return { header: `t=${timestamp},v1=${v1}${old}`, timestamp };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrasyncai/verification-gateway",
3
- "version": "5.18.1",
3
+ "version": "5.20.0",
4
4
  "description": "AstraSync KYA Platform SDK — counterparty verification gateway (verify incoming requests) + agent registration (register AI agents with the KYA backend).",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",