@ix-xs/node-comfort 1.2.0 → 2.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +136 -549
  3. package/index.js +80 -35
  4. package/package.json +160 -33
  5. package/src/Arr.js +887 -419
  6. package/src/Async.js +585 -0
  7. package/src/Cache.js +329 -0
  8. package/src/Checker.js +971 -539
  9. package/src/Cli.js +1034 -0
  10. package/src/Color.js +415 -0
  11. package/src/Crypto.js +707 -0
  12. package/src/Emitter.js +289 -0
  13. package/src/Env.js +555 -0
  14. package/src/FS.js +1483 -894
  15. package/src/Func.js +670 -353
  16. package/src/Http.js +538 -0
  17. package/src/Id.js +384 -201
  18. package/src/Logger.js +739 -378
  19. package/src/Num.js +844 -361
  20. package/src/Obj.js +855 -372
  21. package/src/SQLite.js +1308 -1005
  22. package/src/Schema.js +1586 -0
  23. package/src/Str.js +983 -436
  24. package/src/Sys.js +511 -0
  25. package/src/Time.js +1260 -422
  26. package/src/Utils.js +321 -325
  27. package/src/errors.js +292 -0
  28. package/types/index.d.ts +693 -526
  29. package/types/src/Arr.d.ts +455 -145
  30. package/types/src/Arr.d.ts.map +1 -1
  31. package/types/src/Async.d.ts +321 -0
  32. package/types/src/Async.d.ts.map +1 -0
  33. package/types/src/Cache.d.ts +195 -0
  34. package/types/src/Cache.d.ts.map +1 -0
  35. package/types/src/Checker.d.ts +501 -235
  36. package/types/src/Checker.d.ts.map +1 -1
  37. package/types/src/Cli.d.ts +582 -0
  38. package/types/src/Cli.d.ts.map +1 -0
  39. package/types/src/Color.d.ts +294 -0
  40. package/types/src/Color.d.ts.map +1 -0
  41. package/types/src/Crypto.d.ts +534 -0
  42. package/types/src/Crypto.d.ts.map +1 -0
  43. package/types/src/Emitter.d.ts +170 -0
  44. package/types/src/Emitter.d.ts.map +1 -0
  45. package/types/src/Env.d.ts +382 -0
  46. package/types/src/Env.d.ts.map +1 -0
  47. package/types/src/FS.d.ts +686 -238
  48. package/types/src/FS.d.ts.map +1 -1
  49. package/types/src/Func.d.ts +380 -135
  50. package/types/src/Func.d.ts.map +1 -1
  51. package/types/src/Http.d.ts +374 -0
  52. package/types/src/Http.d.ts.map +1 -0
  53. package/types/src/Id.d.ts +149 -68
  54. package/types/src/Id.d.ts.map +1 -1
  55. package/types/src/Logger.d.ts +363 -46
  56. package/types/src/Logger.d.ts.map +1 -1
  57. package/types/src/Num.d.ts +473 -137
  58. package/types/src/Num.d.ts.map +1 -1
  59. package/types/src/Obj.d.ts +390 -104
  60. package/types/src/Obj.d.ts.map +1 -1
  61. package/types/src/SQLite.d.ts +646 -309
  62. package/types/src/SQLite.d.ts.map +1 -1
  63. package/types/src/Schema.d.ts +940 -0
  64. package/types/src/Schema.d.ts.map +1 -0
  65. package/types/src/Str.d.ts +569 -166
  66. package/types/src/Str.d.ts.map +1 -1
  67. package/types/src/Sys.d.ts +269 -0
  68. package/types/src/Sys.d.ts.map +1 -0
  69. package/types/src/Time.d.ts +663 -204
  70. package/types/src/Time.d.ts.map +1 -1
  71. package/types/src/Utils.d.ts +154 -102
  72. package/types/src/Utils.d.ts.map +1 -1
  73. package/types/src/errors.d.ts +292 -0
  74. package/types/src/errors.d.ts.map +1 -0
  75. package/types/index.d.ts.map +0 -1
