@ix-xs/node-comfort 1.2.0 → 2.1.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 (78) hide show
  1. package/CHANGELOG.md +340 -0
  2. package/README.md +137 -548
  3. package/index.js +80 -35
  4. package/package.json +161 -33
  5. package/src/Arr.js +1050 -418
  6. package/src/Async.js +817 -0
  7. package/src/Cache.js +433 -0
  8. package/src/Checker.js +1202 -539
  9. package/src/Cli.js +1293 -0
  10. package/src/Color.js +556 -0
  11. package/src/Crypto.js +1082 -0
  12. package/src/Emitter.js +356 -0
  13. package/src/Env.js +689 -0
  14. package/src/FS.js +1626 -894
  15. package/src/Func.js +823 -356
  16. package/src/Http.js +700 -0
  17. package/src/Id.js +568 -200
  18. package/src/Logger.js +963 -378
  19. package/src/Num.js +965 -360
  20. package/src/Obj.js +1077 -379
  21. package/src/SQLite.js +1629 -1005
  22. package/src/Schema.js +2101 -0
  23. package/src/Str.js +1209 -432
  24. package/src/Sys.js +654 -0
  25. package/src/Time.js +1533 -440
  26. package/src/Utils.js +342 -325
  27. package/src/errors.js +335 -0
  28. package/src/internal/shared.js +415 -0
  29. package/types/index.d.ts +735 -526
  30. package/types/src/Arr.d.ts +533 -145
  31. package/types/src/Arr.d.ts.map +1 -1
  32. package/types/src/Async.d.ts +372 -0
  33. package/types/src/Async.d.ts.map +1 -0
  34. package/types/src/Cache.d.ts +221 -0
  35. package/types/src/Cache.d.ts.map +1 -0
  36. package/types/src/Checker.d.ts +651 -232
  37. package/types/src/Checker.d.ts.map +1 -1
  38. package/types/src/Cli.d.ts +664 -0
  39. package/types/src/Cli.d.ts.map +1 -0
  40. package/types/src/Color.d.ts +300 -0
  41. package/types/src/Color.d.ts.map +1 -0
  42. package/types/src/Crypto.d.ts +620 -0
  43. package/types/src/Crypto.d.ts.map +1 -0
  44. package/types/src/Emitter.d.ts +189 -0
  45. package/types/src/Emitter.d.ts.map +1 -0
  46. package/types/src/Env.d.ts +412 -0
  47. package/types/src/Env.d.ts.map +1 -0
  48. package/types/src/FS.d.ts +717 -238
  49. package/types/src/FS.d.ts.map +1 -1
  50. package/types/src/Func.d.ts +508 -135
  51. package/types/src/Func.d.ts.map +1 -1
  52. package/types/src/Http.d.ts +444 -0
  53. package/types/src/Http.d.ts.map +1 -0
  54. package/types/src/Id.d.ts +199 -66
  55. package/types/src/Id.d.ts.map +1 -1
  56. package/types/src/Logger.d.ts +414 -46
  57. package/types/src/Logger.d.ts.map +1 -1
  58. package/types/src/Num.d.ts +514 -137
  59. package/types/src/Num.d.ts.map +1 -1
  60. package/types/src/Obj.d.ts +453 -104
  61. package/types/src/Obj.d.ts.map +1 -1
  62. package/types/src/SQLite.d.ts +754 -308
  63. package/types/src/SQLite.d.ts.map +1 -1
  64. package/types/src/Schema.d.ts +1189 -0
  65. package/types/src/Schema.d.ts.map +1 -0
  66. package/types/src/Str.d.ts +617 -166
  67. package/types/src/Str.d.ts.map +1 -1
  68. package/types/src/Sys.d.ts +314 -0
  69. package/types/src/Sys.d.ts.map +1 -0
  70. package/types/src/Time.d.ts +759 -204
  71. package/types/src/Time.d.ts.map +1 -1
  72. package/types/src/Utils.d.ts +158 -102
  73. package/types/src/Utils.d.ts.map +1 -1
  74. package/types/src/errors.d.ts +316 -0
  75. package/types/src/errors.d.ts.map +1 -0
  76. package/types/src/internal/shared.d.ts +123 -0
  77. package/types/src/internal/shared.d.ts.map +1 -0
  78. package/types/index.d.ts.map +0 -1
package/src/Id.js CHANGED
@@ -1,40 +1,153 @@
1
- const crypto = require("node:crypto");
1
+ "use strict";
2
2
 
