@cstar.help/js 0.14.0 → 0.15.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.
@@ -35,7 +35,7 @@ function normalizeArgs(messageOrStatus, maybeBody, defaults) {
35
35
  }
36
36
  };
37
37
  }
38
- var CStarError = class extends Error {
38
+ var CStarErrorImpl = class extends Error {
39
39
  constructor(messageOrStatus, body) {
40
40
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body);
41
41
  super(resolved.message);
@@ -48,7 +48,7 @@ var CStarError = class extends Error {
48
48
  this.statusCode = statusCode;
49
49
  }
50
50
  };
51
- var CStarAuthenticationError = class extends CStarError {
51
+ var CStarAuthenticationErrorImpl = class extends CStarErrorImpl {
52
52
  constructor(messageOrStatus, body) {
53
53
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
54
54
  type: ErrorType.AUTHENTICATION_ERROR,
@@ -59,7 +59,7 @@ var CStarAuthenticationError = class extends CStarError {
59
59
  this.name = "CStarAuthenticationError";
60
60
  }
61
61
  };
62
- var CStarRateLimitError = class extends CStarError {
62
+ var CStarRateLimitErrorImpl = class extends CStarErrorImpl {
63
63
  constructor(messageOrStatus, body, retryAfter) {
64
64
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
65
65
  type: ErrorType.RATE_LIMIT_ERROR,
@@ -71,7 +71,7 @@ var CStarRateLimitError = class extends CStarError {
71
71
  this.retryAfter = retryAfter ?? 60;
72
72
  }
73
73
  };
74
- var CStarNotFoundError = class extends CStarError {
74
+ var CStarNotFoundErrorImpl = class extends CStarErrorImpl {
75
75
  constructor(messageOrStatus, body) {
76
76
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
77
77
  type: ErrorType.NOT_FOUND_ERROR,
@@ -82,7 +82,7 @@ var CStarNotFoundError = class extends CStarError {
82
82
  this.name = "CStarNotFoundError";
83
83
  }
84
84
  };
85
- var CStarValidationError = class extends CStarError {
85
+ var CStarValidationErrorImpl = class extends CStarErrorImpl {
86
86
  constructor(messageOrStatus, body) {
87
87
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
88
88
  type: ErrorType.INVALID_REQUEST_ERROR,
@@ -93,7 +93,7 @@ var CStarValidationError = class extends CStarError {
93
93
  this.name = "CStarValidationError";
94
94
  }
95
95
  };
96
- var CStarPermissionError = class extends CStarError {
96
+ var CStarPermissionErrorImpl = class extends CStarErrorImpl {
97
97
  constructor(messageOrStatus, body) {
98
98
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
99
99
  type: ErrorType.PERMISSION_ERROR,
@@ -104,7 +104,7 @@ var CStarPermissionError = class extends CStarError {
104
104
  this.name = "CStarPermissionError";
105
105
  }
106
106
  };
107
- var CStarConflictError = class extends CStarError {
107
+ var CStarConflictErrorImpl = class extends CStarErrorImpl {
108
108
  constructor(messageOrStatus, body) {
109
109
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
110
110
  type: ErrorType.CONFLICT_ERROR,
@@ -115,7 +115,7 @@ var CStarConflictError = class extends CStarError {
115
115
  this.name = "CStarConflictError";
116
116
  }
117
117
  };
118
- var CStarServerError = class extends CStarError {
118
+ var CStarServerErrorImpl = class extends CStarErrorImpl {
119
119
  constructor(messageOrStatus, body) {
120
120
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
121
121
  type: ErrorType.SERVER_ERROR,
@@ -126,7 +126,7 @@ var CStarServerError = class extends CStarError {
126
126
  this.name = "CStarServerError";
127
127
  }
128
128
  };
129
- var CStarFeatureDisabledError = class extends CStarError {
129
+ var CStarFeatureDisabledErrorImpl = class extends CStarErrorImpl {
130
130
  constructor(messageOrStatus, body) {
131
131
  const { statusCode, body: resolved } = normalizeArgs(messageOrStatus, body, {
132
132
  type: ErrorType.FEATURE_NOT_CONFIGURED,
@@ -139,6 +139,82 @@ var CStarFeatureDisabledError = class extends CStarError {
139
139
  this.enableUrl = resolved.enable_url ?? "";
140
140
  }
141
141
  };
142
+ var CStarRealtimeUnavailableErrorImpl = class extends CStarErrorImpl {
143
+ constructor(message) {
144
+ const resolved = message ?? "EventSource is not available in this environment. Real-time subscriptions only work in the browser. Move the call to a client-only path (`useEffect` / `onMount`), or pass an `eventSource` polyfill via the realtime options. See https://cstar.help/developers/sdk/realtime#ssr";
145
+ super(0, {
146
+ type: ErrorType.API_ERROR,
147
+ code: "realtime_unavailable",
148
+ message: resolved,
149
+ doc_url: "https://cstar.help/developers/sdk/realtime#ssr"
150
+ });
151
+ this.name = "CStarRealtimeUnavailableError";
152
+ }
153
+ };
154
+ var REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@cstar.help/js/errors/v1");
155
+ var globalSlot = globalThis;
156
+ var registry = globalSlot[REGISTRY_KEY] ?? (globalSlot[REGISTRY_KEY] = {
157
+ CStarError: CStarErrorImpl,
158
+ CStarAuthenticationError: CStarAuthenticationErrorImpl,
159
+ CStarRateLimitError: CStarRateLimitErrorImpl,
160
+ CStarNotFoundError: CStarNotFoundErrorImpl,
161
+ CStarValidationError: CStarValidationErrorImpl,
162
+ CStarPermissionError: CStarPermissionErrorImpl,
163
+ CStarConflictError: CStarConflictErrorImpl,
164
+ CStarServerError: CStarServerErrorImpl,
165
+ CStarFeatureDisabledError: CStarFeatureDisabledErrorImpl,
166
+ CStarRealtimeUnavailableError: CStarRealtimeUnavailableErrorImpl
167
+ });
168
+ var CStarError = registry.CStarError;
169
+ var CStarAuthenticationError = registry.CStarAuthenticationError;
170
+ var CStarRateLimitError = registry.CStarRateLimitError;
171
+ var CStarNotFoundError = registry.CStarNotFoundError;
172
+ var CStarValidationError = registry.CStarValidationError;
173
+ var CStarPermissionError = registry.CStarPermissionError;
174
+ var CStarConflictError = registry.CStarConflictError;
175
+ var CStarServerError = registry.CStarServerError;
176
+ var CStarFeatureDisabledError = registry.CStarFeatureDisabledError;
177
+ var CStarRealtimeUnavailableError = registry.CStarRealtimeUnavailableError;
178
+ function parsePublicError(status, body, fallbackMessage) {
179
+ const obj = body && typeof body === "object" ? body : null;
180
+ const errVal = obj?.error;
181
+ let message = fallbackMessage;
182
+ let structured = null;
183
+ if (typeof errVal === "string") {
184
+ message = errVal;
185
+ } else if (errVal && typeof errVal === "object") {
186
+ const e = errVal;
187
+ if (typeof e.message === "string") message = e.message;
188
+ structured = {
189
+ type: typeof e.type === "string" ? e.type : ErrorType.API_ERROR,
190
+ code: typeof e.code === "string" ? e.code : "api_error",
191
+ message,
192
+ param: typeof e.param === "string" ? e.param : void 0,
193
+ doc_url: typeof e.doc_url === "string" ? e.doc_url : DEFAULT_DOC_URL,
194
+ request_id: typeof e.request_id === "string" ? e.request_id : void 0,
195
+ feature: typeof e.feature === "string" ? e.feature : void 0,
196
+ enable_url: typeof e.enable_url === "string" ? e.enable_url : void 0
197
+ };
198
+ } else if (typeof obj?.message === "string") {
199
+ message = obj.message;
200
+ }
201
+ const errorBody = structured ?? {
202
+ type: ErrorType.API_ERROR,
203
+ code: status >= 500 ? "server_error" : "request_failed",
204
+ message,
205
+ doc_url: DEFAULT_DOC_URL
206
+ };
207
+ return parseError(status, { error: errorBody });
208
+ }
209
+ var STATUS_TO_CLASS = {
210
+ 400: CStarValidationError,
211
+ 401: CStarAuthenticationError,
212
+ 403: CStarPermissionError,
213
+ 404: CStarNotFoundError,
214
+ 409: CStarConflictError,
215
+ 422: CStarValidationError,
216
+ 429: CStarRateLimitError
217
+ };
142
218
  var TYPE_TO_CLASS = {
143
219
  [ErrorType.AUTHENTICATION_ERROR]: CStarAuthenticationError,
144
220
  [ErrorType.PERMISSION_ERROR]: CStarPermissionError,
@@ -149,6 +225,27 @@ var TYPE_TO_CLASS = {
149
225
  [ErrorType.SERVER_ERROR]: CStarServerError,
150
226
  [ErrorType.FEATURE_NOT_CONFIGURED]: CStarFeatureDisabledError
151
227
  };
228
+ function parseError(statusCode, body, retryAfter) {
229
+ const errorBody = body?.error ?? {
230
+ type: ErrorType.API_ERROR,
231
+ code: "unknown_error",
232
+ message: `Request failed with status ${statusCode}`,
233
+ doc_url: DEFAULT_DOC_URL
234
+ };
235
+ if (statusCode === 429) {
236
+ return new CStarRateLimitError(statusCode, errorBody, retryAfter ?? 60);
237
+ }
238
+ const StatusCls = STATUS_TO_CLASS[statusCode];
239
+ if (StatusCls) {
240
+ return new StatusCls(statusCode, errorBody);
241
+ }
242
+ const TypeCls = TYPE_TO_CLASS[errorBody.type];
243
+ if (TypeCls) {
244
+ return new TypeCls(statusCode, errorBody);
245
+ }
246
+ if (statusCode >= 500) return new CStarServerError(statusCode, errorBody);
247
+ return new CStarError(statusCode, errorBody);
248
+ }
152
249
 
153
250
  // src/quickhelp/index.ts
154
251
  var DEFAULT_BASE_URL = "https://www.cstar.help";
@@ -282,6 +379,19 @@ function parseRetryAfter(header) {
282
379
  return Number.isFinite(n) && n > 0 ? n : void 0;
283
380
  }
284
381
  export {
382
+ CStarAuthenticationError,
383
+ CStarConflictError,
384
+ CStarError,
385
+ CStarFeatureDisabledError,
386
+ CStarNotFoundError,
387
+ CStarPermissionError,
388
+ CStarRateLimitError,
389
+ CStarRealtimeUnavailableError,
390
+ CStarServerError,
391
+ CStarValidationError,
392
+ ErrorType,
285
393
  QuickHelpClient,
286
- QuickHelpError
394
+ QuickHelpError,
395
+ parseError,
396
+ parsePublicError
287
397
  };
@@ -20,19 +20,55 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/webhook/index.ts
21
21
  var webhook_exports = {};
22
22
  __export(webhook_exports, {
23
+ DEFAULT_TOLERANCE_SECONDS: () => DEFAULT_TOLERANCE_SECONDS,
23
24
  WebhookSignatureVerificationError: () => WebhookSignatureVerificationError,
24
25
  constructEventAsync: () => constructEventAsync,
26
+ signPayloadStripeAsync: () => signPayloadStripeAsync,
25
27
  verifySignatureAsync: () => verifySignatureAsync
26
28
  });
27
29
  module.exports = __toCommonJS(webhook_exports);
30
+ var DEFAULT_TOLERANCE_SECONDS = 300;
28
31
  var WebhookSignatureVerificationError = class extends Error {
29
32
  constructor(message) {
30
33
  super(message);
31
34
  this.name = "WebhookSignatureVerificationError";
32
35
  }
33
36
  };
34
- async function constructEventAsync(rawBody, signature, secret) {
35
- const valid = await verifySignatureAsync(rawBody, signature, secret);
37
+ function parseStripeHeader(header) {
38
+ if (!header || header.startsWith("sha256=")) return null;
39
+ let timestamp = null;
40
+ const v1 = [];
41
+ for (const part of header.split(",")) {
42
+ const eq = part.indexOf("=");
43
+ if (eq === -1) continue;
44
+ const key = part.slice(0, eq).trim();
45
+ const value = part.slice(eq + 1).trim();
46
+ if (key === "t") {
47
+ const parsed = Number.parseInt(value, 10);
48
+ if (Number.isFinite(parsed)) timestamp = parsed;
49
+ } else if (key === "v1") {
50
+ v1.push(value);
51
+ }
52
+ }
53
+ if (timestamp === null || v1.length === 0) return null;
54
+ return { timestamp, v1 };
55
+ }
56
+ async function signPayloadStripeAsync(rawBody, secret) {
57
+ const ts = Math.floor(Date.now() / 1e3);
58
+ const encoder = new TextEncoder();
59
+ const key = await crypto.subtle.importKey(
60
+ "raw",
61
+ encoder.encode(secret),
62
+ { name: "HMAC", hash: "SHA-256" },
63
+ false,
64
+ ["sign"]
65
+ );
66
+ const sigBytes = await crypto.subtle.sign("HMAC", key, encoder.encode(`${ts}.${rawBody}`));
67
+ const hex = Array.from(new Uint8Array(sigBytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
68
+ return `t=${ts},v1=${hex}`;
69
+ }
70
+ async function constructEventAsync(rawBody, signature, secret, options) {
71
+ const valid = await verifySignatureAsync(rawBody, signature, secret, options);
36
72
  if (!valid) {
37
73
  throw new WebhookSignatureVerificationError(
38
74
  "Webhook signature verification failed. Ensure you are using the raw request body and the correct endpoint secret."
@@ -40,7 +76,7 @@ async function constructEventAsync(rawBody, signature, secret) {
40
76
  }
41
77
  return JSON.parse(rawBody);
42
78
  }
43
- async function verifySignatureAsync(rawBody, signature, secret) {
79
+ async function hmacHex(material, secret) {
44
80
  const encoder = new TextEncoder();
45
81
  const key = await crypto.subtle.importKey(
46
82
  "raw",
@@ -49,18 +85,38 @@ async function verifySignatureAsync(rawBody, signature, secret) {
49
85
  false,
50
86
  ["sign"]
51
87
  );
52
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(rawBody));
53
- const expected = "sha256=" + Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
54
- if (signature.length !== expected.length) return false;
88
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(material));
89
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
90
+ }
91
+ function constantTimeEqual(a, b) {
92
+ if (a.length !== b.length) return false;
55
93
  let result = 0;
56
- for (let i = 0; i < signature.length; i++) {
57
- result |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
94
+ for (let i = 0; i < a.length; i++) {
95
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
58
96
  }
59
97
  return result === 0;
60
98
  }
99
+ async function verifySignatureAsync(rawBody, signature, secret, options) {
100
+ if (typeof signature !== "string" || signature.length === 0) return false;
101
+ const stripe = parseStripeHeader(signature);
102
+ if (stripe) {
103
+ const tolerance = options?.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
104
+ const now = Math.floor(Date.now() / 1e3);
105
+ if (Math.abs(now - stripe.timestamp) > tolerance) return false;
106
+ const expected2 = await hmacHex(`${stripe.timestamp}.${rawBody}`, secret);
107
+ for (const candidate of stripe.v1) {
108
+ if (constantTimeEqual(candidate, expected2)) return true;
109
+ }
110
+ return false;
111
+ }
112
+ const expected = "sha256=" + await hmacHex(rawBody, secret);
113
+ return constantTimeEqual(signature, expected);
114
+ }
61
115
  // Annotate the CommonJS export names for ESM import in node:
62
116
  0 && (module.exports = {
117
+ DEFAULT_TOLERANCE_SECONDS,
63
118
  WebhookSignatureVerificationError,
64
119
  constructEventAsync,
120
+ signPayloadStripeAsync,
65
121
  verifySignatureAsync
66
122
  });
@@ -412,10 +412,27 @@ interface WebhookEvent<T extends WebhookEventType = WebhookEventType> {
412
412
  data: WebhookEventDataMap[T];
413
413
  }
414
414
 
415
+ /** Default replay window for Stripe-style signatures: 5 minutes (300s). */
416
+ declare const DEFAULT_TOLERANCE_SECONDS = 300;
417
+ /** Options for signature verification — accepted by both verify and construct calls. */
418
+ interface VerifyOptions {
419
+ /**
420
+ * Maximum age (in seconds) of a Stripe-style timestamp before the signature
421
+ * is rejected as expired. Defaults to {@link DEFAULT_TOLERANCE_SECONDS}.
422
+ * Ignored for legacy `sha256=<hex>` signatures (which carry no timestamp).
423
+ */
424
+ tolerance?: number;
425
+ }
415
426
  /** Thrown when webhook signature verification fails. */
416
427
  declare class WebhookSignatureVerificationError extends Error {
417
428
  constructor(message: string);
418
429
  }
430
+ /**
431
+ * Sign a payload in Stripe-style format `t=<unix>,v1=<hex>` using Web Crypto.
432
+ * The HMAC is computed over `${ts}.${rawBody}`. Used by the cStar server
433
+ * dispatcher and `cstar listen --forward-to`.
434
+ */
435
+ declare function signPayloadStripeAsync(rawBody: string, secret: string): Promise<string>;
419
436
  /**
420
437
  * Verify a webhook signature and parse the event.
421
438
  *
@@ -427,14 +444,21 @@ declare class WebhookSignatureVerificationError extends Error {
427
444
  * @param rawBody - Raw request body as a string (NOT parsed JSON)
428
445
  * @param signature - Value of the `X-Signature` header
429
446
  * @param secret - Your webhook endpoint's secret (`whsec_*`)
447
+ * @param options - Optional verifier options (e.g. replay tolerance)
430
448
  * @returns Parsed and verified webhook event
431
449
  * @throws {WebhookSignatureVerificationError} If verification fails
432
450
  */
433
- declare function constructEventAsync(rawBody: string, signature: string, secret: string): Promise<WebhookEvent>;
451
+ declare function constructEventAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<WebhookEvent>;
434
452
  /**
435
453
  * Verify an HMAC-SHA256 signature using the Web Crypto API.
454
+ *
455
+ * Accepts both the canonical Stripe-style format `t=<unix>,v1=<hex>` (signed
456
+ * over `${ts}.${rawBody}` with replay-window enforcement) and the legacy
457
+ * `sha256=<hex>` format (signed over `rawBody` only, no timestamp). Use
458
+ * `options.tolerance` to override the default 300s replay window.
459
+ *
436
460
  * Constant-time comparison to prevent timing attacks.
437
461
  */
438
- declare function verifySignatureAsync(rawBody: string, signature: string, secret: string): Promise<boolean>;
462
+ declare function verifySignatureAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<boolean>;
439
463
 
440
- export { type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, verifySignatureAsync };
464
+ export { DEFAULT_TOLERANCE_SECONDS, type VerifyOptions, type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, signPayloadStripeAsync, verifySignatureAsync };
@@ -412,10 +412,27 @@ interface WebhookEvent<T extends WebhookEventType = WebhookEventType> {
412
412
  data: WebhookEventDataMap[T];
413
413
  }
414
414
 
415
+ /** Default replay window for Stripe-style signatures: 5 minutes (300s). */
416
+ declare const DEFAULT_TOLERANCE_SECONDS = 300;
417
+ /** Options for signature verification — accepted by both verify and construct calls. */
418
+ interface VerifyOptions {
419
+ /**
420
+ * Maximum age (in seconds) of a Stripe-style timestamp before the signature
421
+ * is rejected as expired. Defaults to {@link DEFAULT_TOLERANCE_SECONDS}.
422
+ * Ignored for legacy `sha256=<hex>` signatures (which carry no timestamp).
423
+ */
424
+ tolerance?: number;
425
+ }
415
426
  /** Thrown when webhook signature verification fails. */
416
427
  declare class WebhookSignatureVerificationError extends Error {
417
428
  constructor(message: string);
418
429
  }
430
+ /**
431
+ * Sign a payload in Stripe-style format `t=<unix>,v1=<hex>` using Web Crypto.
432
+ * The HMAC is computed over `${ts}.${rawBody}`. Used by the cStar server
433
+ * dispatcher and `cstar listen --forward-to`.
434
+ */
435
+ declare function signPayloadStripeAsync(rawBody: string, secret: string): Promise<string>;
419
436
  /**
420
437
  * Verify a webhook signature and parse the event.
421
438
  *
@@ -427,14 +444,21 @@ declare class WebhookSignatureVerificationError extends Error {
427
444
  * @param rawBody - Raw request body as a string (NOT parsed JSON)
428
445
  * @param signature - Value of the `X-Signature` header
429
446
  * @param secret - Your webhook endpoint's secret (`whsec_*`)
447
+ * @param options - Optional verifier options (e.g. replay tolerance)
430
448
  * @returns Parsed and verified webhook event
431
449
  * @throws {WebhookSignatureVerificationError} If verification fails
432
450
  */
433
- declare function constructEventAsync(rawBody: string, signature: string, secret: string): Promise<WebhookEvent>;
451
+ declare function constructEventAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<WebhookEvent>;
434
452
  /**
435
453
  * Verify an HMAC-SHA256 signature using the Web Crypto API.
454
+ *
455
+ * Accepts both the canonical Stripe-style format `t=<unix>,v1=<hex>` (signed
456
+ * over `${ts}.${rawBody}` with replay-window enforcement) and the legacy
457
+ * `sha256=<hex>` format (signed over `rawBody` only, no timestamp). Use
458
+ * `options.tolerance` to override the default 300s replay window.
459
+ *
436
460
  * Constant-time comparison to prevent timing attacks.
437
461
  */
438
- declare function verifySignatureAsync(rawBody: string, signature: string, secret: string): Promise<boolean>;
462
+ declare function verifySignatureAsync(rawBody: string, signature: string, secret: string, options?: VerifyOptions): Promise<boolean>;
439
463
 
440
- export { type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, verifySignatureAsync };
464
+ export { DEFAULT_TOLERANCE_SECONDS, type VerifyOptions, type WebhookEvent, WebhookSignatureVerificationError, constructEventAsync, signPayloadStripeAsync, verifySignatureAsync };
@@ -1,12 +1,46 @@
1
1
  // src/webhook/index.ts
2
+ var DEFAULT_TOLERANCE_SECONDS = 300;
2
3
  var WebhookSignatureVerificationError = class extends Error {
3
4
  constructor(message) {
4
5
  super(message);
5
6
  this.name = "WebhookSignatureVerificationError";
6
7
  }
7
8
  };
8
- async function constructEventAsync(rawBody, signature, secret) {
9
- const valid = await verifySignatureAsync(rawBody, signature, secret);
9
+ function parseStripeHeader(header) {
10
+ if (!header || header.startsWith("sha256=")) return null;
11
+ let timestamp = null;
12
+ const v1 = [];
13
+ for (const part of header.split(",")) {
14
+ const eq = part.indexOf("=");
15
+ if (eq === -1) continue;
16
+ const key = part.slice(0, eq).trim();
17
+ const value = part.slice(eq + 1).trim();
18
+ if (key === "t") {
19
+ const parsed = Number.parseInt(value, 10);
20
+ if (Number.isFinite(parsed)) timestamp = parsed;
21
+ } else if (key === "v1") {
22
+ v1.push(value);
23
+ }
24
+ }
25
+ if (timestamp === null || v1.length === 0) return null;
26
+ return { timestamp, v1 };
27
+ }
28
+ async function signPayloadStripeAsync(rawBody, secret) {
29
+ const ts = Math.floor(Date.now() / 1e3);
30
+ const encoder = new TextEncoder();
31
+ const key = await crypto.subtle.importKey(
32
+ "raw",
33
+ encoder.encode(secret),
34
+ { name: "HMAC", hash: "SHA-256" },
35
+ false,
36
+ ["sign"]
37
+ );
38
+ const sigBytes = await crypto.subtle.sign("HMAC", key, encoder.encode(`${ts}.${rawBody}`));
39
+ const hex = Array.from(new Uint8Array(sigBytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
40
+ return `t=${ts},v1=${hex}`;
41
+ }
42
+ async function constructEventAsync(rawBody, signature, secret, options) {
43
+ const valid = await verifySignatureAsync(rawBody, signature, secret, options);
10
44
  if (!valid) {
11
45
  throw new WebhookSignatureVerificationError(
12
46
  "Webhook signature verification failed. Ensure you are using the raw request body and the correct endpoint secret."
@@ -14,7 +48,7 @@ async function constructEventAsync(rawBody, signature, secret) {
14
48
  }
15
49
  return JSON.parse(rawBody);
16
50
  }
17
- async function verifySignatureAsync(rawBody, signature, secret) {
51
+ async function hmacHex(material, secret) {
18
52
  const encoder = new TextEncoder();
19
53
  const key = await crypto.subtle.importKey(
20
54
  "raw",
@@ -23,17 +57,37 @@ async function verifySignatureAsync(rawBody, signature, secret) {
23
57
  false,
24
58
  ["sign"]
25
59
  );
26
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(rawBody));
27
- const expected = "sha256=" + Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
28
- if (signature.length !== expected.length) return false;
60
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(material));
61
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
62
+ }
63
+ function constantTimeEqual(a, b) {
64
+ if (a.length !== b.length) return false;
29
65
  let result = 0;
30
- for (let i = 0; i < signature.length; i++) {
31
- result |= signature.charCodeAt(i) ^ expected.charCodeAt(i);
66
+ for (let i = 0; i < a.length; i++) {
67
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
32
68
  }
33
69
  return result === 0;
34
70
  }
71
+ async function verifySignatureAsync(rawBody, signature, secret, options) {
72
+ if (typeof signature !== "string" || signature.length === 0) return false;
73
+ const stripe = parseStripeHeader(signature);
74
+ if (stripe) {
75
+ const tolerance = options?.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
76
+ const now = Math.floor(Date.now() / 1e3);
77
+ if (Math.abs(now - stripe.timestamp) > tolerance) return false;
78
+ const expected2 = await hmacHex(`${stripe.timestamp}.${rawBody}`, secret);
79
+ for (const candidate of stripe.v1) {
80
+ if (constantTimeEqual(candidate, expected2)) return true;
81
+ }
82
+ return false;
83
+ }
84
+ const expected = "sha256=" + await hmacHex(rawBody, secret);
85
+ return constantTimeEqual(signature, expected);
86
+ }
35
87
  export {
88
+ DEFAULT_TOLERANCE_SECONDS,
36
89
  WebhookSignatureVerificationError,
37
90
  constructEventAsync,
91
+ signPayloadStripeAsync,
38
92
  verifySignatureAsync
39
93
  };
@@ -20,14 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/webhook/node.ts
21
21
  var node_exports = {};
22
22
  __export(node_exports, {
23
+ DEFAULT_TOLERANCE_SECONDS: () => DEFAULT_TOLERANCE_SECONDS,
23
24
  WebhookSignatureVerificationError: () => WebhookSignatureVerificationError,
24
25
  constructEvent: () => constructEvent,
26
+ signPayloadStripe: () => signPayloadStripe,
25
27
  verifySignature: () => verifySignature
26
28
  });
27
29
  module.exports = __toCommonJS(node_exports);
28
30
  var import_node_crypto = require("crypto");
29
31
 
30
32
  // src/webhook/index.ts
33
+ var DEFAULT_TOLERANCE_SECONDS = 300;
31
34
  var WebhookSignatureVerificationError = class extends Error {
32
35
  constructor(message) {
33
36
  super(message);
@@ -36,22 +39,67 @@ var WebhookSignatureVerificationError = class extends Error {
36
39
  };
37
40
 
38
41
  // src/webhook/node.ts
39
- function constructEvent(rawBody, signature, secret) {
40
- if (!verifySignature(rawBody, signature, secret)) {
42
+ function parseStripeHeader(header) {
43
+ if (!header || header.startsWith("sha256=")) return null;
44
+ let timestamp = null;
45
+ const v1 = [];
46
+ for (const part of header.split(",")) {
47
+ const eq = part.indexOf("=");
48
+ if (eq === -1) continue;
49
+ const key = part.slice(0, eq).trim();
50
+ const value = part.slice(eq + 1).trim();
51
+ if (key === "t") {
52
+ const parsed = Number.parseInt(value, 10);
53
+ if (Number.isFinite(parsed)) timestamp = parsed;
54
+ } else if (key === "v1") {
55
+ v1.push(value);
56
+ }
57
+ }
58
+ if (timestamp === null || v1.length === 0) return null;
59
+ return { timestamp, v1 };
60
+ }
61
+ function constantTimeEqualHex(a, b) {
62
+ if (a.length !== b.length) return false;
63
+ try {
64
+ return (0, import_node_crypto.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+ function signPayloadStripe(rawBody, secret) {
70
+ const ts = Math.floor(Date.now() / 1e3);
71
+ const sig = (0, import_node_crypto.createHmac)("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
72
+ return `t=${ts},v1=${sig}`;
73
+ }
74
+ function constructEvent(rawBody, signature, secret, options) {
75
+ if (!verifySignature(rawBody, signature, secret, options)) {
41
76
  throw new WebhookSignatureVerificationError(
42
77
  "Webhook signature verification failed. Ensure you are using the raw request body and the correct endpoint secret."
43
78
  );
44
79
  }
45
80
  return JSON.parse(rawBody);
46
81
  }
47
- function verifySignature(rawBody, signature, secret) {
82
+ function verifySignature(rawBody, signature, secret, options) {
83
+ if (typeof signature !== "string" || signature.length === 0) return false;
84
+ const stripe = parseStripeHeader(signature);
85
+ if (stripe) {
86
+ const tolerance = options?.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
87
+ const now = Math.floor(Date.now() / 1e3);
88
+ if (Math.abs(now - stripe.timestamp) > tolerance) return false;
89
+ const expected2 = (0, import_node_crypto.createHmac)("sha256", secret).update(`${stripe.timestamp}.${rawBody}`).digest("hex");
90
+ for (const candidate of stripe.v1) {
91
+ if (constantTimeEqualHex(candidate, expected2)) return true;
92
+ }
93
+ return false;
94
+ }
48
95
  const expected = "sha256=" + (0, import_node_crypto.createHmac)("sha256", secret).update(rawBody).digest("hex");
49
- if (signature.length !== expected.length) return false;
50
- return (0, import_node_crypto.timingSafeEqual)(Buffer.from(signature), Buffer.from(expected));
96
+ return constantTimeEqualHex(signature, expected);
51
97
  }
52
98
  // Annotate the CommonJS export names for ESM import in node:
53
99
  0 && (module.exports = {
100
+ DEFAULT_TOLERANCE_SECONDS,
54
101
  WebhookSignatureVerificationError,
55
102
  constructEvent,
103
+ signPayloadStripe,
56
104
  verifySignature
57
105
  });
@@ -412,11 +412,28 @@ interface WebhookEvent<T extends WebhookEventType = WebhookEventType> {
412
412
  data: WebhookEventDataMap[T];
413
413
  }
414
414
 
415
+ /** Default replay window for Stripe-style signatures: 5 minutes (300s). */
416
+ declare const DEFAULT_TOLERANCE_SECONDS = 300;
417
+ /** Options for signature verification — accepted by both verify and construct calls. */
418
+ interface VerifyOptions {
419
+ /**
420
+ * Maximum age (in seconds) of a Stripe-style timestamp before the signature
421
+ * is rejected as expired. Defaults to {@link DEFAULT_TOLERANCE_SECONDS}.
422
+ * Ignored for legacy `sha256=<hex>` signatures (which carry no timestamp).
423
+ */
424
+ tolerance?: number;
425
+ }
415
426
  /** Thrown when webhook signature verification fails. */
416
427
  declare class WebhookSignatureVerificationError extends Error {
417
428
  constructor(message: string);
418
429
  }
419
430
 
431
+ /**
432
+ * Sign a payload in Stripe-style format `t=<unix>,v1=<hex>` (Node-only,
433
+ * synchronous). Used by the cStar server-side webhook dispatcher and
434
+ * `cstar listen --forward-to`. The HMAC is computed over `${ts}.${rawBody}`.
435
+ */
436
+ declare function signPayloadStripe(rawBody: string, secret: string): string;
420
437
  /**
421
438
  * Verify a webhook signature and parse the event (synchronous, Node.js only).
422
439
  *
@@ -427,14 +444,20 @@ declare class WebhookSignatureVerificationError extends Error {
427
444
  * @param rawBody - Raw request body as a string (NOT parsed JSON)
428
445
  * @param signature - Value of the `X-Signature` header
429
446
  * @param secret - Your webhook endpoint's secret (`whsec_*`)
447
+ * @param options - Optional verifier options (e.g. replay tolerance)
430
448
  * @returns Parsed and verified webhook event
431
449
  * @throws {WebhookSignatureVerificationError} If verification fails
432
450
  */
433
- declare function constructEvent(rawBody: string, signature: string, secret: string): WebhookEvent;
451
+ declare function constructEvent(rawBody: string, signature: string, secret: string, options?: VerifyOptions): WebhookEvent;
434
452
  /**
435
453
  * Verify an HMAC-SHA256 signature (synchronous, Node.js only).
436
- * Uses timing-safe comparison to prevent timing attacks.
454
+ *
455
+ * Accepts both the canonical Stripe-style format `t=<unix>,v1=<hex>` (signed
456
+ * over `${ts}.${rawBody}`, with replay-window enforcement) and the legacy
457
+ * `sha256=<hex>` format (signed over `rawBody`, no timestamp). Use
458
+ * `options.tolerance` to override the default 300s replay window. Uses
459
+ * timing-safe comparison.
437
460
  */
438
- declare function verifySignature(rawBody: string, signature: string, secret: string): boolean;
461
+ declare function verifySignature(rawBody: string, signature: string, secret: string, options?: VerifyOptions): boolean;
439
462
 
440
- export { type WebhookEvent, WebhookSignatureVerificationError, constructEvent, verifySignature };
463
+ export { DEFAULT_TOLERANCE_SECONDS, type VerifyOptions, type WebhookEvent, WebhookSignatureVerificationError, constructEvent, signPayloadStripe, verifySignature };