package/src/Crypto.js ADDED
@@ -0,0 +1,707 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Crypto without the footguns, built on `node:crypto`: hashes, HMAC,
5
+ * password hashing (scrypt), encryption (AES-256-GCM), JWT, two-factor
6
+ * codes (TOTP) and secure random values.
7
+ *
8
+ * @example
9
+ * const stored = await crypto.hashPassword("hunter2");
10
+ * await crypto.verifyPassword("hunter2", stored); // true
11
+ *
12
+ * const token = crypto.signJWT({ sub: user.id }, SECRET, { expiresIn: "1h" });
13
+ * const claims = crypto.verifyJWT(token, SECRET);
14
+ */
15
+
16
+ const { JWTError } = require("./errors.js");
17
+ const { parseDuration } = require("./Time.js");
18
+
19
+ /** @type {typeof import("node:crypto") | undefined} */
20
+ let _nodeCrypto;
21
+ // node:crypto takes a few milliseconds to initialize: load it on first use.
22
+ const _crypto = () => (_nodeCrypto ??= require("node:crypto"));
23
+
24
+ /**
25
+ * @param {import("node:crypto").BinaryLike} password
26
+ * @param {import("node:crypto").BinaryLike} salt
27
+ * @param {number} keylen
28
+ * @param {import("node:crypto").ScryptOptions} options
29
+ * @returns {Promise<Buffer>}
30
+ */
31
+ const _scrypt = (password, salt, keylen, options) =>
32
+ new Promise((resolve, reject) => {
33
+ _crypto().scrypt(password, salt, keylen, options, (error, key) => (error ? reject(error) : resolve(key)));
34
+ });
35
+
36
+ /**
37
+ * Text encodings for digests.
38
+ * @typedef {"hex" | "base64" | "base64url"} DigestEncoding
39
+ */
40
+
41
+ /**
42
+ * Options for `hash()` and `hmac()`.
43
+ * @typedef {object} HashOptions
44
+ * @property {"sha256" | "sha384" | "sha512" | "sha1" | "md5" | "sha3-256" | "sha3-512" | (string & {})} [algorithm] Any algorithm OpenSSL supports. Defaults to `"sha256"`.
45
+ * @property {DigestEncoding} [encoding] Defaults to `"hex"`.
46
+ */
47
+
48
+ /**
49
+ * Options for `hashPassword()`.
50
+ * @typedef {object} HashPasswordOptions
51
+ * @property {number} [cost] Work factor as a power of two; each +1 doubles the time. Defaults to `15`.
52
+ * @property {number} [blockSize] scrypt `r`. Defaults to `8`.
53
+ * @property {number} [parallelization] scrypt `p`. Defaults to `1`.
54
+ * @property {number} [saltLength] In bytes. Defaults to `16`.
55
+ * @property {number} [keyLength] In bytes. Defaults to `32`.
56
+ */
57
+
58
+ /**
59
+ * Options for `encrypt()`.
60
+ * @typedef {object} EncryptOptions
61
+ * @property {string | Buffer} [associatedData] Data that's authenticated but not encrypted, like a user id. Pass the same value to `decrypt()`.
62
+ */
63
+
64
+ /**
65
+ * Options for `decrypt()`.
66
+ * @typedef {object} DecryptOptions
67
+ * @property {string | Buffer} [associatedData] The value given to `encrypt()`, if any.
68
+ * @property {"utf8" | "buffer"} [output] Get a string or a `Buffer`. Defaults to `"utf8"`.
69
+ */
70
+
71
+ /**
72
+ * Standard JWT claims.
73
+ * @typedef {object} JWTClaims
74
+ * @property {string} [iss] Issuer.
75
+ * @property {string} [sub] Subject, usually the user id.
76
+ * @property {string | string[]} [aud] Audience.
77
+ * @property {number} [exp] Expiry, in Unix seconds.
78
+ * @property {number} [nbf] Not valid before, in Unix seconds.
79
+ * @property {number} [iat] Issued at, in Unix seconds.
80
+ * @property {string} [jti] Unique token id.
81
+ */
82
+
83
+ /**
84
+ * Supported JWT algorithms.
85
+ * @typedef {"HS256" | "HS384" | "HS512"} JWTAlgorithm
86
+ */
87
+
88
+ /**
89
+ * Options for `signJWT()`.
90
+ * @typedef {object} SignJWTOptions
91
+ * @property {JWTAlgorithm} [algorithm] Defaults to `"HS256"`.
92
+ * @property {string | number} [expiresIn] Lifetime in seconds or as a duration like `"15m"`. Sets `exp`.
93
+ * @property {string | number} [notBefore] Delay before the token is valid. Sets `nbf`.
94
+ * @property {string} [issuer] Sets `iss`.
95
+ * @property {string} [subject] Sets `sub`.
96
+ * @property {string | string[]} [audience] Sets `aud`.
97
+ * @property {string} [jwtId] Sets `jti`, handy for revocation lists.
98
+ * @property {boolean} [noTimestamp] Don't add `iat`.
99
+ * @property {Record<string, unknown>} [header] Extra header fields, like `{ kid: "2024-key" }`.
100
+ */
101
+
102
+ /**
103
+ * Options for `verifyJWT()`.
104
+ * @typedef {object} VerifyJWTOptions
105
+ * @property {JWTAlgorithm[]} [algorithms] Accepted algorithms. Defaults to all three HMAC ones.
106
+ * @property {string | string[]} [issuer] Required issuer.
107
+ * @property {string | string[]} [audience] Required audience; one match is enough.
108
+ * @property {string} [subject] Required subject.
109
+ * @property {number} [clockTolerance] Seconds of slack for `exp` and `nbf` when server clocks differ.
110
+ * @property {string | number} [maxAge] Reject tokens issued longer ago than this.
111
+ * @property {boolean} [ignoreExpiration] Skip the `exp` check.
112
+ * @property {Date | number} [now] What time it is. Defaults to now.
113
+ */
114
+
115
+ /**
116
+ * A decoded JWT.
117
+ * @template [P=Record<string, unknown>]
118
+ * @typedef {object} DecodedJWT
119
+ * @property {{ alg: string, typ?: string, [key: string]: unknown }} header
120
+ * @property {P & JWTClaims} payload
121
+ * @property {string} signature Base64URL.
122
+ */
123
+
124
+ /**
125
+ * Options for `totp()` and `verifyTOTP()`.
126
+ * @typedef {object} TOTPOptions
127
+ * @property {number} [digits] Defaults to `6`.
128
+ * @property {number} [period] How long a code lasts, in seconds. Defaults to `30`.
129
+ * @property {"SHA1" | "SHA256" | "SHA512"} [algorithm] Defaults to `"SHA1"`, which is what authenticator apps expect.
130
+ * @property {Date | number} [time] Compute the code for this moment. Defaults to now.
131
+ */
132
+
133
+ /**
134
+ * Options for `verifyTOTP()`. `window` is how many periods before and after
135
+ * now are accepted (defaults to `1`).
136
+ * @typedef {TOTPOptions & { window?: number }} VerifyTOTPOptions
137
+ */
138
+
139
+ /**
140
+ * Options for `totpURI()`.
141
+ * @typedef {object} TOTPURIOptions
142
+ * @property {string} label The account shown in the app, usually the email.
143
+ * @property {string} [issuer] Your app's name.
144
+ * @property {number} [digits] Defaults to `6`.
145
+ * @property {number} [period] Defaults to `30`.
146
+ * @property {"SHA1" | "SHA256" | "SHA512"} [algorithm] Defaults to `"SHA1"`.
147
+ */
148
+
149
+ const _BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
150
+
151
+ /**
152
+ * @param {Buffer} buffer
153
+ * @returns {string}
154
+ */
155
+ const _base32Encode = (buffer) => {
156
+ let bits = 0;
157
+ let value = 0;
158
+ let out = "";
159
+ for (const byte of buffer) {
160
+ value = (value << 8) | byte;
161
+ bits += 8;
162
+ while (bits >= 5) {
163
+ out += _BASE32[(value >>> (bits - 5)) & 31];
164
+ bits -= 5;
165
+ }
166
+ }
167
+ if (bits > 0) out += _BASE32[(value << (5 - bits)) & 31];
168
+ return out;
169
+ };
170
+
171
+ /**
172
+ * @param {string} text
173
+ * @returns {Buffer}
174
+ */
175
+ const _base32Decode = (text) => {
176
+ const clean = text.toUpperCase().replace(/[\s=-]/g, "");
177
+ let bits = 0;
178
+ let value = 0;
179
+ const out = [];
180
+ for (const char of clean) {
181
+ const index = _BASE32.indexOf(char);
182
+ if (index === -1) throw new TypeError(`Invalid Base32 character "${char}"`);
183
+ value = (value << 5) | index;
184
+ bits += 5;
185
+ if (bits >= 8) {
186
+ out.push((value >>> (bits - 8)) & 255);
187
+ bits -= 8;
188
+ }
189
+ }
190
+ return Buffer.from(out);
191
+ };
192
+
193
+ /** @param {unknown} data */
194
+ const _toBuffer = (data) => (Buffer.isBuffer(data) ? data : data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(String(data), "utf8"));
195
+
196
+ /**
197
+ * @param {string | number} value
198
+ * @returns {number}
199
+ */
200
+ const _seconds = (value) => {
201
+ if (typeof value === "number") return value;
202
+ const ms = parseDuration(value);
203
+ if (ms === null) throw new TypeError(`Invalid duration "${value}"`);
204
+ return Math.round(ms / 1000);
205
+ };
206
+
207
+ /**
208
+ * Hashes data, SHA-256 in hex by default.
209
+ *
210
+ * @example
211
+ * crypto.hash("hello");
212
+ * crypto.hash("hello", { algorithm: "sha512", encoding: "base64url" });
213
+ *
214
+ * @param {string | Buffer | Uint8Array} data Strings are read as UTF-8.
215
+ * @param {HashOptions} [options]
216
+ * @returns {string}
217
+ */
218
+ function hash(data, options = {}) {
219
+ return _crypto().createHash(options.algorithm ?? "sha256").update(_toBuffer(data)).digest(options.encoding ?? "hex");
220
+ }
221
+
222
+ /**
223
+ * An HMAC signature: a hash keyed with a secret. The usual way to sign and
224
+ * check webhooks.
225
+ *
226
+ * @example
227
+ * const signature = crypto.hmac(rawBody, process.env.WEBHOOK_SECRET);
228
+ * crypto.safeEqual(signature, req.headers["x-signature"]);
229
+ *
230
+ * @param {string | Buffer | Uint8Array} data
231
+ * @param {string | Buffer | Uint8Array} secret
232
+ * @param {HashOptions} [options]
233
+ * @returns {string}
234
+ */
235
+ function hmac(data, secret, options = {}) {
236
+ return _crypto().createHmac(options.algorithm ?? "sha256", _toBuffer(secret)).update(_toBuffer(data)).digest(options.encoding ?? "hex");
237
+ }
238
+
239
+ /**
240
+ * Compares two secrets in constant time, so the comparison can't leak
241
+ * anything through timing. Use it instead of `===` for tokens, signatures
242
+ * and API keys.
243
+ *
244
+ * @example
245
+ * if (!crypto.safeEqual(providedKey, process.env.API_KEY)) throw new Error("Forbidden");
246
+ *
247
+ * @param {string | Buffer | Uint8Array} a
248
+ * @param {string | Buffer | Uint8Array} b
249
+ * @returns {boolean}
250
+ */
251
+ function safeEqual(a, b) {
252
+ const x = _toBuffer(a);
253
+ const y = _toBuffer(b);
254
+ if (x.length !== y.length) {
255
+ _crypto().timingSafeEqual(x, x);
256
+ return false;
257
+ }
258
+ return _crypto().timingSafeEqual(x, y);
259
+ }
260
+
261
+ /**
262
+ * Hashes a password with scrypt and a random salt. The result records its
263
+ * own settings, so you can raise the cost later without breaking old
264
+ * hashes.
265
+ *
266
+ * @example
267
+ * const stored = await crypto.hashPassword(password);
268
+ * // "$scrypt$ln=15,r=8,p=1$4xY...$kq9...", store this
269
+ *
270
+ * @param {string} password
271
+ * @param {HashPasswordOptions} [options]
272
+ * @returns {Promise<string>}
273
+ */
274
+ async function hashPassword(password, options = {}) {
275
+ const ln = options.cost ?? 15;
276
+ const r = options.blockSize ?? 8;
277
+ const p = options.parallelization ?? 1;
278
+ const salt = _crypto().randomBytes(options.saltLength ?? 16);
279
+ const N = 2 ** ln;
280
+ const key = await _scrypt(password.normalize("NFKC"), salt, options.keyLength ?? 32, { N, r, p, maxmem: 256 * N * r + 1024 * 1024 });
281
+ return `$scrypt$ln=${ln},r=${r},p=${p}$${salt.toString("base64url")}$${key.toString("base64url")}`;
282
+ }
283
+
284
+ /**
285
+ * Checks a password against a stored hash, in constant time. A wrong
286
+ * password or a broken hash gives `false`, never an error.
287
+ *
288
+ * @example
289
+ * if (!(await crypto.verifyPassword(input, user.passwordHash))) throw new Error("Invalid credentials");
290
+ *
291
+ * @param {string} password
292
+ * @param {string} stored
293
+ * @returns {Promise<boolean>}
294
+ */
295
+ async function verifyPassword(password, stored) {
296
+ const match = /^\$scrypt\$ln=(\d+),r=(\d+),p=(\d+)\$([\w-]+)\$([\w-]+)$/.exec(String(stored));
297
+ if (!match) return false;
298
+ const [, ln, r, p, salt, key] = match;
299
+ const expected = Buffer.from(key, "base64url");
300
+ const N = 2 ** Number(ln);
301
+ try {
302
+ const actual = await _scrypt(String(password).normalize("NFKC"), Buffer.from(salt, "base64url"), expected.length, {
303
+ N, r: Number(r), p: Number(p), maxmem: 256 * N * Number(r) + 1024 * 1024,
304
+ });
305
+ return _crypto().timingSafeEqual(actual, expected);
306
+ } catch {
307
+ return false;
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Was this hash made with weaker settings than yours? If so, re-hash the
313
+ * password right after a successful login.
314
+ *
315
+ * @example
316
+ * if (await crypto.verifyPassword(input, user.hash) && crypto.needsRehash(user.hash, { cost: 16 })) {
317
+ * user.hash = await crypto.hashPassword(input, { cost: 16 });
318
+ * }
319
+ *
320
+ * @param {string} stored
321
+ * @param {HashPasswordOptions} [options] The settings you use today.
322
+ * @returns {boolean}
323
+ */
324
+ function needsRehash(stored, options = {}) {
325
+ const match = /^\$scrypt\$ln=(\d+),r=(\d+),p=(\d+)\$/.exec(String(stored));
326
+ if (!match) return true;
327
+ return Number(match[1]) < (options.cost ?? 15) || Number(match[2]) < (options.blockSize ?? 8) || Number(match[3]) < (options.parallelization ?? 1);
328
+ }
329
+
330
+ /**
331
+ * A random 256-bit key for `encrypt()`, as text you can put in an
332
+ * environment variable.
333
+ *
334
+ * @returns {string}
335
+ */
336
+ function generateKey() {
337
+ return _crypto().randomBytes(32).toString("base64url");
338
+ }
339
+
340
+ /**
341
+ * @param {string | Buffer | Uint8Array} secret
342
+ * @param {Buffer} salt
343
+ * @returns {Buffer}
344
+ */
345
+ const _deriveKey = (secret, salt) => {
346
+ const raw = typeof secret === "string" && /^[A-Za-z0-9_-]{43}$/.test(secret) ? Buffer.from(secret, "base64url") : _toBuffer(secret);
347
+ if (!raw.length) throw new TypeError("The encryption secret must not be empty");
348
+ return Buffer.from(_crypto().hkdfSync("sha256", raw, salt, "node-comfort:aes-256-gcm", 32));
349
+ };
350
+
351
+ /**
352
+ * Encrypts data with AES-256-GCM, which also detects tampering. Every call
353
+ * uses a fresh salt and nonce, so the same text never encrypts the same way
354
+ * twice.
355
+ *
356
+ * Use a key from `generateKey()` or another long random secret, not a
357
+ * password someone typed: that would be easy to brute-force.
358
+ *
359
+ * @example
360
+ * const box = crypto.encrypt(JSON.stringify(card), process.env.ENCRYPTION_KEY);
361
+ * const card = JSON.parse(crypto.decrypt(box, process.env.ENCRYPTION_KEY));
362
+ *
363
+ * @param {string | Buffer | Uint8Array} data
364
+ * @param {string | Buffer | Uint8Array} secret
365
+ * @param {EncryptOptions} [options]
366
+ * @returns {string} A compact, URL-safe string.
367
+ */
368
+ function encrypt(data, secret, options = {}) {
369
+ const salt = _crypto().randomBytes(16);
370
+ const iv = _crypto().randomBytes(12);
371
+ const cipher = _crypto().createCipheriv("aes-256-gcm", _deriveKey(secret, salt), iv);
372
+ if (options.associatedData !== undefined) cipher.setAAD(_toBuffer(options.associatedData));
373
+ const encrypted = Buffer.concat([cipher.update(_toBuffer(data)), cipher.final()]);
374
+ return ["v1", salt, iv, cipher.getAuthTag(), encrypted].map((part) => (typeof part === "string" ? part : part.toString("base64url"))).join(".");
375
+ }
376
+
377
+ /**
378
+ * Decrypts what `encrypt()` produced. Throws if the secret is wrong or the
379
+ * data was changed.
380
+ *
381
+ * @example
382
+ * const text = crypto.decrypt(box, SECRET);
383
+ * const bytes = crypto.decrypt(box, SECRET, { output: "buffer" });
384
+ *
385
+ * @overload
386
+ * @param {string} payload
387
+ * @param {string | Buffer | Uint8Array} secret
388
+ * @param {DecryptOptions & { output: "buffer" }} options
389
+ * @returns {Buffer}
390
+ */
391
+ /**
392
+ * Decrypts what `encrypt()` produced, as text.
393
+ *
394
+ * @overload
395
+ * @param {string} payload
396
+ * @param {string | Buffer | Uint8Array} secret
397
+ * @param {DecryptOptions} [options]
398
+ * @returns {string}
399
+ */
400
+ /**
401
+ * @param {string} payload
402
+ * @param {string | Buffer | Uint8Array} secret
403
+ * @param {DecryptOptions} [options]
404
+ * @returns {string | Buffer}
405
+ * @throws {Error} If the secret is wrong or the payload was modified.
406
+ */
407
+ function decrypt(payload, secret, options = {}) {
408
+ const parts = String(payload).split(".");
409
+ if (parts.length !== 5 || parts[0] !== "v1") throw new TypeError("Invalid encrypted payload");
410
+ const [salt, iv, tag, encrypted] = parts.slice(1).map((part) => Buffer.from(part, "base64url"));
411
+ const decipher = _crypto().createDecipheriv("aes-256-gcm", _deriveKey(secret, salt), iv);
412
+ decipher.setAuthTag(tag);
413
+ if (options.associatedData !== undefined) decipher.setAAD(_toBuffer(options.associatedData));
414
+ let decrypted;
415
+ try {
416
+ decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
417
+ } catch (error) {
418
+ throw new Error("Unable to decrypt: wrong secret or tampered data", { cause: error });
419
+ }
420
+ return options.output === "buffer" ? decrypted : decrypted.toString("utf8");
421
+ }
422
+
423
+ /**
424
+ * Creates a signed JSON Web Token. `iat` is added for you.
425
+ *
426
+ * @example
427
+ * const token = crypto.signJWT({ sub: String(user.id), role: user.role }, process.env.JWT_SECRET, {
428
+ * expiresIn: "15m",
429
+ * issuer: "api.example.com",
430
+ * });
431
+ *
432
+ * @param {Record<string, unknown>} payload
433
+ * @param {string | Buffer} secret At least 32 random bytes.
434
+ * @param {SignJWTOptions} [options]
435
+ * @returns {string}
436
+ */
437
+ function signJWT(payload, secret, options = {}) {
438
+ const algorithm = options.algorithm ?? "HS256";
439
+ if (!/^HS(256|384|512)$/.test(algorithm)) throw new JWTError(`Unsupported algorithm "${algorithm}"`, { code: "ERR_JWT_ALGORITHM" });
440
+ const now = Math.floor(Date.now() / 1000);
441
+ /** @type {Record<string, unknown>} */
442
+ const claims = { ...payload };
443
+ if (!options.noTimestamp && claims.iat === undefined) claims.iat = now;
444
+ if (options.expiresIn !== undefined) claims.exp = now + _seconds(options.expiresIn);
445
+ if (options.notBefore !== undefined) claims.nbf = now + _seconds(options.notBefore);
446
+ if (options.issuer !== undefined) claims.iss = options.issuer;
447
+ if (options.subject !== undefined) claims.sub = options.subject;
448
+ if (options.audience !== undefined) claims.aud = options.audience;
449
+ if (options.jwtId !== undefined) claims.jti = options.jwtId;
450
+ const header = { ...options.header, alg: algorithm, typ: "JWT" };
451
+ const body = `${Buffer.from(JSON.stringify(header)).toString("base64url")}.${Buffer.from(JSON.stringify(claims)).toString("base64url")}`;
452
+ return `${body}.${hmac(body, secret, { algorithm: `sha${algorithm.slice(2)}`, encoding: "base64url" })}`;
453
+ }
454
+
455
+ /**
456
+ * Reads a JWT without checking it. Fine for display; call `verifyJWT()`
457
+ * before trusting anything in it.
458
+ *
459
+ * @example
460
+ * crypto.decodeJWT(token).payload.exp;
461
+ *
462
+ * @template [P=Record<string, unknown>]
463
+ * @param {string} token
464
+ * @returns {DecodedJWT<P>}
465
+ * @throws {JWTError} With code `ERR_JWT_MALFORMED` if the token can't be read.
466
+ */
467
+ function decodeJWT(token) {
468
+ const parts = String(token).split(".");
469
+ if (parts.length !== 3) throw new JWTError("Malformed JWT: expected 3 parts", { code: "ERR_JWT_MALFORMED" });
470
+ try {
471
+ const header = JSON.parse(Buffer.from(parts[0], "base64url").toString("utf8"));
472
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
473
+ if (!header || typeof header !== "object" || !payload || typeof payload !== "object") throw new TypeError("not an object");
474
+ return { header, payload, signature: parts[2] };
475
+ } catch (error) {
476
+ throw new JWTError("Malformed JWT: header or payload is not valid JSON", { code: "ERR_JWT_MALFORMED", cause: error });
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Verifies a JWT: algorithm, signature, expiry, and whichever claims you
482
+ * require. Returns the payload, or throws a `JWTError` whose `code` says
483
+ * what's wrong.
484
+ *
485
+ * @example
486
+ * try {
487
+ * const { sub } = crypto.verifyJWT(token, process.env.JWT_SECRET, { issuer: "api.example.com" });
488
+ * } catch (error) {
489
+ * res.status(401).json({ error: error.code }); // "ERR_JWT_EXPIRED"...
490
+ * }
491
+ *
492
+ * @template [P=Record<string, unknown>]
493
+ * @param {string} token
494
+ * @param {string | Buffer} secret
495
+ * @param {VerifyJWTOptions} [options]
496
+ * @returns {P & JWTClaims}
497
+ * @throws {JWTError}
498
+ */
499
+ function verifyJWT(token, secret, options = {}) {
500
+ const { header, payload } = decodeJWT(token);
501
+ const allowed = options.algorithms ?? ["HS256", "HS384", "HS512"];
502
+ if (typeof header.alg !== "string" || !allowed.includes(/** @type {JWTAlgorithm} */ (header.alg))) {
503
+ throw new JWTError(`Algorithm "${String(header.alg)}" is not allowed`, { code: "ERR_JWT_ALGORITHM" });
504
+ }
505
+ const [head, body, signature] = String(token).split(".");
506
+ const expected = hmac(`${head}.${body}`, secret, { algorithm: `sha${header.alg.slice(2)}`, encoding: "base64url" });
507
+ if (!safeEqual(expected, signature)) throw new JWTError("Invalid signature", { code: "ERR_JWT_SIGNATURE" });
508
+
509
+ const now = Math.floor((options.now instanceof Date ? options.now.getTime() : options.now ?? Date.now()) / 1000);
510
+ const tolerance = options.clockTolerance ?? 0;
511
+ const claims = /** @type {JWTClaims & Record<string, unknown>} */ (payload);
512
+ if (!options.ignoreExpiration && typeof claims.exp === "number" && now - tolerance >= claims.exp) {
513
+ throw new JWTError("Token expired", { code: "ERR_JWT_EXPIRED", expiredAt: new Date(claims.exp * 1000) });
514
+ }
515
+ if (typeof claims.nbf === "number" && now + tolerance < claims.nbf) throw new JWTError("Token not active yet", { code: "ERR_JWT_NOT_BEFORE" });
516
+ if (options.maxAge !== undefined && (typeof claims.iat !== "number" || now - tolerance > claims.iat + _seconds(options.maxAge))) {
517
+ throw new JWTError("Token is too old", { code: "ERR_JWT_EXPIRED" });
518
+ }
519
+ if (options.issuer !== undefined && ![options.issuer].flat().includes(/** @type {string} */ (claims.iss))) {
520
+ throw new JWTError("Invalid issuer", { code: "ERR_JWT_CLAIM" });
521
+ }
522
+ if (options.subject !== undefined && claims.sub !== options.subject) throw new JWTError("Invalid subject", { code: "ERR_JWT_CLAIM" });
523
+ if (options.audience !== undefined) {
524
+ const expectedAud = [options.audience].flat();
525
+ const actualAud = [claims.aud ?? []].flat();
526
+ if (!actualAud.some((aud) => expectedAud.includes(/** @type {string} */ (aud)))) throw new JWTError("Invalid audience", { code: "ERR_JWT_CLAIM" });
527
+ }
528
+ return /** @type {P & JWTClaims} */ (payload);
529
+ }
530
+
531
+ /**
532
+ * A new two-factor secret, in the Base32 format authenticator apps expect.
533
+ * Store it encrypted.
534
+ *
535
+ * @example
536
+ * user.totpSecret = crypto.totpSecret();
537
+ *
538
+ * @param {number} [bytes=20]
539
+ * @returns {string}
540
+ */
541
+ function totpSecret(bytes = 20) {
542
+ return _base32Encode(_crypto().randomBytes(bytes));
543
+ }
544
+
545
+ /**
546
+ * The current 6-digit code for a two-factor secret, as shown by
547
+ * authenticator apps.
548
+ *
549
+ * @example
550
+ * crypto.totp(user.totpSecret); // "492039"
551
+ *
552
+ * @param {string} secret
553
+ * @param {TOTPOptions} [options]
554
+ * @returns {string}
555
+ */
556
+ function totp(secret, options = {}) {
557
+ const digits = options.digits ?? 6;
558
+ const period = options.period ?? 30;
559
+ const time = options.time instanceof Date ? options.time.getTime() : options.time ?? Date.now();
560
+ const counter = Math.floor(time / 1000 / period);
561
+ const buffer = Buffer.alloc(8);
562
+ buffer.writeBigUInt64BE(BigInt(counter));
563
+ const digest = _crypto().createHmac((options.algorithm ?? "SHA1").toLowerCase(), _base32Decode(secret)).update(buffer).digest();
564
+ const off = digest[digest.length - 1] & 0x0f;
565
+ const code = ((digest[off] & 0x7f) << 24) | (digest[off + 1] << 16) | (digest[off + 2] << 8) | digest[off + 3];
566
+ return String(code % 10 ** digits).padStart(digits, "0");
567
+ }
568
+
569
+ /**
570
+ * Checks a code typed by a user. Codes from the neighbouring periods are
571
+ * accepted too, to cope with clock drift.
572
+ *
573
+ * @example
574
+ * if (!crypto.verifyTOTP(req.body.code, user.totpSecret)) throw new Error("Invalid code");
575
+ *
576
+ * @param {string} token Spaces are ignored.
577
+ * @param {string} secret
578
+ * @param {VerifyTOTPOptions} [options]
579
+ * @returns {boolean}
580
+ */
581
+ function verifyTOTP(token, secret, options = {}) {
582
+ const code = String(token).replace(/\s/g, "");
583
+ const digits = options.digits ?? 6;
584
+ if (!new RegExp(`^\\d{${digits}}$`).test(code)) return false;
585
+ const period = options.period ?? 30;
586
+ const time = options.time instanceof Date ? options.time.getTime() : options.time ?? Date.now();
587
+ const window = options.window ?? 1;
588
+ let valid = false;
589
+ for (let i = -window; i <= window; i++) {
590
+ if (safeEqual(totp(secret, { ...options, time: time + i * period * 1000 }), code)) valid = true;
591
+ }
592
+ return valid;
593
+ }
594
+
595
+ /**
596
+ * The `otpauth://` link to show as a QR code when a user turns on 2FA.
597
+ *
598
+ * @example
599
+ * const uri = crypto.totpURI(user.totpSecret, { label: user.email, issuer: "My App" });
600
+ *
601
+ * @param {string} secret
602
+ * @param {TOTPURIOptions} options
603
+ * @returns {string}
604
+ */
605
+ function totpURI(secret, options) {
606
+ const label = options.issuer ? `${encodeURIComponent(options.issuer)}:${encodeURIComponent(options.label)}` : encodeURIComponent(options.label);
607
+ const params = new URLSearchParams({ secret: secret.replace(/\s/g, "").toUpperCase() });
608
+ if (options.issuer) params.set("issuer", options.issuer);
609
+ params.set("algorithm", options.algorithm ?? "SHA1");
610
+ params.set("digits", String(options.digits ?? 6));
611
+ params.set("period", String(options.period ?? 30));
612
+ return `otpauth://totp/${label}?${params.toString().replace(/\+/g, "%20")}`;
613
+ }
614
+
615
+ /**
616
+ * Secure random bytes.
617
+ *
618
+ * @param {number} size
619
+ * @returns {Buffer}
620
+ */
621
+ function randomBytes(size) {
622
+ return _crypto().randomBytes(size);
623
+ }
624
+
625
+ /**
626
+ * A secure random integer from `min` to `max`, both included, with no bias.
627
+ * For anything that has to be fair or unpredictable.
628
+ *
629
+ * @example
630
+ * crypto.randomInt(1, 6); // a fair dice roll
631
+ * crypto.randomInt(0, 999_999); // a 6-digit code
632
+ *
633
+ * @param {number} min
634
+ * @param {number} max
635
+ * @returns {number}
636
+ */
637
+ function randomInt(min, max) {
638
+ return _crypto().randomInt(Math.ceil(min), Math.floor(max) + 1);
639
+ }
640
+
641
+ /**
642
+ * Encodes to Base64, or URL-safe Base64.
643
+ *
644
+ * @example
645
+ * crypto.toBase64("héllo"); // "aMOpbGxv"
646
+ * crypto.toBase64("héllo", { urlSafe: true });
647
+ *
648
+ * @param {string | Buffer | Uint8Array} data
649
+ * @param {{ urlSafe?: boolean }} [options]
650
+ * @returns {string}
651
+ */
652
+ function toBase64(data, options = {}) {
653
+ return _toBuffer(data).toString(options.urlSafe ? "base64url" : "base64");
654
+ }
655
+
656
+ /**
657
+ * Decodes Base64 or URL-safe Base64.
658
+ *
659
+ * @example
660
+ * crypto.fromBase64("aMOpbGxv"); // "héllo"
661
+ * crypto.fromBase64("aMOpbGxv", { output: "buffer" }); // <Buffer 68 c3 a9 6c 6c 6f>
662
+ *
663
+ * @overload
664
+ * @param {string} text
665
+ * @param {{ output: "buffer" }} options
666
+ * @returns {Buffer}
667
+ */
668
+ /**
669
+ * Decodes Base64 or URL-safe Base64 into text.
670
+ *
671
+ * @overload
672
+ * @param {string} text
673
+ * @param {{ output?: "utf8" }} [options]
674
+ * @returns {string}
675
+ */
676
+ /**
677
+ * @param {string} text
678
+ * @param {{ output?: "utf8" | "buffer" }} [options]
679
+ * @returns {string | Buffer}
680
+ */
681
+ function fromBase64(text, options = {}) {
682
+ const buffer = Buffer.from(String(text).replace(/-/g, "+").replace(/_/g, "/"), "base64");
683
+ return options.output === "buffer" ? buffer : buffer.toString("utf8");
684
+ }
685
+
686
+ module.exports = {
687
+ hash,
688
+ hmac,
689
+ safeEqual,
690
+ hashPassword,
691
+ verifyPassword,
692
+ needsRehash,
693
+ generateKey,
694
+ encrypt,
695
+ decrypt,
696
+ signJWT,
697
+ decodeJWT,
698
+ verifyJWT,
699
+ totpSecret,
700
+ totp,
701
+ verifyTOTP,
702
+ totpURI,
703
+ randomBytes,
704
+ randomInt,
705
+ toBase64,
706
+ fromBase64,
707
+ };