@kirimdev/sdk 2.0.0 → 3.0.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/webhooks.js CHANGED
@@ -1,64 +1,125 @@
1
1
  /**
2
2
  * Webhook HMAC verification + typed event payloads.
3
3
  *
4
- * The Kirim API signs outbound webhook deliveries with HMAC-SHA256 over the
5
- * raw request body, encoded as `sha256=<hex>` in the `X-Kirim-Signature`
6
- * header. This module exposes a constant-time verifier using Web Crypto
7
- * (`crypto.subtle`) so it runs on Node 18+, Bun, Deno, and edge runtimes.
4
+ * Kirim signs outbound webhook deliveries using a Stripe-style header
5
+ * format mirroring `apps/worker/src/jobs/webhook-delivery/sign.ts`:
8
6
  *
9
- * The signing scheme mirrors `apps/api/src/lib/webhook-security.ts`.
7
+ * X-Kirim-Signature: t=<unix_seconds>,v1=<hex>[,v1=<hex>...]
10
8
  *
11
- * Usage:
12
- * import { verifyWebhookSignature } from '@kirimdev/sdk/webhooks'
9
+ * - `t` is the time the signature was computed; callers reject when
10
+ * |now - t| exceeds `toleranceSeconds` (default 5 minutes).
11
+ * - Each `v1=` is HMAC-SHA256 of `${t}.${rawBody}` under one ACTIVE
12
+ * signing secret. During rotation a subscription may have multiple
13
+ * active secrets and the header carries one `v1=` per secret —
14
+ * verification passes if ANY caller-supplied secret matches ANY
15
+ * `v1=` value.
13
16
  *
14
- * const ok = await verifyWebhookSignature(
15
- * await req.text(),
16
- * req.headers.get('x-kirim-signature') ?? '',
17
- * process.env.KIRIM_WEBHOOK_SECRET!,
18
- * )
19
- * if (!ok) return new Response('bad signature', { status: 401 })
20
- */
21
- /**
22
- * Verify a Kirim webhook signature.
17
+ * Uses Web Crypto (`crypto.subtle`) so this module runs unchanged on
18
+ * Node 18+, Bun, Deno, and edge runtimes.
23
19
  *
24
- * MUST be called with the raw request body (the same bytes Kirim signed —
25
- * re-serializing JSON will break the signature). Returns `false` (never
26
- * throws) when the header is absent / malformed / mismatched. The caller
27
- * MUST reject the request when this returns `false` (fail-closed).
20
+ * The error classes in this module (`KirimWebhookError`,
21
+ * `InvalidSignatureError`, `SignatureExpiredError`,
22
+ * `MalformedPayloadError`) form a SEPARATE hierarchy from the HTTP
23
+ * `KirimError` tree in `@kirimdev/sdk` webhook errors carry no
24
+ * `requestId` / `status` / `code` because they fire before any HTTP
25
+ * round-trip. Consumers wanting a single catch surface should branch
26
+ * on `instanceof KirimWebhookError` separately from `instanceof
27
+ * KirimError`.
28
28
  */
