mymx 0.3.5 → 0.3.7

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.
@@ -1,218 +0,0 @@
1
- "use strict";
2
- const require_errors = require('./errors-DOkb_I6u.cjs');
3
- const node_crypto = require_errors.__toESM(require("node:crypto"));
4
-
5
- //#region src/encoding.ts
6
- const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
7
- /**
8
- * Convert a Buffer to string with strict UTF-8 validation.
9
- * Throws if the buffer contains invalid UTF-8 sequences.
10
- *
11
- * Uses TextDecoder with `fatal: true` for robust validation that catches
12
- * all invalid UTF-8 sequences, not just those that produce replacement characters.
13
- *
14
- * @param buffer - The buffer to convert
15
- * @param label - Label for error messages (e.g., "request body")
16
- * @returns The UTF-8 decoded string
17
- * @throws WebhookPayloadError if the buffer contains invalid UTF-8
18
- */
19
- function bufferToString(buffer, label) {
20
- try {
21
- return utf8Decoder.decode(buffer);
22
- } catch (err) {
23
- throw new require_errors.WebhookPayloadError("INVALID_ENCODING", `${label} contains invalid UTF-8 bytes`, `Ensure the ${label} is valid UTF-8 encoded text. If the data is binary, it should be base64 encoded first.`, err instanceof Error ? err : void 0);
24
- }
25
- }
26
-
27
- //#endregion
28
- //#region src/signing.ts
29
- /**
30
- * WeakMap cache for computed signatures.
31
- * Only works for Buffer bodies (strings cannot be WeakMap keys).
32
- * Automatically garbage collected when Buffer is no longer referenced.
33
- */
34
- const signatureCache = new WeakMap();
35
- /**
36
- * Hash a secret for cache key comparison.
37
- * @internal
38
- */
39
- function hashSecret(secret) {
40
- return (0, node_crypto.createHash)("sha256").update(secret).digest("hex");
41
- }
42
- /** Header name for incoming webhook signature */
43
- const MYMX_SIGNATURE_HEADER = "MyMX-Signature";
44
- /** Header name to confirm webhook was processed (prevents retries) */
45
- const MYMX_CONFIRMED_HEADER = "X-MyMX-Confirmed";
46
- /** Default max age for webhook requests (5 minutes) */
47
- const DEFAULT_TOLERANCE_SECONDS = 5 * 60;
48
- /** Future clock skew tolerance (1 minute) */
49
- const FUTURE_TOLERANCE_SECONDS = 60;
50
- /** Valid hex pattern for signature verification */
51
- const HEX_PATTERN = /^[0-9a-f]+$/i;
52
- /**
53
- * Sign a webhook payload using HMAC-SHA256.
54
- *
55
- * Useful for:
56
- * - Internal testing and dogfooding
57
- * - Generating test vectors
58
- * - Integration tests
59
- *
60
- * @param rawBody - The raw JSON body string to sign
61
- * @param secret - The webhook secret key
62
- * @param timestamp - Unix timestamp in seconds (defaults to current time)
63
- */
64
- function signWebhookPayload(rawBody, secret, timestamp) {
65
- const ts = timestamp ?? Math.floor(Date.now() / 1e3);
66
- const body = typeof rawBody === "string" ? rawBody : bufferToString(rawBody, "rawBody");
67
- const signedPayloadString = `${ts}.${body}`;
68
- const hmac = (0, node_crypto.createHmac)("sha256", secret);
69
- hmac.update(signedPayloadString);
70
- const v1 = hmac.digest("hex");
71
- return {
72
- header: `t=${ts},v1=${v1}`,
73
- timestamp: ts,
74
- v1
75
- };
76
- }
77
- /**
78
- * Parse the MyMX-Signature header into its components.
79
- * Tolerant of whitespace around keys/values.
80
- *
81
- * @internal
82
- */
83
- function parseSignatureHeader(signatureHeader) {
84
- if (!signatureHeader || typeof signatureHeader !== "string") return null;
85
- const parts = signatureHeader.split(",");
86
- let timestamp = null;
87
- const signatures = [];
88
- for (const part of parts) {
89
- const idx = part.indexOf("=");
90
- if (idx === -1) continue;
91
- const key = part.slice(0, idx).trim();
92
- const value = part.slice(idx + 1).trim();
93
- if (!key || !value) continue;
94
- if (key === "t") {
95
- const parsed = Number.parseInt(value, 10);
96
- if (!Number.isNaN(parsed)) timestamp = parsed;
97
- } else if (key === "v1") signatures.push(value);
98
- }
99
- if (timestamp === null || signatures.length === 0) return null;
100
- return {
101
- timestamp,
102
- signatures
103
- };
104
- }
105
- /**
106
- * Check if a string is valid lowercase hex of expected length
107
- */
108
- function isValidHex(str, expectedLength) {
109
- return str.length === expectedLength && HEX_PATTERN.test(str);
110
- }
111
- /**
112
- * Verify a webhook signature.
113
- *
114
- * Throws `WebhookVerificationError` on failure with a specific error code.
115
- *
116
- * @example
117
- * ```typescript
118
- * import { verifyWebhookSignature, WebhookVerificationError } from 'mymx';
119
- *
120
- * try {
121
- * verifyWebhookSignature({
122
- * rawBody: req.body, // raw string, NOT parsed JSON
123
- * signatureHeader: req.headers['mymx-signature'],
124
- * secret: process.env.MYMX_WEBHOOK_SECRET,
125
- * });
126
- * // Signature is valid, process the webhook
127
- * } catch (err) {
128
- * if (err instanceof WebhookVerificationError) {
129
- * console.error('Invalid webhook:', err.code, err.message);
130
- * }
131
- * return res.status(400).send('Invalid signature');
132
- * }
133
- * ```
134
- */
135
- function verifyWebhookSignature(opts) {
136
- const { rawBody, signatureHeader, secret, toleranceSeconds = DEFAULT_TOLERANCE_SECONDS, nowSeconds } = opts;
137
- if (!secret || typeof secret === "string" && secret.length === 0) throw new require_errors.WebhookVerificationError("MISSING_SECRET", "Webhook secret is required but was empty or not provided");
138
- const parsed = parseSignatureHeader(signatureHeader);
139
- if (!parsed) throw new require_errors.WebhookVerificationError("INVALID_SIGNATURE_HEADER", "Invalid MyMX-Signature header format. Expected: t={timestamp},v1={signature}");
140
- const { timestamp, signatures } = parsed;
141
- const now = nowSeconds ?? Math.floor(Date.now() / 1e3);
142
- const age = now - timestamp;
143
- if (age > toleranceSeconds) throw new require_errors.WebhookVerificationError("TIMESTAMP_OUT_OF_RANGE", `Webhook timestamp too old (${age}s). Max age is ${toleranceSeconds}s.`);
144
- if (age < -FUTURE_TOLERANCE_SECONDS) throw new require_errors.WebhookVerificationError("TIMESTAMP_OUT_OF_RANGE", "Webhook timestamp is too far in the future. Check server clock sync.");
145
- const body = typeof rawBody === "string" ? rawBody : bufferToString(rawBody, "request body");
146
- let expectedHex;
147
- if (Buffer.isBuffer(rawBody)) {
148
- const cached = signatureCache.get(rawBody);
149
- const currentSecretHash = hashSecret(secret);
150
- if (cached && cached.timestamp === timestamp && cached.secretHash === currentSecretHash) expectedHex = cached.computed;
151
- else {
152
- const signedPayloadString = `${timestamp}.${body}`;
153
- const hmac = (0, node_crypto.createHmac)("sha256", secret);
154
- hmac.update(signedPayloadString);
155
- expectedHex = hmac.digest("hex");
156
- signatureCache.set(rawBody, {
157
- secretHash: currentSecretHash,
158
- timestamp,
159
- computed: expectedHex
160
- });
161
- }
162
- } else {
163
- const signedPayloadString = `${timestamp}.${body}`;
164
- const hmac = (0, node_crypto.createHmac)("sha256", secret);
165
- hmac.update(signedPayloadString);
166
- expectedHex = hmac.digest("hex");
167
- }
168
- for (const receivedHex of signatures) {
169
- if (!isValidHex(receivedHex, 64)) continue;
170
- const receivedBytes = Buffer.from(receivedHex, "hex");
171
- const expectedBytes = Buffer.from(expectedHex, "hex");
172
- if ((0, node_crypto.timingSafeEqual)(receivedBytes, expectedBytes)) return true;
173
- }
174
- const reserializationHint = detectReserializedBody(body);
175
- const message = reserializationHint ? `No valid signature found. ${reserializationHint}` : "No valid signature found. Verify the webhook secret matches and you're using the raw request body (not re-serialized JSON).";
176
- throw new require_errors.WebhookVerificationError("SIGNATURE_MISMATCH", message);
177
- }
178
- /**
179
- * Detect if a body looks like it was re-serialized by a framework.
180
- * Returns a helpful message if detected, null otherwise.
181
- * @internal
182
- */
183
- function detectReserializedBody(body) {
184
- if (/^\s*\{[\s\S]*\n\s{2,}/.test(body)) return "Request body appears re-serialized (pretty-printed). Use the raw request body before any JSON.parse() or JSON.stringify() calls.";
185
- return null;
186
- }
187
-
188
- //#endregion
189
- Object.defineProperty(exports, 'MYMX_CONFIRMED_HEADER', {
190
- enumerable: true,
191
- get: function () {
192
- return MYMX_CONFIRMED_HEADER;
193
- }
194
- });
195
- Object.defineProperty(exports, 'MYMX_SIGNATURE_HEADER', {
196
- enumerable: true,
197
- get: function () {
198
- return MYMX_SIGNATURE_HEADER;
199
- }
200
- });
201
- Object.defineProperty(exports, 'bufferToString', {
202
- enumerable: true,
203
- get: function () {
204
- return bufferToString;
205
- }
206
- });
207
- Object.defineProperty(exports, 'signWebhookPayload', {
208
- enumerable: true,
209
- get: function () {
210
- return signWebhookPayload;
211
- }
212
- });
213
- Object.defineProperty(exports, 'verifyWebhookSignature', {
214
- enumerable: true,
215
- get: function () {
216
- return verifyWebhookSignature;
217
- }
218
- });
@@ -1,84 +0,0 @@
1
- //#region src/signing.d.ts
2
- /**
3
- * Webhook HMAC Signing (Stripe-style format)
4
- *
5
- * Implements HMAC-SHA256 signature for webhook security with timestamp validation.
6
- * Prevents replay attacks by including timestamp in signature.
7
- *
8
- * Header format:
9
- * MyMX-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
10
- *
11
- * Signed payload format: "{timestamp}.{raw_body}"
12
- *
13
- * This format matches Stripe's webhook signature scheme, which is widely understood
14
- * and easy to implement in any language with ~15 lines of code.
15
- */
16
- /** Header name for incoming webhook signature */
17
- declare const MYMX_SIGNATURE_HEADER = "MyMX-Signature";
18
- /** Header name to confirm webhook was processed (prevents retries) */
19
- declare const MYMX_CONFIRMED_HEADER = "X-MyMX-Confirmed";
20
- /**
21
- * Result from signing a webhook payload
22
- */
23
- interface SignResult {
24
- /** Full header value ready to set on MyMX-Signature */
25
- header: string;
26
- /** Unix timestamp used for signing */
27
- timestamp: number;
28
- /** Raw hex signature (useful for debugging/tests) */
29
- v1: string;
30
- }
31
- /**
32
- * Options for verifying a webhook signature
33
- */
34
- interface VerifyOptions {
35
- /** The raw HTTP request body (must be the exact bytes, not re-serialized JSON) */
36
- rawBody: string | Buffer;
37
- /** The full MyMX-Signature header value */
38
- signatureHeader: string;
39
- /** Your webhook secret */
40
- secret: string | Buffer;
41
- /** Max age in seconds (default: 300) */
42
- toleranceSeconds?: number;
43
- /** Override current time for testing (unix seconds) */
44
- nowSeconds?: number;
45
- }
46
- /**
47
- * Sign a webhook payload using HMAC-SHA256.
48
- *
49
- * Useful for:
50
- * - Internal testing and dogfooding
51
- * - Generating test vectors
52
- * - Integration tests
53
- *
54
- * @param rawBody - The raw JSON body string to sign
55
- * @param secret - The webhook secret key
56
- * @param timestamp - Unix timestamp in seconds (defaults to current time)
57
- */
58
- declare function signWebhookPayload(rawBody: string | Buffer, secret: string | Buffer, timestamp?: number): SignResult;
59
- /**
60
- * Verify a webhook signature.
61
- *
62
- * Throws `WebhookVerificationError` on failure with a specific error code.
63
- *
64
- * @example
65
- * ```typescript
66
- * import { verifyWebhookSignature, WebhookVerificationError } from 'mymx';
67
- *
68
- * try {
69
- * verifyWebhookSignature({
70
- * rawBody: req.body, // raw string, NOT parsed JSON
71
- * signatureHeader: req.headers['mymx-signature'],
72
- * secret: process.env.MYMX_WEBHOOK_SECRET,
73
- * });
74
- * // Signature is valid, process the webhook
75
- * } catch (err) {
76
- * if (err instanceof WebhookVerificationError) {
77
- * console.error('Invalid webhook:', err.code, err.message);
78
- * }
79
- * return res.status(400).send('Invalid signature');
80
- * }
81
- * ```
82
- */
83
- declare function verifyWebhookSignature(opts: VerifyOptions): true; //#endregion
84
- export { MYMX_CONFIRMED_HEADER, MYMX_SIGNATURE_HEADER, SignResult, VerifyOptions, signWebhookPayload, verifyWebhookSignature };