3
3
  /**
4
- * ID, token & hashing utilities.
4
+ * Unique ids: UUID v4, v5 and v7, ULID, ObjectId, nanoid-style ids,
5
+ * Snowflakes, tokens and short codes. Random ones use the secure generator
6
+ * of `node:crypto`, with no bias.
5
7
  *
6
- * Cryptographically-secure identifiers (UUID v4, ULID, nanoid-style tokens),
7
- * hashing helpers and constant-time comparison • all built on Node's native
8
- * `node:crypto`, with zero external dependencies.
8
+ * @example
9
+ * id.uuidv7(); // "0190a5c4-7c1e-7b2a-9f3e-1c2d3e4f5a6b", sorts by creation time
10
+ * id.nano(); // "V1StGXR8_Z5jdHi6B-myT"
11
+ * id.prefixed("user"); // "user_7Hq2mXc0LrT9aZ4bWk1PdE8s"
12
+ * id.code(); // "K7QF9X"
13
+ * id.token(32); // for API keys and reset links
9
14
  */
10
15
 
11
- const NANOID_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
16
+ /** @type {typeof import("node:crypto") | undefined} */
17
+ let _nodeCrypto;
18
+ const _crypto = () => (_nodeCrypto ??= require("node:crypto"));
19
+
20
+ /**
21
+ * Encodings for `token()`.
22
+ * @typedef {"hex" | "base64" | "base64url" | "base58" | "base32"} TokenEncoding
23
+ */
24
+
25
+ /**
26
+ * A namespace for `uuidv5()`: one of the four standard ones, or any UUID of
27
+ * your own.
28
+ * @typedef {"dns" | "url" | "oid" | "x500" | (string & {})} UUIDNamespace
29
+ */
30
+
31
+ /**
32
+ * Options for `snowflake()` and `parseSnowflake()`.
33
+ * @typedef {object} SnowflakeOptions
34
+ * @property {number} [epoch] Start of time, in ms. Defaults to Discord's (2015-01-01). Twitter uses `1288834974657`.
35
+ * @property {number} [workerId] 0 to 31. Defaults to `0`.
36
+ * @property {number} [processId] 0 to 31. Defaults to `process.pid % 32`.
37
+ */
38
+
39
+ /**
40
+ * A decoded Snowflake.
41
+ * @typedef {object} ParsedSnowflake
42
+ * @property {Date} date When it was created.
43
+ * @property {number} timestamp When it was created, in ms.
44
+ * @property {number} workerId
45
+ * @property {number} processId
46
+ * @property {number} increment Sequence number within the millisecond, 0 to 4095.
47
+ */
48
+
49
+ const NANO_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
12
50
  const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
13
- const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford base32
51
+ const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
52
+ const CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
53
+ const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
54
+ const DISCORD_EPOCH = 1420070400000;
55
+ const MAX_TIME = 2 ** 48 - 1;
56
+
57
+ /** @type {Record<string, string>} */
58
+ const NAMESPACES = {
59
+ dns: "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
60
+ url: "6ba7b811-9dad-11d1-80b4-00c04fd430c8",
61
+ oid: "6ba7b812-9dad-11d1-80b4-00c04fd430c8",
62
+ x500: "6ba7b814-9dad-11d1-80b4-00c04fd430c8",
63
+ };
64
+
65
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
66
+ const UUID_V7 = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
67
+ const ULID = /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/i;
68
+ const OBJECT_ID = /^[0-9a-f]{24}$/i;
69
+
70
+ /** @type {string[]} */
71
+ const HEX = [];
72
+ for (let i = 0; i < 256; i++) HEX.push(i.toString(16).padStart(2, "0"));
73
+
74
+ // Asking the system for random bytes is slow, so they are fetched 4 KB at a
75
+ // time and handed out from there.
76
+ const POOL_SIZE = 4096;
77
+ /** @type {Buffer | undefined} */
78
+ let _pool;
79
+ let _poolAt = POOL_SIZE;
14
80
 
15
81
  /**
16
- * Per-prefix counters for {@link module:Id.seq}.
17
- * @private
18
- * @type {Record<string, number>}
82
+ * Reserves `count` fresh random bytes in the pool.
83
+ * @param {number} count At most `POOL_SIZE`.
84
+ * @returns {number} Where they start.
19
85
  */
20
- const _seqCounters = {};
86
+ const _take = (count) => {
87
+ if (_poolAt + count > POOL_SIZE) {
88
+ _crypto().randomFillSync((_pool ??= Buffer.allocUnsafeSlow(POOL_SIZE)));
89
+ _poolAt = 0;
90
+ }
91
+ const at = _poolAt;
92
+ _poolAt += count;
93
+ return at;
94
+ };
21
95
 
22
96
  /**
23
- * Fills a buffer with cryptographically secure random bytes.
24
- * @private
25
97
  * @param {number} size
26
- * @returns {Buffer}
98
+ * @param {string} what
27
99
  */