29
- export async function verifyWebhookSignature(body, signatureHeader, secret) {
30
- if (!signatureHeader || !secret)
31
- return false;
32
- const provided = signatureHeader.startsWith('sha256=')
33
- ? signatureHeader.slice('sha256='.length)
34
- : signatureHeader;
35
- const expected = await hmacSha256Hex(secret, body);
36
- // Length comparison short-circuits *before* the timing-safe step. That's
37
- // safe because byte length is not a secret (HMAC-SHA256 hex is always 64
38
- // chars); leaking it gives the attacker nothing.
39
- if (expected.length !== provided.length)
40
- return false;
41
- return timingSafeEqualHex(expected, provided);
29
+ export class KirimWebhookError extends Error {
30
+ constructor(message) {
31
+ super(message);
32
+ this.name = 'KirimWebhookError';
33
+ }
34
+ }
35
+ export class InvalidSignatureError extends KirimWebhookError {
36
+ constructor(message = 'Invalid webhook signature') {
37
+ super(message);
38
+ this.name = 'InvalidSignatureError';
39
+ }
40
+ }
41
+ export class SignatureExpiredError extends KirimWebhookError {
42
+ constructor(message = 'Webhook signature timestamp outside tolerance window') {
43
+ super(message);
44
+ this.name = 'SignatureExpiredError';
45
+ }
46
+ }
47
+ export class MalformedPayloadError extends KirimWebhookError {
48
+ constructor(message = 'Webhook payload is not valid JSON') {
49
+ super(message);
50
+ this.name = 'MalformedPayloadError';
51
+ }
52
+ }
53
+ function parseHeader(header) {
54
+ const parts = header.split(',').map((s) => s.trim()).filter(Boolean);
55
+ let t = null;
56
+ const v1 = [];
57
+ for (const part of parts) {
58
+ const eq = part.indexOf('=');
59
+ if (eq <= 0)
60
+ return null;
61
+ const k = part.slice(0, eq);
62
+ const v = part.slice(eq + 1);
63
+ if (k === 't') {
64
+ const n = Number(v);
65
+ if (!Number.isFinite(n) || n <= 0)
66
+ return null;
67
+ t = n;
68
+ }
69
+ else if (k === 'v1') {
70
+ if (!/^[0-9a-f]+$/i.test(v))
71
+ return null;
72
+ v1.push(v.toLowerCase());
73
+ }
74
+ // Unknown scheme tokens (future-proof: e.g. v2=) are ignored.
75
+ }
76
+ if (t === null || v1.length === 0)
77
+ return null;
78
+ return { t, v1 };
79
+ }
80
+ export async function verifyWebhookSignature(opts) {
81
+ const tolerance = opts.toleranceSeconds ?? 300;
82
+ const now = (opts.nowSeconds ?? (() => Math.floor(Date.now() / 1000)))();
83
+ if (!opts.signatureHeader || opts.secrets.length === 0) {
84
+ throw new InvalidSignatureError('Missing signature header or secrets');
85
+ }
86
+ const parsed = parseHeader(opts.signatureHeader);
87
+ if (!parsed)
88
+ throw new InvalidSignatureError('Malformed signature header');
89
+ if (Math.abs(now - parsed.t) > tolerance) {
90
+ throw new SignatureExpiredError();
91
+ }
92
+ const signedString = `${parsed.t}.${opts.rawBody}`;
93
+ for (const secret of opts.secrets) {
94
+ const expected = await hmacSha256Hex(secret, signedString);
95
+ for (const candidate of parsed.v1) {
96
+ if (expected.length === candidate.length && timingSafeEqualHex(expected, candidate)) {
97
+ try {
98
+ return JSON.parse(opts.rawBody);
99
+ }
100
+ catch {
101
+ throw new MalformedPayloadError();
102
+ }
103
+ }
104
+ }
105
+ }
106
+ throw new InvalidSignatureError('No v1 signature matched any provided secret');
42
107
  }
43
108
  async function hmacSha256Hex(secret, body) {
44
109
  const enc = new TextEncoder();
45
110
  const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
46
111
  const buf = await crypto.subtle.sign('HMAC', key, enc.encode(body));
47
- return bufferToHex(new Uint8Array(buf));
48
- }
49
- function bufferToHex(buf) {
50
112
  let out = '';
51
- for (let i = 0; i < buf.length; i++) {
52
- const byte = buf[i] ?? 0;
53
- out += byte.toString(16).padStart(2, '0');
113
+ const view = new Uint8Array(buf);
114
+ for (let i = 0; i < view.length; i++) {
115
+ out += (view[i] ?? 0).toString(16).padStart(2, '0');
54
116
  }
55
117
  return out;
56
118
  }
57
- /** Constant-time string comparison. Inputs MUST be equal length. */
58
119
  function timingSafeEqualHex(a, b) {
59
120
  let diff = 0;
60
121
  for (let i = 0; i < a.length; i++) {
61
- diff |= (a.charCodeAt(i) ^ b.charCodeAt(i));
122
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
62
123
  }
63
124
  return diff === 0;
64
125
  }
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,IAAY,EACZ,eAA0C,EAC1C,MAAc;IAEd,IAAI,CAAC,eAAe,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC7C,MAAM,QAAQ,GAAG,eAAe,CAAC,UAAU,CAAC,SAAS,CAAC;QACpD,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC;QACzC,CAAC,CAAC,eAAe,CAAA;IAEnB,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAClD,yEAAyE;IACzE,yEAAyE;IACzE,iDAAiD;IACjD,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACrD,OAAO,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAC/C,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,MAAc,EAAE,IAAY;IACvD,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAA;IAC7B,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAClB,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,EACjC,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAA;IACD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACnE,OAAO,WAAW,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;AACzC,CAAC;AAED,SAAS,WAAW,CAAC,GAAe;IAClC,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACxB,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,oEAAoE;AACpE,SAAS,kBAAkB,CAAC,CAAS,EAAE,CAAS;IAC9C,IAAI,IAAI,GAAG,CAAC,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAA;AACnB,CAAC"}
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"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"}