@cstar.help/js 0.14.0 → 0.15.1
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/auth/index.cjs +93 -11
- package/dist/auth/index.d.cts +398 -1
- package/dist/auth/index.d.ts +398 -1
- package/dist/auth/index.js +79 -10
- package/dist/chat/index.cjs +30 -8
- package/dist/chat/index.d.cts +18 -15
- package/dist/chat/index.d.ts +18 -15
- package/dist/chat/index.js +30 -8
- package/dist/community/index.cjs +93 -11
- package/dist/community/index.d.cts +398 -1
- package/dist/community/index.d.ts +398 -1
- package/dist/community/index.js +79 -10
- package/dist/index.cjs +55 -11
- package/dist/index.d.cts +334 -53
- package/dist/index.d.ts +334 -53
- package/dist/index.js +55 -11
- package/dist/library/index.cjs +93 -11
- package/dist/library/index.d.cts +398 -1
- package/dist/library/index.d.ts +398 -1
- package/dist/library/index.js +79 -10
- package/dist/quickhelp/index.cjs +154 -11
- package/dist/quickhelp/index.d.cts +398 -1
- package/dist/quickhelp/index.d.ts +398 -1
- package/dist/quickhelp/index.js +140 -10
- package/dist/webhook/index.cjs +64 -8
- package/dist/webhook/index.d.cts +27 -3
- package/dist/webhook/index.d.ts +27 -3
- package/dist/webhook/index.js +62 -8
- package/dist/webhook/node.cjs +53 -5
- package/dist/webhook/node.d.cts +27 -4
- package/dist/webhook/node.d.ts +27 -4
- package/dist/webhook/node.js +51 -5
- package/package.json +1 -1
package/dist/webhook/index.cjs
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
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
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
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 <
|
|
57
|
-
result |=
|
|
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
|
});
|
package/dist/webhook/index.d.cts
CHANGED
|
@@ -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 };
|
package/dist/webhook/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/webhook/index.js
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
|
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(
|
|
27
|
-
|
|
28
|
-
|
|
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 <
|
|
31
|
-
result |=
|
|
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
|
};
|
package/dist/webhook/node.cjs
CHANGED
|
@@ -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
|
|
40
|
-
if (!
|
|
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
|
-
|
|
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
|
});
|
package/dist/webhook/node.d.cts
CHANGED
|
@@ -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
|
-
*
|
|
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 };
|
package/dist/webhook/node.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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 };
|
package/dist/webhook/node.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
3
3
|
|
|
4
4
|
// src/webhook/index.ts
|
|
5
|
+
var DEFAULT_TOLERANCE_SECONDS = 300;
|
|
5
6
|
var WebhookSignatureVerificationError = class extends Error {
|
|
6
7
|
constructor(message) {
|
|
7
8
|
super(message);
|
|
@@ -10,21 +11,66 @@ var WebhookSignatureVerificationError = class extends Error {
|
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
// src/webhook/node.ts
|
|
13
|
-
function
|
|
14
|
-
if (!
|
|
14
|
+
function parseStripeHeader(header) {
|
|
15
|
+
if (!header || header.startsWith("sha256=")) return null;
|
|
16
|
+
let timestamp = null;
|
|
17
|
+
const v1 = [];
|
|
18
|
+
for (const part of header.split(",")) {
|
|
19
|
+
const eq = part.indexOf("=");
|
|
20
|
+
if (eq === -1) continue;
|
|
21
|
+
const key = part.slice(0, eq).trim();
|
|
22
|
+
const value = part.slice(eq + 1).trim();
|
|
23
|
+
if (key === "t") {
|
|
24
|
+
const parsed = Number.parseInt(value, 10);
|
|
25
|
+
if (Number.isFinite(parsed)) timestamp = parsed;
|
|
26
|
+
} else if (key === "v1") {
|
|
27
|
+
v1.push(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (timestamp === null || v1.length === 0) return null;
|
|
31
|
+
return { timestamp, v1 };
|
|
32
|
+
}
|
|
33
|
+
function constantTimeEqualHex(a, b) {
|
|
34
|
+
if (a.length !== b.length) return false;
|
|
35
|
+
try {
|
|
36
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function signPayloadStripe(rawBody, secret) {
|
|
42
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
43
|
+
const sig = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
|
|
44
|
+
return `t=${ts},v1=${sig}`;
|
|
45
|
+
}
|
|
46
|
+
function constructEvent(rawBody, signature, secret, options) {
|
|
47
|
+
if (!verifySignature(rawBody, signature, secret, options)) {
|
|
15
48
|
throw new WebhookSignatureVerificationError(
|
|
16
49
|
"Webhook signature verification failed. Ensure you are using the raw request body and the correct endpoint secret."
|
|
17
50
|
);
|
|
18
51
|
}
|
|
19
52
|
return JSON.parse(rawBody);
|
|
20
53
|
}
|
|
21
|
-
function verifySignature(rawBody, signature, secret) {
|
|
54
|
+
function verifySignature(rawBody, signature, secret, options) {
|
|
55
|
+
if (typeof signature !== "string" || signature.length === 0) return false;
|
|
56
|
+
const stripe = parseStripeHeader(signature);
|
|
57
|
+
if (stripe) {
|
|
58
|
+
const tolerance = options?.tolerance ?? DEFAULT_TOLERANCE_SECONDS;
|
|
59
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
60
|
+
if (Math.abs(now - stripe.timestamp) > tolerance) return false;
|
|
61
|
+
const expected2 = createHmac("sha256", secret).update(`${stripe.timestamp}.${rawBody}`).digest("hex");
|
|
62
|
+
for (const candidate of stripe.v1) {
|
|
63
|
+
if (constantTimeEqualHex(candidate, expected2)) return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
22
67
|
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
23
|
-
|
|
24
|
-
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
|
|
68
|
+
return constantTimeEqualHex(signature, expected);
|
|
25
69
|
}
|
|
26
70
|
export {
|
|
71
|
+
DEFAULT_TOLERANCE_SECONDS,
|
|
27
72
|
WebhookSignatureVerificationError,
|
|
28
73
|
constructEvent,
|
|
74
|
+
signPayloadStripe,
|
|
29
75
|
verifySignature
|
|
30
76
|
};
|