28
- const _randomBytes = (size) => crypto.randomBytes(size);
100
+ const _checkSize = (size, what) => {
101
+ if (!Number.isInteger(size) || size < 0) throw new RangeError(`${what} must be a whole number, 0 or more, got ${size}`);
102
+ };
103
+
104
+ /** @type {Map<string, (size: number) => string>} */
105
+ const _samplers = new Map();
106
+
107
+ /**
108
+ * A function that draws random characters from an alphabet. Bytes that fall
109
+ * outside the alphabet are thrown away, so every character is as likely as
110
+ * the others.
111
+ *
112
+ * @param {string} alphabet
113
+ * @returns {(size: number) => string}
114
+ */
115
+ const _sampler = (alphabet) => {
116
+ const known = _samplers.get(alphabet);
117
+ if (known) return known;
118
+ const len = typeof alphabet === "string" ? alphabet.length : 0;
119
+ if (len < 2 || len > 256) throw new RangeError("The alphabet must contain between 2 and 256 characters");
120
+ const mask = (2 << (31 - Math.clz32((len - 1) | 1))) - 1;
121
+ /** @type {(size: number) => string} */
122
+ const sample = (size) => {
123
+ _checkSize(size, "The size");
124
+ let out = "";
125
+ let left = size;
126
+ while (left > 0) {
127
+ const count = Math.min(left, POOL_SIZE);
128
+ const at = _take(count);
129
+ const pool = /** @type {Buffer} */ (_pool);
130
+ for (let i = at; i < at + count; i++) {
131
+ const index = pool[i] & mask;
132
+ if (index < len) {
133
+ out += alphabet[index];
134
+ left--;
135
+ }
136
+ }
137
+ }
138
+ return out;
139
+ };
140
+ if (_samplers.size >= 100) _samplers.clear();
141
+ _samplers.set(alphabet, sample);
142
+ return sample;
143
+ };
29
144
 
30
145
  /**
31
- * Encodes a buffer as Base58 using the Bitcoin alphabet.
32
- * @private
33
- * @param {Buffer} buffer - Bytes to encode.
34
- * @returns {string} The Base58 string.
146
+ * @param {Buffer} buffer
147
+ * @returns {string}
35
148
  */
