@ix-xs/node-comfort 2.0.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.
- package/CHANGELOG.md +150 -0
- package/README.md +32 -30
- package/index.js +3 -3
- package/package.json +8 -7
- package/src/Arr.js +222 -58
- package/src/Async.js +322 -90
- package/src/Cache.js +148 -44
- package/src/Checker.js +257 -26
- package/src/Cli.js +404 -145
- package/src/Color.js +224 -83
- package/src/Crypto.js +481 -106
- package/src/Emitter.js +104 -37
- package/src/Env.js +176 -42
- package/src/FS.js +203 -60
- package/src/Func.js +226 -76
- package/src/Http.js +311 -149
- package/src/Id.js +281 -96
- package/src/Logger.js +302 -78
- package/src/Num.js +182 -60
- package/src/Obj.js +340 -125
- package/src/SQLite.js +426 -105
- package/src/Schema.js +602 -87
- package/src/Str.js +316 -86
- package/src/Sys.js +197 -54
- package/src/Time.js +572 -317
- package/src/Utils.js +62 -41
- package/src/errors.js +54 -11
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +59 -17
- package/types/src/Arr.d.ts +78 -0
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +52 -1
- package/types/src/Async.d.ts.map +1 -1
- package/types/src/Cache.d.ts +33 -7
- package/types/src/Cache.d.ts.map +1 -1
- package/types/src/Checker.d.ts +155 -2
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +97 -15
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +7 -1
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +99 -13
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +36 -17
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +31 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +36 -5
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +147 -19
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +113 -43
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts +60 -8
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +53 -2
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +48 -7
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +67 -4
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +123 -14
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +256 -7
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +49 -1
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +51 -6
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +130 -34
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +15 -11
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +27 -3
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
package/src/Id.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Unique ids: UUID v4 and v7, ULID, nanoid-style ids,
|
|
5
|
-
* and short codes.
|
|
6
|
-
* `node:crypto`, with no bias.
|
|
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.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
9
|
-
* id.uuidv7();
|
|
10
|
-
* id.nano();
|
|
11
|
-
* id.
|
|
12
|
-
* id.
|
|
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
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/** @type {typeof import("node:crypto") | undefined} */
|
|
16
17
|
let _nodeCrypto;
|
|
17
18
|
const _crypto = () => (_nodeCrypto ??= require("node:crypto"));
|
|
18
|
-
const nodeCrypto = require("./Crypto.js");
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Encodings for `token()`.
|
|
22
22
|
* @typedef {"hex" | "base64" | "base64url" | "base58" | "base32"} TokenEncoding
|
|
23
23
|
*/
|
|
24
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
|
+
|
|
25
31
|
/**
|
|
26
32
|
* Options for `snowflake()` and `parseSnowflake()`.
|
|
27
33
|
* @typedef {object} SnowflakeOptions
|
|
@@ -42,32 +48,98 @@ const nodeCrypto = require("./Crypto.js");
|
|
|
42
48
|
|
|
43
49
|
const NANO_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
44
50
|
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
51
|
+
const BASE62_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
52
|
+
const CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
45
53
|
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
46
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
|
+
};
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
const
|
|
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;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reserves `count` fresh random bytes in the pool.
|
|
83
|
+
* @param {number} count At most `POOL_SIZE`.
|
|
84
|
+
* @returns {number} Where they start.
|
|
85
|
+
*/
|
|
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
|
+
};
|
|
50
95
|
|
|
51
96
|
/**
|
|
52
|
-
* Secure random characters, with rejection sampling so there's no bias.
|
|
53
|
-
* @param {string} alphabet
|
|
54
97
|
* @param {number} size
|
|
55
|
-
* @
|
|
98
|
+
* @param {string} what
|
|
99
|
+
*/
|
|
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}
|
|
56
114
|
*/
|
|
57
|
-
const
|
|
58
|
-
const
|
|
115
|
+
const _sampler = (alphabet) => {
|
|
116
|
+
const known = _samplers.get(alphabet);
|
|
117
|
+
if (known) return known;
|
|
118
|
+
const len = typeof alphabet === "string" ? alphabet.length : 0;
|
|
59
119
|
if (len < 2 || len > 256) throw new RangeError("The alphabet must contain between 2 and 256 characters");
|
|
60
|
-
const mask = (2 <<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
137
|
}
|
|
69
|
-
|
|
70
|
-
|
|
138
|
+
return out;
|
|
139
|
+
};
|
|
140
|
+
if (_samplers.size >= 100) _samplers.clear();
|
|
141
|
+
_samplers.set(alphabet, sample);
|
|
142
|
+
return sample;
|
|
71
143
|
};
|
|
72
144
|
|
|
73
145
|
/**
|
|
@@ -104,7 +176,7 @@ const _base32 = (buffer) => {
|
|
|
104
176
|
let value = 0;
|
|
105
177
|
let out = "";
|
|
106
178
|
for (const byte of buffer) {
|
|
107
|
-
value = (value << 8) | byte;
|
|
179
|
+
value = ((value << 8) | byte) & 0xffff;
|
|
108
180
|
bits += 8;
|
|
109
181
|
while (bits >= 5) {
|
|
110
182
|
out += CROCKFORD[(value >>> (bits - 5)) & 31];
|
|
@@ -115,6 +187,18 @@ const _base32 = (buffer) => {
|
|
|
115
187
|
return out;
|
|
116
188
|
};
|
|
117
189
|
|
|
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
|
+
|
|
118
202
|
/**
|
|
119
203
|
* A random UUID (version 4). For ids that sort by date, see `uuidv7()`.
|
|
120
204
|
*
|
|
@@ -127,14 +211,41 @@ function uuid() {
|
|
|
127
211
|
return _crypto().randomUUID();
|
|
128
212
|
}
|
|
129
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
|
+
|
|
130
240
|
let _v7LastMs = 0;
|
|
131
241
|
let _v7Counter = 0;
|
|
242
|
+
let _v7Head = "";
|
|
132
243
|
|
|
133
244
|
/**
|
|
134
245
|
* A UUID v7: it starts with a timestamp, so ids sort by creation time.
|
|
135
246
|
* That keeps database indexes compact, which makes them great primary
|
|
136
247
|
* keys. Ids from the same process always increase, even within a
|
|
137
|
-
* millisecond.
|
|
248
|
+
* millisecond, and even if the system clock steps back.
|
|
138
249
|
*
|
|
139
250
|
* @example
|
|
140
251
|
* id.uuidv7(); // "0190a5c4-7c1e-7b2a-9f3e-1c2d3e4f5a6b"
|
|
@@ -143,66 +254,108 @@ let _v7Counter = 0;
|
|
|
143
254
|
* @returns {string}
|
|
144
255
|
*/
|
|
145
256
|
function uuidv7() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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`;
|
|
154
266
|
} else {
|
|
155
|
-
|
|
156
|
-
_v7Counter = _crypto().randomInt(0, 0x800);
|
|
267
|
+
_v7Counter++;
|
|
157
268
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
bytes[8] = 0x80 | (bytes[8] & 0x3f);
|
|
163
|
-
const hex = bytes.toString("hex");
|
|
164
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
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
|
+
);
|
|
165
273
|
}
|
|
166
274
|
|
|
167
275
|
let _ulidLastMs = -1;
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
};
|
|
170
292
|
|
|
171
293
|
/**
|
|
172
294
|
* A ULID: 26 URL-safe characters that sort by creation time. Ids made in
|
|
173
295
|
* the same millisecond still increase.
|
|
174
296
|
*
|
|
175
297
|
* @example
|
|
176
|
-
* id.ulid();
|
|
177
|
-
* id.ulid(Date.UTC(2024, 0, 1));
|
|
298
|
+
* id.ulid(); // "01HQ3Z8K9V3N5W7Y2XJ4T6R8PB"
|
|
299
|
+
* id.ulid(Date.UTC(2024, 0, 1)); // for a given time
|
|
178
300
|
*
|
|
179
|
-
* @param {number} [time]
|
|
301
|
+
* @param {number | Date} [time] The moment to encode. Defaults to now.
|
|
180
302
|
* @returns {string}
|
|
303
|
+
* @throws {RangeError} If the time is not a valid timestamp.
|
|
181
304
|
*/
|
|
182
305
|
function ulid(time) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
let i =
|
|
189
|
-
|
|
190
|
-
if (i < 0) throw new RangeError("ULID overflow: too many ids in the same millisecond");
|
|
191
|
-
random[i]++;
|
|
192
|
-
} else {
|
|
193
|
-
random = [..._crypto().randomBytes(16)].map((b) => b & 31);
|
|
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];
|
|
312
|
+
return out;
|
|
194
313
|
}
|
|
195
|
-
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
if (now > _ulidLastMs) {
|
|
196
316
|
_ulidLastMs = now;
|
|
197
|
-
|
|
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]++;
|
|
198
327
|
}
|
|
199
|
-
let
|
|
200
|
-
let
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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];
|
|
204
355
|
}
|
|
205
|
-
|
|
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];
|
|
206
359
|
}
|
|
207
360
|
|
|
208
361
|
/**
|
|
@@ -219,7 +372,7 @@ function ulid(time) {
|
|
|
219
372
|
* @returns {string}
|
|
220
373
|
*/
|
|
221
374
|
function nano(size = 21, alphabet = NANO_ALPHABET) {
|
|
222
|
-
return
|
|
375
|
+
return _sampler(alphabet)(size);
|
|
223
376
|
}
|
|
224
377
|
|
|
225
378
|
/**
|
|
@@ -234,8 +387,26 @@ function nano(size = 21, alphabet = NANO_ALPHABET) {
|
|
|
234
387
|
* @returns {(size?: number) => string}
|
|
235
388
|
*/
|
|
236
389
|
function customAlphabet(alphabet, size = 21) {
|
|
237
|
-
|
|
238
|
-
return (length = size) =>
|
|
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)}`;
|
|
239
410
|
}
|
|
240
411
|
|
|
241
412
|
/**
|
|
@@ -251,6 +422,8 @@ function customAlphabet(alphabet, size = 21) {
|
|
|
251
422
|
* @returns {string}
|
|
252
423
|
*/
|
|
253
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.
|
|
254
427
|
const buffer = _crypto().randomBytes(bytes);
|
|
255
428
|
if (encoding === "base58") return _base58(buffer);
|
|
256
429
|
if (encoding === "base32") return _base32(buffer);
|
|
@@ -269,8 +442,8 @@ function token(bytes = 32, encoding = "hex") {
|
|
|
269
442
|
* @param {string} [alphabet="ABCDEFGHJKMNPQRSTUVWXYZ23456789"]
|
|
270
443
|
* @returns {string}
|
|
271
444
|
*/
|
|
272
|
-
function code(length = 6, alphabet =
|
|
273
|
-
return
|
|
445
|
+
function code(length = 6, alphabet = CODE_ALPHABET) {
|
|
446
|
+
return _sampler(alphabet)(length);
|
|
274
447
|
}
|
|
275
448
|
|
|
276
449
|
let _snowflakeLastMs = 0;
|
|
@@ -285,6 +458,7 @@ let _snowflakeIncrement = 0;
|
|
|
285
458
|
*
|
|
286
459
|
* @param {SnowflakeOptions} [options]
|
|
287
460
|
* @returns {string}
|
|
461
|
+
* @throws {RangeError} If the epoch is in the future.
|
|
288
462
|
*/
|
|
289
463
|
function snowflake(options = {}) {
|
|
290
464
|
const epoch = options.epoch ?? DISCORD_EPOCH;
|
|
@@ -297,9 +471,9 @@ function snowflake(options = {}) {
|
|
|
297
471
|
_snowflakeLastMs = now;
|
|
298
472
|
_snowflakeIncrement = 0;
|
|
299
473
|
}
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
return ((BigInt(now - epoch) << 22n) |
|
|
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();
|
|
303
477
|
}
|
|
304
478
|
|
|
305
479
|
/**
|
|
@@ -311,42 +485,49 @@ function snowflake(options = {}) {
|
|
|
311
485
|
* @param {string | bigint} id
|
|
312
486
|
* @param {SnowflakeOptions} [options] Use the epoch it was created with.
|
|
313
487
|
* @returns {ParsedSnowflake}
|
|
488
|
+
* @throws {TypeError} If the id is not a string of digits or a bigint.
|
|
314
489
|
*/
|
|
315
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
|
+
}
|
|
316
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);
|
|
317
497
|
const timestamp = Number(value >> 22n) + (options.epoch ?? DISCORD_EPOCH);
|
|
318
498
|
return {
|
|
319
499
|
date: new Date(timestamp),
|
|
320
500
|
timestamp,
|
|
321
|
-
workerId:
|
|
322
|
-
processId:
|
|
323
|
-
increment:
|
|
501
|
+
workerId: (low >> 17) & 31,
|
|
502
|
+
processId: (low >> 12) & 31,
|
|
503
|
+
increment: low & 0xfff,
|
|
324
504
|
};
|
|
325
505
|
}
|
|
326
506
|
|
|
327
507
|
/**
|
|
328
|
-
* When a ULID or
|
|
508
|
+
* When a UUID v7, a ULID or an ObjectId was created.
|
|
329
509
|
*
|
|
330
510
|
* @example
|
|
331
|
-
* id.timestamp(id.ulid());
|
|
332
|
-
* id.timestamp(id.
|
|
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
|
|
333
514
|
*
|
|
334
515
|
* @param {string} value
|
|
335
516
|
* @returns {Date | undefined}
|
|
336
517
|
*/
|
|
337
518
|
function timestamp(value) {
|
|
338
519
|
const s = String(value);
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return new Date(ms);
|
|
346
|
-
}
|
|
347
|
-
return undefined;
|
|
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);
|
|
348
526
|
}
|
|
349
527
|
|
|
528
|
+
/** @type {Map<string, number>} */
|
|
529
|
+
const _counters = new Map();
|
|
530
|
+
|
|
350
531
|
/**
|
|
351
532
|
* Readable sequential ids like `"user-1"`, `"user-2"`, counted per prefix.
|
|
352
533
|
* Only unique within the current process; handy in logs and tests.
|
|
@@ -359,8 +540,9 @@ function timestamp(value) {
|
|
|
359
540
|
* @returns {string}
|
|
360
541
|
*/
|
|
361
542
|
function seq(prefix = "") {
|
|
362
|
-
|
|
363
|
-
|
|
543
|
+
const next = (_counters.get(prefix) ?? 0) + 1;
|
|
544
|
+
_counters.set(prefix, next);
|
|
545
|
+
return prefix ? `${prefix}-${next}` : String(next);
|
|
364
546
|
}
|
|
365
547
|
|
|
366
548
|
/**
|
|
@@ -372,7 +554,7 @@ function seq(prefix = "") {
|
|
|
372
554
|
* @returns {string}
|
|
373
555
|
*/
|
|
374
556
|
function hash(value, options) {
|
|
375
|
-
return
|
|
557
|
+
return require("./Crypto.js").hash(value, options);
|
|
376
558
|
}
|
|
377
559
|
|
|
378
560
|
/**
|
|
@@ -385,7 +567,7 @@ function hash(value, options) {
|
|
|
385
567
|
* @returns {string}
|
|
386
568
|
*/
|
|
387
569
|
function hmac(value, secret, options) {
|
|
388
|
-
return
|
|
570
|
+
return require("./Crypto.js").hmac(value, secret, options);
|
|
389
571
|
}
|
|
390
572
|
|
|
391
573
|
/**
|
|
@@ -397,15 +579,18 @@ function hmac(value, secret, options) {
|
|
|
397
579
|
* @returns {boolean}
|
|
398
580
|
*/
|
|
399
581
|
function safeEqual(a, b) {
|
|
400
|
-
return
|
|
582
|
+
return require("./Crypto.js").safeEqual(a, b);
|
|
401
583
|
}
|
|
402
584
|
|
|
403
585
|
module.exports = {
|
|
404
586
|
uuid,
|
|
587
|
+
uuidv5,
|
|
405
588
|
uuidv7,
|
|
406
589
|
ulid,
|
|
590
|
+
objectId,
|
|
407
591
|
nano,
|
|
408
592
|
customAlphabet,
|
|
593
|
+
prefixed,
|
|
409
594
|
token,
|
|
410
595
|
code,
|
|
411
596
|
snowflake,
|