@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.
- package/CHANGELOG.md +340 -0
- package/README.md +137 -548
- package/index.js +80 -35
- package/package.json +161 -33
- package/src/Arr.js +1050 -418
- package/src/Async.js +817 -0
- package/src/Cache.js +433 -0
- package/src/Checker.js +1202 -539
- package/src/Cli.js +1293 -0
- package/src/Color.js +556 -0
- package/src/Crypto.js +1082 -0
- package/src/Emitter.js +356 -0
- package/src/Env.js +689 -0
- package/src/FS.js +1626 -894
- package/src/Func.js +823 -356
- package/src/Http.js +700 -0
- package/src/Id.js +568 -200
- package/src/Logger.js +963 -378
- package/src/Num.js +965 -360
- package/src/Obj.js +1077 -379
- package/src/SQLite.js +1629 -1005
- package/src/Schema.js +2101 -0
- package/src/Str.js +1209 -432
- package/src/Sys.js +654 -0
- package/src/Time.js +1533 -440
- package/src/Utils.js +342 -325
- package/src/errors.js +335 -0
- package/src/internal/shared.js +415 -0
- package/types/index.d.ts +735 -526
- package/types/src/Arr.d.ts +533 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +372 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +221 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +651 -232
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +664 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +300 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +620 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +189 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +412 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +717 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +508 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +444 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +199 -66
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +414 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +514 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +453 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +754 -308
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +1189 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +617 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +314 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +759 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +158 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +316 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/src/internal/shared.d.ts +123 -0
- package/types/src/internal/shared.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Id.js
CHANGED
|
@@ -1,40 +1,153 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
* @
|
|
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
|
|
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
|
-
* @
|
|
98
|
+
* @param {string} what
|
|
27
99
|
*/
|
|
28
|
-
const
|
|
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
|
-
*
|
|
32
|
-
* @
|
|
33
|
-
* @param {Buffer} buffer - Bytes to encode.
|
|
34
|
-
* @returns {string} The Base58 string.
|
|
146
|
+
* @param {Buffer} buffer
|
|
147
|
+
* @returns {string}
|
|
35
148
|
*/
|
|
36
|
-
const
|
|
37
|
-
if (buffer.length
|
|
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
|
|
52
|
-
for (let i = 0;
|
|
53
|
-
for (let i = digits.length - 1; i >= 0; i--)
|
|
54
|
-
return
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
};
|