36
- const _toBase58 = (buffer) => {
37
- if (buffer.length === 0) return "";
149
+ const _base58 = (buffer) => {
150
+ if (!buffer.length) return "";
38
151
  const digits = [0];
39
152
  for (const byte of buffer) {
40
153
  let carry = byte;
@@ -48,188 +161,443 @@ const _toBase58 = (buffer) => {
48
161
  carry = Math.floor(carry / 58);
49
162
  }
50
163
  }
51
- let str = "";
52
- for (let i = 0; buffer[i] === 0 && i < buffer.length - 1; i++) str += BASE58_ALPHABET[0];
53
- for (let i = digits.length - 1; i >= 0; i--) str += BASE58_ALPHABET[digits[i]];
54
- return str;
164
+ let out = "";
165
+ for (let i = 0; i < buffer.length - 1 && buffer[i] === 0; i++) out += BASE58_ALPHABET[0];
166
+ for (let i = digits.length - 1; i >= 0; i--) out += BASE58_ALPHABET[digits[i]];
167
+ return out;
55
168
  };
56
169
 
57
- module.exports = {
58
- /**
59
- * Generates a random UUID v4.
60
- *
61
- * @example
62
- * nodeComfort.id.uuid(); // "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
63
- *
64
- * @returns {string} A UUID v4 string.
65
- */
66
- uuid() {
67
- return crypto.randomUUID();
68
- },
69
-
70
- /**
71
- * Generates a URL-safe, collision-resistant ID (nanoid-compatible alphabet).
72
- *
73
- * Uses secure randomness with unbiased rejection sampling.
74
- *
75
- * @example
76
- * nodeComfort.id.nano(); // "V1StGXR8_Z5jdHi6B-myT"
77
- * nodeComfort.id.nano(10); // "IRFa-VaY2b"
78
- *
79
- * @param {number} [size=21] - Desired length.
80
- * @param {string} [alphabet] - Custom alphabet.
81
- * @returns {string} The generated ID.
82
- */
83
- nano(size = 21, alphabet = NANOID_ALPHABET) {
84
- const len = alphabet.length;
85
- // Bitmask for unbiased sampling.
86
- const mask = (2 << Math.floor(Math.log2(len - 1))) - 1;
87
- const step = Math.ceil((1.6 * mask * size) / len);
88
- let id = "";
89
- while (id.length < size) {
90
- const bytes = _randomBytes(step);
91
- for (let i = 0; i < step && id.length < size; i++) {
92
- const index = bytes[i] & mask;
93
- if (index < len) id += alphabet[index];
94
- }
95
- }
96
- return id;
97
- },
98
-
99
- /**
100
- * Generates a ULID • a lexicographically-sortable, timestamp-prefixed ID.
101
- *
102
- * ULIDs sort by creation time and are a great primary-key alternative to UUIDs.
103
- *
104
- * @example
105
- * nodeComfort.id.ulid(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
106
- *
107
- * @param {number} [time=Date.now()] - Timestamp in ms to encode.
108
- * @returns {string} A 26-character ULID.
109
- */
110
- ulid(time = Date.now()) {
111
- let timeChars = "";
112
- let t = time;
113
- for (let i = 0; i < 10; i++) {
114
- timeChars = ULID_ALPHABET[t % 32] + timeChars;
115
- t = Math.floor(t / 32);
170
+ /**
171
+ * @param {Buffer} buffer
172
+ * @returns {string}
173
+ */
174
+ const _base32 = (buffer) => {
175
+ let bits = 0;
176
+ let value = 0;
177
+ let out = "";
178
+ for (const byte of buffer) {
179
+ value = ((value << 8) | byte) & 0xffff;
180
+ bits += 8;
181
+ while (bits >= 5) {
182
+ out += CROCKFORD[(value >>> (bits - 5)) & 31];
183
+ bits -= 5;
116
184
  }
185
+ }
186
+ if (bits > 0) out += CROCKFORD[(value << (5 - bits)) & 31];
187
+ return out;
188
+ };
117
189
 
118
- const bytes = _randomBytes(16);
119
- let randChars = "";
120
- for (let i = 0; i < 16; i++) {
121
- randChars += ULID_ALPHABET[bytes[i] % 32];
122
- }
123
- return timeChars + randChars;
124
- },
125
-
126
- /**
127
- * Generates a cryptographically-secure random token.
128
- *
129
- * @example
130
- * nodeComfort.id.token(); // 32-byte hex string
131
- * nodeComfort.id.token(16, "base64url"); // 16 bytes, URL-safe base64
132
- *
133
- * @param {number} [bytes=32] - Number of random bytes.
134
- * @param {"hex"|"base64"|"base64url"|"base58"} [encoding="hex"] - Output encoding.
135
- * @returns {string} The token.
136
- */
137
- token(bytes = 32, encoding = "hex") {
138
- const buffer = _randomBytes(bytes);
139
- if (encoding === "base58") return _toBase58(buffer);
140
- return buffer.toString(encoding);
141
- },
142
-
143
- /**
144
- * Generates a short, human-friendly random code (uppercase + digits by default).
145
- *
146
- * Ambiguous characters (`0/O`, `1/I`) are excluded, making it suitable for
147
- * coupons, OTP-like codes and invite links.
148
- *
149
- * @example
150
- * nodeComfort.id.code(); // "K7QF9X"
151
- * nodeComfort.id.code(4, "1234"); // "3142"
152
- *
153
- * @param {number} [length=6] - Number of characters.
154
- * @param {string} [alphabet="ABCDEFGHJKLMNPQRSTUVWXYZ23456789"] - Characters to pick from.
155
- * @returns {string} The random code.
156
- */
157
- code(length = 6, alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789") {
158
- const bytes = _randomBytes(length);
159
- let out = "";
160
- for (let i = 0; i < length; i++) {
161
- out += alphabet[bytes[i] % alphabet.length];
162
- }
190
+ /**
191
+ * @param {number | Date | undefined} time
192
+ * @param {string} what
193
+ * @returns {number | undefined}
194
+ */
195
+ const _time = (time, what) => {
196
+ if (time === undefined) return undefined;
197
+ const ms = +time;
198
+ if (!Number.isInteger(ms) || ms < 0 || ms > MAX_TIME) throw new RangeError(`${what}: the time must be a date or a timestamp in ms, got ${String(time)}`);
199
+ return ms;
200
+ };
201
+
202
+ /**
203
+ * A random UUID (version 4). For ids that sort by date, see `uuidv7()`.
204
+ *
205
+ * @example
206
+ * id.uuid(); // "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
207
+ *
208
+ * @returns {string}
209
+ */
210
+ function uuid() {
211
+ return _crypto().randomUUID();
212
+ }
213
+
214
+ /**
215
+ * A UUID v5: the same name in the same namespace always gives the same id.
216
+ * Use it to derive a stable id from something you already have, like a URL
217
+ * or an email, without storing a mapping.
218
+ *
219
+ * @example
220
+ * id.uuidv5("example.com", "dns"); // "cfbff0d1-9375-5685-968c-48ce8b15ae17", every time
221
+ * id.uuidv5("https://example.com/a", "url");
222
+ * id.uuidv5("ada@example.com", MY_APP_NAMESPACE); // any UUID works as a namespace
223
+ *
224
+ * @param {string | Buffer | Uint8Array} name
225
+ * @param {UUIDNamespace} namespace `"dns"`, `"url"`, `"oid"`, `"x500"` or a UUID.
226
+ * @returns {string}
227
+ */
228
+ function uuidv5(name, namespace) {
229
+ const space = Object.hasOwn(NAMESPACES, namespace) ? NAMESPACES[namespace] : namespace;
230
+ if (typeof space !== "string" || !UUID.test(space)) {
231
+ throw new TypeError(`uuidv5(): the namespace must be "dns", "url", "oid", "x500" or a UUID, got ${String(namespace)}`);
232
+ }
233
+ const bytes = _crypto().createHash("sha1").update(Buffer.from(space.replace(/-/g, ""), "hex")).update(name).digest();
234
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
235
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
236
+ const hex = bytes.toString("hex", 0, 16);
237
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
238
+ }
239
+
240
+ let _v7LastMs = 0;
241
+ let _v7Counter = 0;
242
+ let _v7Head = "";
243
+
244
+ /**
245
+ * A UUID v7: it starts with a timestamp, so ids sort by creation time.
246
+ * That keeps database indexes compact, which makes them great primary
247
+ * keys. Ids from the same process always increase, even within a
248
+ * millisecond, and even if the system clock steps back.
249
+ *
250
+ * @example
251
+ * id.uuidv7(); // "0190a5c4-7c1e-7b2a-9f3e-1c2d3e4f5a6b"
252
+ * id.timestamp(id.uuidv7()); // when it was created
253
+ *
254
+ * @returns {string}
255
+ */
256
+ function uuidv7() {
257
+ const now = Date.now();
258
+ const at = _take(10);
259
+ const pool = /** @type {Buffer} */ (_pool);
260
+ if (now > _v7LastMs || _v7Counter === 0xfff) {
261
+ // A new millisecond, or the counter is used up and we borrow the next one.
262
+ _v7LastMs = now > _v7LastMs ? now : _v7LastMs + 1;
263
+ _v7Counter = ((pool[at + 8] << 8) | pool[at + 9]) & 0x7ff;
264
+ const time = _v7LastMs.toString(16).padStart(12, "0");
265
+ _v7Head = `${time.slice(0, 8)}-${time.slice(8)}-7`;
266
+ } else {
267
+ _v7Counter++;
268
+ }
269
+ return (
270
+ `${_v7Head}${HEX[_v7Counter >> 8][1]}${HEX[_v7Counter & 0xff]}-${HEX[0x80 | (pool[at] & 0x3f)]}${HEX[pool[at + 1]]}-` +
271
+ HEX[pool[at + 2]] + HEX[pool[at + 3]] + HEX[pool[at + 4]] + HEX[pool[at + 5]] + HEX[pool[at + 6]] + HEX[pool[at + 7]]
272
+ );
273
+ }
274
+
275
+ let _ulidLastMs = -1;
276
+ let _ulidHead = "";
277
+ const _ulidRandom = new Uint8Array(16);
278
+
279
+ /**
280
+ * @param {number} ms
281
+ * @returns {string}
282
+ */
283
+ const _ulidTime = (ms) => {
284
+ let head = "";
285
+ let left = ms;
286
+ for (let i = 0; i < 10; i++) {
287
+ head = CROCKFORD[left % 32] + head;
288
+ left = Math.floor(left / 32);
289
+ }
290
+ return head;
291
+ };
292
+
293
+ /**
294
+ * A ULID: 26 URL-safe characters that sort by creation time. Ids made in
295
+ * the same millisecond still increase.
296
+ *
297
+ * @example
298
+ * id.ulid(); // "01HQ3Z8K9V3N5W7Y2XJ4T6R8PB"
299
+ * id.ulid(Date.UTC(2024, 0, 1)); // for a given time
300
+ *
301
+ * @param {number | Date} [time] The moment to encode. Defaults to now.
302
+ * @returns {string}
303
+ * @throws {RangeError} If the time is not a valid timestamp.
304
+ */
305
+ function ulid(time) {
306
+ const given = _time(time, "ulid()");
307
+ if (given !== undefined) {
308
+ const at = _take(16);
309
+ const pool = /** @type {Buffer} */ (_pool);
310
+ let out = _ulidTime(given);
311
+ for (let i = at; i < at + 16; i++) out += CROCKFORD[pool[i] & 31];
163
312
  return out;
164
- },
165
-
166
- /**
167
- * Hashes a value with a chosen algorithm.
168
- *
169
- * @example
170
- * nodeComfort.id.hash("hello"); // sha256 hex
171
- * nodeComfort.id.hash("hello", { algorithm: "md5" });
172
- *
173
- * @param {string|Buffer} value - Data to hash.
174
- * @param {{ algorithm?: string, encoding?: "hex"|"base64"|"base64url" }} [options] - Hash options.
175
- * @returns {string} The digest.
176
- */
177
- hash(value, options = {}) {
178
- const algorithm = options.algorithm ?? "sha256";
179
- const encoding = options.encoding ?? "hex";
180
- return crypto.createHash(algorithm).update(value).digest(encoding);
181
- },
182
-
183
- /**
184
- * Computes an HMAC signature for a value using a secret key.
185
- *
186
- * @example
187
- * nodeComfort.id.hmac("payload", "secret"); // sha256 HMAC hex
188
- *
189
- * @param {string|Buffer} value - Data to sign.
190
- * @param {string|Buffer} secret - Secret key.
191
- * @param {{ algorithm?: string, encoding?: "hex"|"base64"|"base64url" }} [options] - HMAC options.
192
- * @returns {string} The signature.
193
- */
194
- hmac(value, secret, options = {}) {
195
- const algorithm = options.algorithm ?? "sha256";
196
- const encoding = options.encoding ?? "hex";
197
- return crypto.createHmac(algorithm, secret).update(value).digest(encoding);
198
- },
199
-
200
- /**
201
- * Compares two strings/buffers in constant time to prevent timing attacks.
202
- *
203
- * Use this when comparing secrets, tokens, or signatures.
204
- *
205
- * @example
206
- * if (nodeComfort.id.safeEqual(provided, expected)) { ... }
207
- *
208
- * @param {string|Buffer} a - First value.
209
- * @param {string|Buffer} b - Second value.
210
- * @returns {boolean} True if equal.
211
- */
212
- safeEqual(a, b) {
213
- const bufA = Buffer.isBuffer(a) ? a : Buffer.from(String(a));
214
- const bufB = Buffer.isBuffer(b) ? b : Buffer.from(String(b));
215
- if (bufA.length !== bufB.length) return false;
216
- return crypto.timingSafeEqual(bufA, bufB);
217
- },
218
-
219
- /**
220
- * Returns a monotonically-increasing counter ID prefixed with an optional label.
221
- *
222
- * Useful for readable, unique keys within a single process run.
223
- *
224
- * @example
225
- * nodeComfort.id.seq("user"); // "user-1"
226
- * nodeComfort.id.seq("user"); // "user-2"
227
- *
228
- * @param {string} [prefix=""] - Optional prefix.
229
- * @returns {string} The sequential ID.
230
- */
231
- seq(prefix = "") {
232
- _seqCounters[prefix] = (_seqCounters[prefix] ?? 0) + 1;
233
- return prefix ? `${prefix}-${_seqCounters[prefix]}` : String(_seqCounters[prefix]);
234
- },
313
+ }
314
+ const now = Date.now();
315
+ if (now > _ulidLastMs) {
316
+ _ulidLastMs = now;
317
+ _ulidHead = _ulidTime(now);
318
+ const at = _take(16);
319
+ const pool = /** @type {Buffer} */ (_pool);
320
+ for (let i = 0; i < 16; i++) _ulidRandom[i] = pool[at + i] & 31;
321
+ } else {
322
+ // Same millisecond, or the clock stepped back: count up from the last id.
323
+ let i = 15;
324
+ while (i >= 0 && _ulidRandom[i] === 31) _ulidRandom[i--] = 0;
325
+ if (i < 0) throw new RangeError("ULID overflow: too many ids in the same millisecond");
326
+ _ulidRandom[i]++;
327
+ }
328
+ let out = _ulidHead;
329
+ for (let i = 0; i < 16; i++) out += CROCKFORD[_ulidRandom[i]];
330
+ return out;
331
+ }
332
+
333
+ let _oidProcess = "";
334
+ let _oidCounter = 0;
335
+
336
+ /**
337
+ * A MongoDB-style ObjectId: 24 hex characters that start with the time, in
338
+ * seconds, and increase within the process.
339
+ *
340
+ * @example
341
+ * id.objectId(); // "65f2a9c47b1e4d0c9a3f2b10"
342
+ * id.timestamp(id.objectId()); // when it was created, to the second
343
+ * id.objectId(new Date("2024-01-01")); // for a given time
344
+ *
345
+ * @param {number | Date} [time] The moment to encode. Defaults to now.
346
+ * @returns {string}
347
+ * @throws {RangeError} If the time is not a valid timestamp.
348
+ */
349
+ function objectId(time) {
350
+ if (!_oidProcess) {
351
+ const at = _take(8);
352
+ const pool = /** @type {Buffer} */ (_pool);
353
+ for (let i = at; i < at + 5; i++) _oidProcess += HEX[pool[i]];
354
+ _oidCounter = (pool[at + 5] << 16) | (pool[at + 6] << 8) | pool[at + 7];
355
+ }
356
+ const seconds = Math.floor((_time(time, "objectId()") ?? Date.now()) / 1000) % 2 ** 32;
357
+ _oidCounter = (_oidCounter + 1) & 0xffffff;
358
+ return seconds.toString(16).padStart(8, "0") + _oidProcess + HEX[_oidCounter >> 16] + HEX[(_oidCounter >> 8) & 0xff] + HEX[_oidCounter & 0xff];
359
+ }
360
+
361
+ /**
362
+ * A short, URL-safe random id, like `nanoid`. At 21 characters, collisions
363
+ * are as unlikely as with a UUID.
364
+ *
365
+ * @example
366
+ * id.nano(); // "V1StGXR8_Z5jdHi6B-myT"
367
+ * id.nano(10); // "IRFa-VaY2b"
368
+ * id.nano(8, "0123456789abcdef"); // "4f90d13a"
369
+ *
370
+ * @param {number} [size=21]
371
+ * @param {string} [alphabet] 2 to 256 characters. Defaults to letters, digits, `_` and `-`.
372
+ * @returns {string}
373
+ */
374
+ function nano(size = 21, alphabet = NANO_ALPHABET) {
375
+ return _sampler(alphabet)(size);
376
+ }
377
+
378
+ /**
379
+ * Makes an id generator with your own alphabet and length.
380
+ *
381
+ * @example
382
+ * const orderId = id.customAlphabet("0123456789ABCDEF", 12);
383
+ * orderId(); // "4F1A09C2BB7E"
384
+ *
385
+ * @param {string} alphabet
386
+ * @param {number} [size=21]
387
+ * @returns {(size?: number) => string}
388
+ */
389
+ function customAlphabet(alphabet, size = 21) {
390
+ const sample = _sampler(alphabet);
391
+ return (length = size) => sample(length);
392
+ }
393
+
394
+ /**
395
+ * A random id with a prefix that says what it identifies, like Stripe's
396
+ * `cus_...` and `sub_...`. A glance at a log line or a support ticket tells
397
+ * you what kind of thing the id points to.
398
+ *
399
+ * @example
400
+ * id.prefixed("user"); // "user_7Hq2mXc0LrT9aZ4bWk1PdE8s"
401
+ * id.prefixed("inv", 12); // "inv_Q0f8ZrT1bK4m"
402
+ *
403
+ * @param {string} prefix
404
+ * @param {number} [size=24] How many random characters come after the prefix (letters and digits).
405
+ * @returns {string}
406
+ */
407
+ function prefixed(prefix, size = 24) {
408
+ if (typeof prefix !== "string" || !prefix) throw new TypeError("prefixed(): the prefix must be a non-empty string");
409
+ return `${prefix}_${_sampler(BASE62_ALPHABET)(size)}`;
410
+ }
411
+
412
+ /**
413
+ * A random token for API keys, session ids, reset links...
414
+ *
415
+ * @example
416
+ * id.token(); // 64 hex characters
417
+ * id.token(16, "base64url"); // 22 URL-safe characters
418
+ * id.token(16, "base58"); // no look-alike characters
419
+ *
420
+ * @param {number} [bytes=32] How much randomness, in bytes.
421
+ * @param {TokenEncoding} [encoding="hex"]
422
+ * @returns {string}
423
+ */
424
+ function token(bytes = 32, encoding = "hex") {
425
+ _checkSize(bytes, "The number of bytes");
426
+ // Tokens are secrets: their bytes never sit in the shared pool.
427
+ const buffer = _crypto().randomBytes(bytes);
428
+ if (encoding === "base58") return _base58(buffer);
429
+ if (encoding === "base32") return _base32(buffer);
430
+ return buffer.toString(encoding);
431
+ }
432
+
433
+ /**
434
+ * A short code that's easy to read aloud and type: no `0/O` or `1/I/L`
435
+ * mix-ups. For invites, coupons and verification codes.
436
+ *
437
+ * @example
438
+ * id.code(); // "K7QF9X"
439
+ * id.code(4, "0123456789"); // "3920"
440
+ *
441
+ * @param {number} [length=6]
442
+ * @param {string} [alphabet="ABCDEFGHJKMNPQRSTUVWXYZ23456789"]
443
+ * @returns {string}
444
+ */
445
+ function code(length = 6, alphabet = CODE_ALPHABET) {
446
+ return _sampler(alphabet)(length);
447
+ }
448
+
449
+ let _snowflakeLastMs = 0;
450
+ let _snowflakeIncrement = 0;
451
+
452
+ /**
453
+ * A Snowflake id, the 64-bit format used by Discord and Twitter. Returned
454
+ * as a string because it doesn't fit in a JavaScript number.
455
+ *
456
+ * @example
457
+ * id.snowflake(); // "1215361932810932224"
458
+ *
459
+ * @param {SnowflakeOptions} [options]
460
+ * @returns {string}
461
+ * @throws {RangeError} If the epoch is in the future.
462
+ */
463
+ function snowflake(options = {}) {
464
+ const epoch = options.epoch ?? DISCORD_EPOCH;
465
+ let now = Date.now();
466
+ if (now <= _snowflakeLastMs) {
467
+ _snowflakeIncrement = (_snowflakeIncrement + 1) & 0xfff;
468
+ if (_snowflakeIncrement === 0) _snowflakeLastMs++;
469
+ now = _snowflakeLastMs;
470
+ } else {
471
+ _snowflakeLastMs = now;
472
+ _snowflakeIncrement = 0;
473
+ }
474
+ if (!(now >= epoch)) throw new RangeError(`snowflake(): the epoch must be a timestamp in the past, got ${epoch}`);
475
+ const low = (((options.workerId ?? 0) & 31) << 17) | (((options.processId ?? process.pid) & 31) << 12) | _snowflakeIncrement;
476
+ return ((BigInt(now - epoch) << 22n) | BigInt(low)).toString();
477
+ }
478
+
479
+ /**
480
+ * Decodes a Snowflake. With the default epoch, it reads Discord ids.
481
+ *
482
+ * @example
483
+ * id.parseSnowflake("175928847299117063").date; // 2016-04-30T11:18:25.796Z
484
+ *
485
+ * @param {string | bigint} id
486
+ * @param {SnowflakeOptions} [options] Use the epoch it was created with.
487
+ * @returns {ParsedSnowflake}
488
+ * @throws {TypeError} If the id is not a string of digits or a bigint.
489
+ */
490
+ function parseSnowflake(id, options = {}) {
491
+ if (typeof id !== "bigint" && !(typeof id === "string" && /^\d{1,20}$/.test(id))) {
492
+ throw new TypeError(`parseSnowflake(): expected a string of digits or a bigint, got ${typeof id === "string" ? JSON.stringify(id) : typeof id}`);
493
+ }
494
+ const value = BigInt(id);
495
+ if (value < 0n) throw new TypeError("parseSnowflake(): a Snowflake can't be negative");
496
+ const low = Number(value & 0x3fffffn);
497
+ const timestamp = Number(value >> 22n) + (options.epoch ?? DISCORD_EPOCH);
498
+ return {
499
+ date: new Date(timestamp),
500
+ timestamp,
501
+ workerId: (low >> 17) & 31,
502
+ processId: (low >> 12) & 31,
503
+ increment: low & 0xfff,
504
+ };
505
+ }
506
+
507
+ /**
508
+ * When a UUID v7, a ULID or an ObjectId was created.
509
+ *
510
+ * @example
511
+ * id.timestamp(id.ulid()); // now
512
+ * id.timestamp(id.objectId()); // now, to the second
513
+ * id.timestamp(id.uuid()); // undefined, v4 has no time in it
514
+ *
515
+ * @param {string} value
516
+ * @returns {Date | undefined}
517
+ */
518
+ function timestamp(value) {
519
+ const s = String(value);
520
+ if (s.length === 36) return UUID_V7.test(s) ? new Date(parseInt(s.slice(0, 8) + s.slice(9, 13), 16)) : undefined;
521
+ if (s.length === 24) return OBJECT_ID.test(s) ? new Date(parseInt(s.slice(0, 8), 16) * 1000) : undefined;
522
+ if (s.length !== 26 || !ULID.test(s)) return undefined;
523
+ let ms = 0;
524
+ for (let i = 0; i < 10; i++) ms = ms * 32 + CROCKFORD.indexOf(s[i].toUpperCase());
525
+ return new Date(ms);
526
+ }
527
+
528
+ /** @type {Map<string, number>} */
529
+ const _counters = new Map();
530
+
531
+ /**
532
+ * Readable sequential ids like `"user-1"`, `"user-2"`, counted per prefix.
533
+ * Only unique within the current process; handy in logs and tests.
534
+ *
535
+ * @example
536
+ * id.seq("user"); // "user-1"
537
+ * id.seq("user"); // "user-2"
538
+ *
539
+ * @param {string} [prefix=""]
540
+ * @returns {string}
541
+ */
542
+ function seq(prefix = "") {
543
+ const next = (_counters.get(prefix) ?? 0) + 1;
544
+ _counters.set(prefix, next);
545
+ return prefix ? `${prefix}-${next}` : String(next);
546
+ }
547
+
548
+ /**
549
+ * Hashes a value.
550
+ *
551
+ * @deprecated Use `nc.crypto.hash()`. This alias goes away in 3.0.
552
+ * @param {string | Buffer | Uint8Array} value
553
+ * @param {import("./Crypto").HashOptions} [options]
554
+ * @returns {string}
555
+ */
556
+ function hash(value, options) {
557
+ return require("./Crypto.js").hash(value, options);
558
+ }
559
+
560
+ /**
561
+ * Signs a value with HMAC.
562
+ *
563
+ * @deprecated Use `nc.crypto.hmac()`. This alias goes away in 3.0.
564
+ * @param {string | Buffer | Uint8Array} value
565
+ * @param {string | Buffer | Uint8Array} secret
566
+ * @param {import("./Crypto").HashOptions} [options]
567
+ * @returns {string}
568
+ */
569
+ function hmac(value, secret, options) {
570
+ return require("./Crypto.js").hmac(value, secret, options);
571
+ }
572
+
573
+ /**
574
+ * Compares two secrets in constant time.
575
+ *
576
+ * @deprecated Use `nc.crypto.safeEqual()`. This alias goes away in 3.0.
577
+ * @param {string | Buffer | Uint8Array} a
578
+ * @param {string | Buffer | Uint8Array} b
579
+ * @returns {boolean}
580
+ */
581
+ function safeEqual(a, b) {
582
+ return require("./Crypto.js").safeEqual(a, b);
583
+ }
584
+
585
+ module.exports = {
586
+ uuid,
587
+ uuidv5,
588
+ uuidv7,
589
+ ulid,
590
+ objectId,
591
+ nano,
592
+ customAlphabet,
593
+ prefixed,
594
+ token,
595
+ code,
596
+ snowflake,
597
+ parseSnowflake,
598
+ timestamp,
599
+ seq,
600
+ hash,
601
+ hmac,
602
+ safeEqual,
235
603
  };