@ppabari/strio 1.0.2
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 +85 -0
- package/LICENSE +21 -0
- package/README.md +565 -0
- package/dist/bin/strio.js +1492 -0
- package/dist/index.cjs +1913 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +885 -0
- package/dist/index.d.ts +885 -0
- package/dist/index.js +1881 -0
- package/dist/index.js.map +1 -0
- package/dist/zod-CINbFpp9.d.cts +211 -0
- package/dist/zod-CINbFpp9.d.ts +211 -0
- package/dist/zod.cjs +165 -0
- package/dist/zod.cjs.map +1 -0
- package/dist/zod.d.cts +1 -0
- package/dist/zod.d.ts +1 -0
- package/dist/zod.js +163 -0
- package/dist/zod.js.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/core.ts
|
|
4
|
+
function getRandomByte() {
|
|
5
|
+
const buf = new Uint8Array(1);
|
|
6
|
+
crypto.getRandomValues(buf);
|
|
7
|
+
return buf[0];
|
|
8
|
+
}
|
|
9
|
+
function getRandomChar(charset) {
|
|
10
|
+
const len = charset.length;
|
|
11
|
+
const maxValid = 256 - 256 % len;
|
|
12
|
+
let byte;
|
|
13
|
+
do {
|
|
14
|
+
byte = getRandomByte();
|
|
15
|
+
} while (byte >= maxValid);
|
|
16
|
+
return charset[byte % len];
|
|
17
|
+
}
|
|
18
|
+
function getRandomChars(charset, count2) {
|
|
19
|
+
const len = charset.length;
|
|
20
|
+
const maxValid = 256 - 256 % len;
|
|
21
|
+
const result = [];
|
|
22
|
+
const bufSize = Math.ceil(count2 * 1.4) + 32;
|
|
23
|
+
const buf = new Uint8Array(bufSize);
|
|
24
|
+
while (result.length < count2) {
|
|
25
|
+
crypto.getRandomValues(buf);
|
|
26
|
+
for (let i = 0; i < buf.length && result.length < count2; i++) {
|
|
27
|
+
const byte = buf[i];
|
|
28
|
+
if (byte < maxValid) {
|
|
29
|
+
result.push(charset[byte % len]);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/seeded.ts
|
|
37
|
+
function rotl32(x, k) {
|
|
38
|
+
return (x << k | x >>> 32 - k) >>> 0;
|
|
39
|
+
}
|
|
40
|
+
function seedState(seed) {
|
|
41
|
+
let a = 2654435769 >>> 0;
|
|
42
|
+
let b = 1818371886 >>> 0;
|
|
43
|
+
for (let i = 0; i < seed.length; i++) {
|
|
44
|
+
const ch = seed.charCodeAt(i);
|
|
45
|
+
a = Math.imul(a ^ ch, 2654435769) + 1388355149 >>> 0;
|
|
46
|
+
b = Math.imul(b ^ ch, 2246822507) + 3266489909 >>> 0;
|
|
47
|
+
}
|
|
48
|
+
function sm32(v) {
|
|
49
|
+
v = Math.imul(v ^ v >>> 16, 73244475) + 461845907 >>> 0;
|
|
50
|
+
v = Math.imul(v ^ v >>> 16, 73244475) + 3864292196 >>> 0;
|
|
51
|
+
return (v ^ v >>> 16) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
return [
|
|
54
|
+
sm32(a),
|
|
55
|
+
sm32(b),
|
|
56
|
+
sm32(a ^ 3735928559),
|
|
57
|
+
sm32(b ^ 3131961357)
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
function nextUint32(s) {
|
|
61
|
+
const result = Math.imul(rotl32(Math.imul(s[1], 5) >>> 0, 7), 9) >>> 0;
|
|
62
|
+
const t = s[1] << 9 >>> 0;
|
|
63
|
+
s[2] = (s[2] ^ s[0]) >>> 0;
|
|
64
|
+
s[3] = (s[3] ^ s[1]) >>> 0;
|
|
65
|
+
s[1] = (s[1] ^ s[2]) >>> 0;
|
|
66
|
+
s[0] = (s[0] ^ s[3]) >>> 0;
|
|
67
|
+
s[2] = (s[2] ^ t) >>> 0;
|
|
68
|
+
s[3] = rotl32(s[3], 11);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function seededChar(charset, state) {
|
|
72
|
+
const len = charset.length;
|
|
73
|
+
const maxValid = Math.floor(4294967296 / len) * len;
|
|
74
|
+
let n;
|
|
75
|
+
do {
|
|
76
|
+
n = nextUint32(state);
|
|
77
|
+
} while (n >= maxValid);
|
|
78
|
+
return charset[n % len];
|
|
79
|
+
}
|
|
80
|
+
function generateSeeded({ seed, charset, length }) {
|
|
81
|
+
const state = seedState(seed);
|
|
82
|
+
const chars = [];
|
|
83
|
+
for (let i = 0; i < length; i++) {
|
|
84
|
+
chars.push(seededChar(charset, state));
|
|
85
|
+
}
|
|
86
|
+
return chars.join("");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/charset-aliases.ts
|
|
90
|
+
var CHARSET_ALIASES = {
|
|
91
|
+
/**
|
|
92
|
+
* Base16 — hex digits, lowercase.
|
|
93
|
+
* Use for: hash representations, color codes, compact binary encoding.
|
|
94
|
+
*/
|
|
95
|
+
base16: "0123456789abcdef",
|
|
96
|
+
/** Base16 uppercase variant */
|
|
97
|
+
base16upper: "0123456789ABCDEF",
|
|
98
|
+
/**
|
|
99
|
+
* Base32 — RFC 4648. Uppercase alpha + digits, no padding chars.
|
|
100
|
+
* Use for: case-insensitive tokens, OTP secrets (Google Authenticator compatible).
|
|
101
|
+
*/
|
|
102
|
+
base32: "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
|
|
103
|
+
/**
|
|
104
|
+
* Base32 hex — RFC 4648 §7. Sortable alternative to standard base32.
|
|
105
|
+
*/
|
|
106
|
+
base32hex: "0123456789ABCDEFGHIJKLMNOPQRSTUV",
|
|
107
|
+
/**
|
|
108
|
+
* Base36 — digits + lowercase alpha.
|
|
109
|
+
* Use for: short URLs, case-insensitive IDs, number system conversions.
|
|
110
|
+
*/
|
|
111
|
+
base36: "0123456789abcdefghijklmnopqrstuvwxyz",
|
|
112
|
+
/**
|
|
113
|
+
* Base58 — Bitcoin alphabet. No 0/O/I/l to avoid visual ambiguity.
|
|
114
|
+
* Use for: Bitcoin addresses, IPFS CIDs, human-readable tokens.
|
|
115
|
+
*/
|
|
116
|
+
base58: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
|
|
117
|
+
/**
|
|
118
|
+
* Base62 — digits + lowercase + uppercase. Max density for alphanumeric.
|
|
119
|
+
* Use for: URL shorteners, compact IDs, high-entropy tokens.
|
|
120
|
+
*/
|
|
121
|
+
base62: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
122
|
+
/**
|
|
123
|
+
* Base64 URL-safe — RFC 4648 §5. No +/= padding; URL and filename safe.
|
|
124
|
+
* Use for: JWT components, URL tokens, file names.
|
|
125
|
+
*/
|
|
126
|
+
base64url: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_",
|
|
127
|
+
/**
|
|
128
|
+
* Alphanumeric — same as base62. Alias for clarity.
|
|
129
|
+
*/
|
|
130
|
+
alphanumeric: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
131
|
+
/**
|
|
132
|
+
* Alpha — letters only, mixed case.
|
|
133
|
+
*/
|
|
134
|
+
alpha: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
|
135
|
+
/**
|
|
136
|
+
* Numeric — digits only.
|
|
137
|
+
*/
|
|
138
|
+
numeric: "0123456789",
|
|
139
|
+
/**
|
|
140
|
+
* Hex — alias for base16 lowercase.
|
|
141
|
+
*/
|
|
142
|
+
hex: "0123456789abcdef",
|
|
143
|
+
/**
|
|
144
|
+
* Crockford Base32 — human-friendly, case-insensitive, excludes I/L/O/U.
|
|
145
|
+
* Use for: serial numbers, redemption codes, user-facing IDs.
|
|
146
|
+
*/
|
|
147
|
+
crockford32: "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
148
|
+
};
|
|
149
|
+
function resolveCharsetAlias(value) {
|
|
150
|
+
return CHARSET_ALIASES[value] ?? value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/charsets.ts
|
|
154
|
+
var CHARSETS = {
|
|
155
|
+
numeric: "0123456789",
|
|
156
|
+
lowercase: "abcdefghijklmnopqrstuvwxyz",
|
|
157
|
+
uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
158
|
+
symbols: "!@#$%^&*()_+[]{}<>?",
|
|
159
|
+
alphanumeric: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
160
|
+
hex: "0123456789abcdef",
|
|
161
|
+
// Ambiguous characters that are easy to confuse when reading/transcribing
|
|
162
|
+
ambiguous: "0Ol1I"
|
|
163
|
+
};
|
|
164
|
+
function dedup(str) {
|
|
165
|
+
return [...new Set(str)].join("");
|
|
166
|
+
}
|
|
167
|
+
function buildCharset(options2) {
|
|
168
|
+
let pool;
|
|
169
|
+
if (options2.charset !== void 0) {
|
|
170
|
+
pool = dedup(resolveCharsetAlias(options2.charset));
|
|
171
|
+
if (pool.length < 2) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"Custom charset must contain at least 2 unique characters."
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return pool;
|
|
177
|
+
}
|
|
178
|
+
const {
|
|
179
|
+
numeric = true,
|
|
180
|
+
lowercase = true,
|
|
181
|
+
uppercase = true,
|
|
182
|
+
symbols = false
|
|
183
|
+
} = options2;
|
|
184
|
+
pool = "";
|
|
185
|
+
if (numeric) pool += CHARSETS.numeric;
|
|
186
|
+
if (lowercase) pool += CHARSETS.lowercase;
|
|
187
|
+
if (uppercase) pool += CHARSETS.uppercase;
|
|
188
|
+
if (symbols) pool += CHARSETS.symbols;
|
|
189
|
+
if (!pool) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"At least one character type must be enabled (numeric, lowercase, uppercase, or symbols)."
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (options2.readable) {
|
|
195
|
+
for (const ch of CHARSETS.ambiguous) {
|
|
196
|
+
pool = pool.split(ch).join("");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (options2.exclude) {
|
|
200
|
+
const excludeChars = typeof options2.exclude === "string" ? options2.exclude : options2.exclude.join("");
|
|
201
|
+
for (const ch of excludeChars) {
|
|
202
|
+
pool = pool.split(ch).join("");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
pool = dedup(pool);
|
|
206
|
+
if (pool.length < 2) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"After applying exclusions, the character pool has fewer than 2 unique characters. Loosen your exclude/readable settings or enable more character types."
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return pool;
|
|
212
|
+
}
|
|
213
|
+
function alphabetSubset(fullPool) {
|
|
214
|
+
return [...fullPool].filter((ch) => /[a-zA-Z]/.test(ch)).join("");
|
|
215
|
+
}
|
|
216
|
+
function numericSubset(fullPool) {
|
|
217
|
+
return [...fullPool].filter((ch) => /[0-9]/.test(ch)).join("");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/generator.ts
|
|
221
|
+
function expandPattern(pattern, fullCharset) {
|
|
222
|
+
const upper = [...fullCharset].filter((c) => /[A-Z]/.test(c)).join("") || CHARSETS.uppercase;
|
|
223
|
+
const lower = [...fullCharset].filter((c) => /[a-z]/.test(c)).join("") || CHARSETS.lowercase;
|
|
224
|
+
const alphanum = [...fullCharset].filter((c) => /[a-zA-Z0-9]/.test(c)).join("") || CHARSETS.alphanumeric;
|
|
225
|
+
const result = [];
|
|
226
|
+
let i = 0;
|
|
227
|
+
while (i < pattern.length) {
|
|
228
|
+
const ch = pattern[i];
|
|
229
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
230
|
+
result.push(pattern[i + 1]);
|
|
231
|
+
i += 2;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
switch (ch) {
|
|
235
|
+
case "#":
|
|
236
|
+
result.push(getRandomChar(CHARSETS.numeric));
|
|
237
|
+
break;
|
|
238
|
+
case "A":
|
|
239
|
+
result.push(getRandomChar(upper));
|
|
240
|
+
break;
|
|
241
|
+
case "a":
|
|
242
|
+
result.push(getRandomChar(lower));
|
|
243
|
+
break;
|
|
244
|
+
case "*":
|
|
245
|
+
result.push(getRandomChar(fullCharset));
|
|
246
|
+
break;
|
|
247
|
+
case "?":
|
|
248
|
+
result.push(getRandomChar(alphanum));
|
|
249
|
+
break;
|
|
250
|
+
default:
|
|
251
|
+
result.push(ch);
|
|
252
|
+
}
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
return result.join("");
|
|
256
|
+
}
|
|
257
|
+
function generateOne(options2) {
|
|
258
|
+
const {
|
|
259
|
+
length = 16,
|
|
260
|
+
prefix = "",
|
|
261
|
+
suffix = "",
|
|
262
|
+
startWith = "any",
|
|
263
|
+
pattern,
|
|
264
|
+
seed,
|
|
265
|
+
...charsetOptions
|
|
266
|
+
} = options2;
|
|
267
|
+
const charset = buildCharset(charsetOptions);
|
|
268
|
+
if (seed !== void 0) {
|
|
269
|
+
if (pattern !== void 0) {
|
|
270
|
+
const patternLength = [...pattern].filter((_, i) => {
|
|
271
|
+
return true;
|
|
272
|
+
}).length;
|
|
273
|
+
const seededStr2 = generateSeeded({ seed, charset, length: patternLength });
|
|
274
|
+
return `${prefix}${seededStr2}${suffix}`;
|
|
275
|
+
}
|
|
276
|
+
const randomLength2 = length - prefix.length - suffix.length;
|
|
277
|
+
if (randomLength2 <= 0) return `${prefix}${suffix}`;
|
|
278
|
+
const seededStr = generateSeeded({ seed, charset, length: randomLength2 });
|
|
279
|
+
return `${prefix}${seededStr}${suffix}`;
|
|
280
|
+
}
|
|
281
|
+
if (pattern !== void 0) {
|
|
282
|
+
const expanded = expandPattern(pattern, charset);
|
|
283
|
+
return `${prefix}${expanded}${suffix}`;
|
|
284
|
+
}
|
|
285
|
+
const randomLength = length - prefix.length - suffix.length;
|
|
286
|
+
if (randomLength < 0) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`prefix ('${prefix}') + suffix ('${suffix}') is longer than the total length (${length}). Increase length or shorten the affixes.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (randomLength === 0) {
|
|
292
|
+
return `${prefix}${suffix}`;
|
|
293
|
+
}
|
|
294
|
+
let firstCharset;
|
|
295
|
+
switch (startWith) {
|
|
296
|
+
case "alphabet": {
|
|
297
|
+
const alpha = alphabetSubset(charset);
|
|
298
|
+
if (!alpha) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
"startWith: 'alphabet' requires at least one of lowercase or uppercase to be enabled."
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
firstCharset = alpha;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case "numeric": {
|
|
307
|
+
const num = numericSubset(charset);
|
|
308
|
+
if (!num) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"startWith: 'numeric' requires numeric to be enabled."
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
firstCharset = num;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
default:
|
|
317
|
+
firstCharset = charset;
|
|
318
|
+
}
|
|
319
|
+
const firstChar = getRandomChar(firstCharset);
|
|
320
|
+
if (randomLength === 1) {
|
|
321
|
+
return `${prefix}${firstChar}${suffix}`;
|
|
322
|
+
}
|
|
323
|
+
const rest = getRandomChars(charset, randomLength - 1).join("");
|
|
324
|
+
return `${prefix}${firstChar}${rest}${suffix}`;
|
|
325
|
+
}
|
|
326
|
+
function generateBatch(options2, count2) {
|
|
327
|
+
if (count2 <= 0) {
|
|
328
|
+
throw new Error("count must be a positive integer.");
|
|
329
|
+
}
|
|
330
|
+
if (!Number.isInteger(count2)) {
|
|
331
|
+
throw new Error("count must be an integer.");
|
|
332
|
+
}
|
|
333
|
+
const results = /* @__PURE__ */ new Set();
|
|
334
|
+
let attempts = 0;
|
|
335
|
+
const maxAttempts = count2 * 10 + 1e3;
|
|
336
|
+
while (results.size < count2) {
|
|
337
|
+
if (++attempts > maxAttempts) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Could not generate ${count2} unique strings after ${maxAttempts} attempts. The charset and length combination may not support this many unique values.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
results.add(generateOne(options2));
|
|
343
|
+
}
|
|
344
|
+
return [...results];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/short-id.ts
|
|
348
|
+
function computeChecksum(payload, charset) {
|
|
349
|
+
const n = charset.length;
|
|
350
|
+
let sum = 0;
|
|
351
|
+
for (let i = 0; i < payload.length; i++) {
|
|
352
|
+
const idx = charset.indexOf(payload[i]);
|
|
353
|
+
if (idx === -1) return charset[0];
|
|
354
|
+
const weight = i % 2 === 0 ? 1 : 2;
|
|
355
|
+
sum += idx * weight;
|
|
356
|
+
}
|
|
357
|
+
return charset[sum % n];
|
|
358
|
+
}
|
|
359
|
+
function generateId(options2 = {}) {
|
|
360
|
+
const {
|
|
361
|
+
prefix,
|
|
362
|
+
separator = "_",
|
|
363
|
+
randomLength = 12,
|
|
364
|
+
charset: rawCharset = "base58",
|
|
365
|
+
checksum = true
|
|
366
|
+
} = options2;
|
|
367
|
+
const resolvedCharset2 = resolveCharsetAlias(rawCharset);
|
|
368
|
+
const charset = buildCharset({ charset: resolvedCharset2 });
|
|
369
|
+
if (randomLength < 4) throw new Error("randomLength must be at least 4.");
|
|
370
|
+
const randomPart = getRandomChars(charset, randomLength).join("");
|
|
371
|
+
const checksumChar = checksum ? computeChecksum(randomPart, charset) : "";
|
|
372
|
+
const randomSection = randomPart + checksumChar;
|
|
373
|
+
if (prefix) {
|
|
374
|
+
return `${prefix}${separator}${randomSection}`;
|
|
375
|
+
}
|
|
376
|
+
return randomSection;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/expiring-token.ts
|
|
380
|
+
var B62 = CHARSET_ALIASES["base62"];
|
|
381
|
+
var B62_LEN = B62.length;
|
|
382
|
+
var EXPIRY_CHARS = 8;
|
|
383
|
+
function encodeBase62(n, width) {
|
|
384
|
+
const digits = [];
|
|
385
|
+
let v = Math.floor(n);
|
|
386
|
+
for (let i = 0; i < width; i++) {
|
|
387
|
+
digits.unshift(B62[v % B62_LEN]);
|
|
388
|
+
v = Math.floor(v / B62_LEN);
|
|
389
|
+
}
|
|
390
|
+
return digits.join("");
|
|
391
|
+
}
|
|
392
|
+
function generateExpiringToken(options2 = {}) {
|
|
393
|
+
const { ttlSeconds = 900, payloadLength = 24 } = options2;
|
|
394
|
+
if (ttlSeconds <= 0) throw new Error("ttlSeconds must be a positive number.");
|
|
395
|
+
if (payloadLength < 8) throw new Error("payloadLength must be at least 8.");
|
|
396
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
|
|
397
|
+
const expiryEpoch = Math.floor(expiresAt.getTime() / 1e3);
|
|
398
|
+
const expiryPart = encodeBase62(expiryEpoch, EXPIRY_CHARS);
|
|
399
|
+
const payload = generateOne({
|
|
400
|
+
length: payloadLength,
|
|
401
|
+
charset: B62
|
|
402
|
+
});
|
|
403
|
+
const token = expiryPart + payload;
|
|
404
|
+
return {
|
|
405
|
+
token,
|
|
406
|
+
expiresAt,
|
|
407
|
+
length: token.length
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/passphrase.ts
|
|
412
|
+
var WORD_LIST = [
|
|
413
|
+
"able",
|
|
414
|
+
"acid",
|
|
415
|
+
"aged",
|
|
416
|
+
"also",
|
|
417
|
+
"area",
|
|
418
|
+
"army",
|
|
419
|
+
"away",
|
|
420
|
+
"back",
|
|
421
|
+
"ball",
|
|
422
|
+
"band",
|
|
423
|
+
"bank",
|
|
424
|
+
"base",
|
|
425
|
+
"bath",
|
|
426
|
+
"bear",
|
|
427
|
+
"beat",
|
|
428
|
+
"been",
|
|
429
|
+
"bell",
|
|
430
|
+
"best",
|
|
431
|
+
"bird",
|
|
432
|
+
"bite",
|
|
433
|
+
"blow",
|
|
434
|
+
"blue",
|
|
435
|
+
"boat",
|
|
436
|
+
"body",
|
|
437
|
+
"bomb",
|
|
438
|
+
"bond",
|
|
439
|
+
"bone",
|
|
440
|
+
"book",
|
|
441
|
+
"bore",
|
|
442
|
+
"born",
|
|
443
|
+
"both",
|
|
444
|
+
"bowl",
|
|
445
|
+
"burn",
|
|
446
|
+
"busy",
|
|
447
|
+
"call",
|
|
448
|
+
"calm",
|
|
449
|
+
"camp",
|
|
450
|
+
"card",
|
|
451
|
+
"care",
|
|
452
|
+
"cart",
|
|
453
|
+
"case",
|
|
454
|
+
"cash",
|
|
455
|
+
"cast",
|
|
456
|
+
"cave",
|
|
457
|
+
"cell",
|
|
458
|
+
"chat",
|
|
459
|
+
"chip",
|
|
460
|
+
"city",
|
|
461
|
+
"clam",
|
|
462
|
+
"clap",
|
|
463
|
+
"clay",
|
|
464
|
+
"clip",
|
|
465
|
+
"club",
|
|
466
|
+
"clue",
|
|
467
|
+
"coal",
|
|
468
|
+
"coat",
|
|
469
|
+
"code",
|
|
470
|
+
"coil",
|
|
471
|
+
"cold",
|
|
472
|
+
"come",
|
|
473
|
+
"cook",
|
|
474
|
+
"cool",
|
|
475
|
+
"copy",
|
|
476
|
+
"core",
|
|
477
|
+
"cork",
|
|
478
|
+
"corn",
|
|
479
|
+
"cost",
|
|
480
|
+
"cozy",
|
|
481
|
+
"crab",
|
|
482
|
+
"crew",
|
|
483
|
+
"crop",
|
|
484
|
+
"crow",
|
|
485
|
+
"curb",
|
|
486
|
+
"cure",
|
|
487
|
+
"dark",
|
|
488
|
+
"dart",
|
|
489
|
+
"data",
|
|
490
|
+
"date",
|
|
491
|
+
"dawn",
|
|
492
|
+
"deal",
|
|
493
|
+
"dear",
|
|
494
|
+
"deck",
|
|
495
|
+
"deed",
|
|
496
|
+
"deep",
|
|
497
|
+
"deft",
|
|
498
|
+
"dent",
|
|
499
|
+
"desk",
|
|
500
|
+
"dial",
|
|
501
|
+
"dice",
|
|
502
|
+
"diet",
|
|
503
|
+
"dime",
|
|
504
|
+
"dine",
|
|
505
|
+
"dirt",
|
|
506
|
+
"dish",
|
|
507
|
+
"disk",
|
|
508
|
+
"dock",
|
|
509
|
+
"dome",
|
|
510
|
+
"door",
|
|
511
|
+
"dose",
|
|
512
|
+
"dove",
|
|
513
|
+
"down",
|
|
514
|
+
"draw",
|
|
515
|
+
"drew",
|
|
516
|
+
"drip",
|
|
517
|
+
"drop",
|
|
518
|
+
"drum",
|
|
519
|
+
"dual",
|
|
520
|
+
"dune",
|
|
521
|
+
"dusk",
|
|
522
|
+
"dust",
|
|
523
|
+
"each",
|
|
524
|
+
"earl",
|
|
525
|
+
"earn",
|
|
526
|
+
"ease",
|
|
527
|
+
"east",
|
|
528
|
+
"edge",
|
|
529
|
+
"else",
|
|
530
|
+
"emit",
|
|
531
|
+
"envy",
|
|
532
|
+
"epic",
|
|
533
|
+
"even",
|
|
534
|
+
"exam",
|
|
535
|
+
"exit",
|
|
536
|
+
"expo",
|
|
537
|
+
"face",
|
|
538
|
+
"fact",
|
|
539
|
+
"fail",
|
|
540
|
+
"fair",
|
|
541
|
+
"fall",
|
|
542
|
+
"fame",
|
|
543
|
+
"farm",
|
|
544
|
+
"fast",
|
|
545
|
+
"fate",
|
|
546
|
+
"fawn",
|
|
547
|
+
"fear",
|
|
548
|
+
"feat",
|
|
549
|
+
"feed",
|
|
550
|
+
"feel",
|
|
551
|
+
"feet",
|
|
552
|
+
"fell",
|
|
553
|
+
"felt",
|
|
554
|
+
"fern",
|
|
555
|
+
"fest",
|
|
556
|
+
"film",
|
|
557
|
+
"find",
|
|
558
|
+
"fine",
|
|
559
|
+
"fire",
|
|
560
|
+
"firm",
|
|
561
|
+
"fish",
|
|
562
|
+
"fist",
|
|
563
|
+
"flag",
|
|
564
|
+
"flat",
|
|
565
|
+
"flaw",
|
|
566
|
+
"flew",
|
|
567
|
+
"flip",
|
|
568
|
+
"flow",
|
|
569
|
+
"foam",
|
|
570
|
+
"fold",
|
|
571
|
+
"folk",
|
|
572
|
+
"fond",
|
|
573
|
+
"font",
|
|
574
|
+
"food",
|
|
575
|
+
"fool",
|
|
576
|
+
"foot",
|
|
577
|
+
"ford",
|
|
578
|
+
"fork",
|
|
579
|
+
"form",
|
|
580
|
+
"fort",
|
|
581
|
+
"foul",
|
|
582
|
+
"four",
|
|
583
|
+
"free",
|
|
584
|
+
"frog",
|
|
585
|
+
"from",
|
|
586
|
+
"fuel",
|
|
587
|
+
"full",
|
|
588
|
+
"fund",
|
|
589
|
+
"fuse",
|
|
590
|
+
"gain",
|
|
591
|
+
"gale",
|
|
592
|
+
"game",
|
|
593
|
+
"gang",
|
|
594
|
+
"gate",
|
|
595
|
+
"gave",
|
|
596
|
+
"gaze",
|
|
597
|
+
"gear",
|
|
598
|
+
"gift",
|
|
599
|
+
"give",
|
|
600
|
+
"glad",
|
|
601
|
+
"glow",
|
|
602
|
+
"glue",
|
|
603
|
+
"goal",
|
|
604
|
+
"goat",
|
|
605
|
+
"gold",
|
|
606
|
+
"golf",
|
|
607
|
+
"gone",
|
|
608
|
+
"good",
|
|
609
|
+
"grab",
|
|
610
|
+
"gray",
|
|
611
|
+
"grew",
|
|
612
|
+
"grid",
|
|
613
|
+
"grin",
|
|
614
|
+
"grip",
|
|
615
|
+
"grow",
|
|
616
|
+
"gulf",
|
|
617
|
+
"gust",
|
|
618
|
+
"half",
|
|
619
|
+
"hall",
|
|
620
|
+
"halt",
|
|
621
|
+
"hand",
|
|
622
|
+
"hang",
|
|
623
|
+
"hard",
|
|
624
|
+
"hare",
|
|
625
|
+
"harm",
|
|
626
|
+
"harp",
|
|
627
|
+
"hash",
|
|
628
|
+
"haul",
|
|
629
|
+
"hawk",
|
|
630
|
+
"head",
|
|
631
|
+
"heal",
|
|
632
|
+
"heap",
|
|
633
|
+
"heat",
|
|
634
|
+
"heel",
|
|
635
|
+
"held",
|
|
636
|
+
"helm",
|
|
637
|
+
"help",
|
|
638
|
+
"herb",
|
|
639
|
+
"herd",
|
|
640
|
+
"hero",
|
|
641
|
+
"hide",
|
|
642
|
+
"high",
|
|
643
|
+
"hike",
|
|
644
|
+
"hill",
|
|
645
|
+
"hint",
|
|
646
|
+
"hire",
|
|
647
|
+
"hold",
|
|
648
|
+
"hole",
|
|
649
|
+
"home",
|
|
650
|
+
"hood",
|
|
651
|
+
"hook",
|
|
652
|
+
"hope",
|
|
653
|
+
"horn",
|
|
654
|
+
"hose",
|
|
655
|
+
"host",
|
|
656
|
+
"hour",
|
|
657
|
+
"huge",
|
|
658
|
+
"hulk",
|
|
659
|
+
"hull",
|
|
660
|
+
"hunt",
|
|
661
|
+
"hymn",
|
|
662
|
+
"idea",
|
|
663
|
+
"idle",
|
|
664
|
+
"inch",
|
|
665
|
+
"into",
|
|
666
|
+
"iron",
|
|
667
|
+
"isle",
|
|
668
|
+
"item",
|
|
669
|
+
"jade",
|
|
670
|
+
"jail",
|
|
671
|
+
"jest",
|
|
672
|
+
"join",
|
|
673
|
+
"joke",
|
|
674
|
+
"jolt",
|
|
675
|
+
"jump",
|
|
676
|
+
"just",
|
|
677
|
+
"keen",
|
|
678
|
+
"keep",
|
|
679
|
+
"kick",
|
|
680
|
+
"kind",
|
|
681
|
+
"king",
|
|
682
|
+
"knee",
|
|
683
|
+
"knew",
|
|
684
|
+
"knot",
|
|
685
|
+
"know",
|
|
686
|
+
"lace",
|
|
687
|
+
"lame",
|
|
688
|
+
"lamp",
|
|
689
|
+
"land",
|
|
690
|
+
"lane",
|
|
691
|
+
"lark",
|
|
692
|
+
"lash",
|
|
693
|
+
"last",
|
|
694
|
+
"late",
|
|
695
|
+
"lawn",
|
|
696
|
+
"lead",
|
|
697
|
+
"leaf",
|
|
698
|
+
"lean",
|
|
699
|
+
"leap",
|
|
700
|
+
"left",
|
|
701
|
+
"lens",
|
|
702
|
+
"lift",
|
|
703
|
+
"like",
|
|
704
|
+
"limb",
|
|
705
|
+
"lime",
|
|
706
|
+
"link",
|
|
707
|
+
"lion",
|
|
708
|
+
"list",
|
|
709
|
+
"live",
|
|
710
|
+
"load",
|
|
711
|
+
"loan",
|
|
712
|
+
"lock",
|
|
713
|
+
"loft",
|
|
714
|
+
"long",
|
|
715
|
+
"look",
|
|
716
|
+
"loop",
|
|
717
|
+
"lore",
|
|
718
|
+
"lose",
|
|
719
|
+
"lost",
|
|
720
|
+
"loud",
|
|
721
|
+
"love",
|
|
722
|
+
"luck",
|
|
723
|
+
"lure",
|
|
724
|
+
"lush",
|
|
725
|
+
"made",
|
|
726
|
+
"mail",
|
|
727
|
+
"main",
|
|
728
|
+
"make",
|
|
729
|
+
"mall",
|
|
730
|
+
"mane",
|
|
731
|
+
"many",
|
|
732
|
+
"mark",
|
|
733
|
+
"mass",
|
|
734
|
+
"mast",
|
|
735
|
+
"math",
|
|
736
|
+
"maze",
|
|
737
|
+
"mead",
|
|
738
|
+
"meal",
|
|
739
|
+
"mean",
|
|
740
|
+
"meet",
|
|
741
|
+
"melt",
|
|
742
|
+
"memo",
|
|
743
|
+
"menu",
|
|
744
|
+
"mesh",
|
|
745
|
+
"mild",
|
|
746
|
+
"mile",
|
|
747
|
+
"mill",
|
|
748
|
+
"mime",
|
|
749
|
+
"mind",
|
|
750
|
+
"mine",
|
|
751
|
+
"mint",
|
|
752
|
+
"miss",
|
|
753
|
+
"mist",
|
|
754
|
+
"mode",
|
|
755
|
+
"mold",
|
|
756
|
+
"mole",
|
|
757
|
+
"mood",
|
|
758
|
+
"moon",
|
|
759
|
+
"more",
|
|
760
|
+
"most",
|
|
761
|
+
"move",
|
|
762
|
+
"much",
|
|
763
|
+
"mule",
|
|
764
|
+
"myth",
|
|
765
|
+
"nail",
|
|
766
|
+
"name",
|
|
767
|
+
"neck",
|
|
768
|
+
"need",
|
|
769
|
+
"nest",
|
|
770
|
+
"next",
|
|
771
|
+
"nine",
|
|
772
|
+
"node",
|
|
773
|
+
"noel",
|
|
774
|
+
"norm",
|
|
775
|
+
"nose",
|
|
776
|
+
"note",
|
|
777
|
+
"noun",
|
|
778
|
+
"oath",
|
|
779
|
+
"once",
|
|
780
|
+
"only",
|
|
781
|
+
"open",
|
|
782
|
+
"oven",
|
|
783
|
+
"over",
|
|
784
|
+
"pack",
|
|
785
|
+
"page",
|
|
786
|
+
"pain",
|
|
787
|
+
"pair",
|
|
788
|
+
"palm",
|
|
789
|
+
"park",
|
|
790
|
+
"part",
|
|
791
|
+
"pass",
|
|
792
|
+
"past",
|
|
793
|
+
"path",
|
|
794
|
+
"peak",
|
|
795
|
+
"peel",
|
|
796
|
+
"peer",
|
|
797
|
+
"pelt",
|
|
798
|
+
"pest",
|
|
799
|
+
"pine",
|
|
800
|
+
"pink",
|
|
801
|
+
"pipe",
|
|
802
|
+
"plan",
|
|
803
|
+
"play",
|
|
804
|
+
"plot",
|
|
805
|
+
"plow",
|
|
806
|
+
"plug",
|
|
807
|
+
"plus",
|
|
808
|
+
"poem",
|
|
809
|
+
"poet",
|
|
810
|
+
"pole",
|
|
811
|
+
"poll",
|
|
812
|
+
"pond",
|
|
813
|
+
"pool",
|
|
814
|
+
"poor",
|
|
815
|
+
"port",
|
|
816
|
+
"pose",
|
|
817
|
+
"post",
|
|
818
|
+
"pour",
|
|
819
|
+
"prey",
|
|
820
|
+
"prop",
|
|
821
|
+
"pull",
|
|
822
|
+
"pump",
|
|
823
|
+
"pure",
|
|
824
|
+
"push",
|
|
825
|
+
"race",
|
|
826
|
+
"rack",
|
|
827
|
+
"rain",
|
|
828
|
+
"ramp",
|
|
829
|
+
"rang",
|
|
830
|
+
"rank",
|
|
831
|
+
"rate",
|
|
832
|
+
"read",
|
|
833
|
+
"real",
|
|
834
|
+
"reap",
|
|
835
|
+
"reed",
|
|
836
|
+
"reef",
|
|
837
|
+
"reel",
|
|
838
|
+
"rely",
|
|
839
|
+
"rest",
|
|
840
|
+
"rice",
|
|
841
|
+
"rich",
|
|
842
|
+
"ride",
|
|
843
|
+
"rift",
|
|
844
|
+
"ring",
|
|
845
|
+
"rise",
|
|
846
|
+
"risk",
|
|
847
|
+
"road",
|
|
848
|
+
"roam",
|
|
849
|
+
"roar",
|
|
850
|
+
"role",
|
|
851
|
+
"roll",
|
|
852
|
+
"roof",
|
|
853
|
+
"room",
|
|
854
|
+
"root",
|
|
855
|
+
"rope",
|
|
856
|
+
"rose",
|
|
857
|
+
"rove",
|
|
858
|
+
"ruin",
|
|
859
|
+
"rule",
|
|
860
|
+
"rush",
|
|
861
|
+
"safe",
|
|
862
|
+
"sage",
|
|
863
|
+
"sail",
|
|
864
|
+
"sake",
|
|
865
|
+
"salt",
|
|
866
|
+
"same",
|
|
867
|
+
"sand",
|
|
868
|
+
"sane",
|
|
869
|
+
"sang",
|
|
870
|
+
"sank",
|
|
871
|
+
"save",
|
|
872
|
+
"seal",
|
|
873
|
+
"seam",
|
|
874
|
+
"seat",
|
|
875
|
+
"seed",
|
|
876
|
+
"seek",
|
|
877
|
+
"seem",
|
|
878
|
+
"seep",
|
|
879
|
+
"self",
|
|
880
|
+
"sell",
|
|
881
|
+
"send",
|
|
882
|
+
"sent",
|
|
883
|
+
"shed",
|
|
884
|
+
"shin",
|
|
885
|
+
"ship",
|
|
886
|
+
"shop",
|
|
887
|
+
"shot",
|
|
888
|
+
"show",
|
|
889
|
+
"shut",
|
|
890
|
+
"sick",
|
|
891
|
+
"side",
|
|
892
|
+
"sift",
|
|
893
|
+
"sign",
|
|
894
|
+
"silk",
|
|
895
|
+
"sill",
|
|
896
|
+
"sing",
|
|
897
|
+
"sink",
|
|
898
|
+
"site",
|
|
899
|
+
"size",
|
|
900
|
+
"skin",
|
|
901
|
+
"slab",
|
|
902
|
+
"slam",
|
|
903
|
+
"slap",
|
|
904
|
+
"slid",
|
|
905
|
+
"slim",
|
|
906
|
+
"slip",
|
|
907
|
+
"slot",
|
|
908
|
+
"slow",
|
|
909
|
+
"slug",
|
|
910
|
+
"snap",
|
|
911
|
+
"snow",
|
|
912
|
+
"soak",
|
|
913
|
+
"soar",
|
|
914
|
+
"sock",
|
|
915
|
+
"soft",
|
|
916
|
+
"soil",
|
|
917
|
+
"sole",
|
|
918
|
+
"some",
|
|
919
|
+
"song",
|
|
920
|
+
"soot",
|
|
921
|
+
"sort",
|
|
922
|
+
"soul",
|
|
923
|
+
"soup",
|
|
924
|
+
"sour",
|
|
925
|
+
"span",
|
|
926
|
+
"spar",
|
|
927
|
+
"spin",
|
|
928
|
+
"spot",
|
|
929
|
+
"spur",
|
|
930
|
+
"star",
|
|
931
|
+
"stay",
|
|
932
|
+
"stem",
|
|
933
|
+
"step",
|
|
934
|
+
"stir",
|
|
935
|
+
"stop",
|
|
936
|
+
"stub",
|
|
937
|
+
"such",
|
|
938
|
+
"suit",
|
|
939
|
+
"sung",
|
|
940
|
+
"sunk",
|
|
941
|
+
"sure",
|
|
942
|
+
"surf",
|
|
943
|
+
"swap",
|
|
944
|
+
"swim",
|
|
945
|
+
"tack",
|
|
946
|
+
"tale",
|
|
947
|
+
"tall",
|
|
948
|
+
"tame",
|
|
949
|
+
"tank",
|
|
950
|
+
"tape",
|
|
951
|
+
"task",
|
|
952
|
+
"team",
|
|
953
|
+
"tear",
|
|
954
|
+
"tell",
|
|
955
|
+
"tend",
|
|
956
|
+
"tent",
|
|
957
|
+
"term",
|
|
958
|
+
"test",
|
|
959
|
+
"text",
|
|
960
|
+
"than",
|
|
961
|
+
"that",
|
|
962
|
+
"them",
|
|
963
|
+
"then",
|
|
964
|
+
"they",
|
|
965
|
+
"thin",
|
|
966
|
+
"this",
|
|
967
|
+
"tide",
|
|
968
|
+
"tilt",
|
|
969
|
+
"time",
|
|
970
|
+
"tiny",
|
|
971
|
+
"tire",
|
|
972
|
+
"toad",
|
|
973
|
+
"toll",
|
|
974
|
+
"tone",
|
|
975
|
+
"took",
|
|
976
|
+
"tool",
|
|
977
|
+
"tops",
|
|
978
|
+
"tore",
|
|
979
|
+
"torn",
|
|
980
|
+
"toss",
|
|
981
|
+
"tour",
|
|
982
|
+
"town",
|
|
983
|
+
"tray",
|
|
984
|
+
"trim",
|
|
985
|
+
"trio",
|
|
986
|
+
"trip",
|
|
987
|
+
"true",
|
|
988
|
+
"tube",
|
|
989
|
+
"tuck",
|
|
990
|
+
"tuft",
|
|
991
|
+
"tune",
|
|
992
|
+
"turf",
|
|
993
|
+
"turn",
|
|
994
|
+
"tusk",
|
|
995
|
+
"twin",
|
|
996
|
+
"type",
|
|
997
|
+
"unit",
|
|
998
|
+
"upon",
|
|
999
|
+
"used",
|
|
1000
|
+
"user",
|
|
1001
|
+
"vast",
|
|
1002
|
+
"veil",
|
|
1003
|
+
"very",
|
|
1004
|
+
"vest",
|
|
1005
|
+
"view",
|
|
1006
|
+
"vine",
|
|
1007
|
+
"void",
|
|
1008
|
+
"vote",
|
|
1009
|
+
"wade",
|
|
1010
|
+
"wage",
|
|
1011
|
+
"wake",
|
|
1012
|
+
"walk",
|
|
1013
|
+
"wall",
|
|
1014
|
+
"want",
|
|
1015
|
+
"ward",
|
|
1016
|
+
"warm",
|
|
1017
|
+
"warp",
|
|
1018
|
+
"wary",
|
|
1019
|
+
"wash",
|
|
1020
|
+
"wave",
|
|
1021
|
+
"weak",
|
|
1022
|
+
"weld",
|
|
1023
|
+
"well",
|
|
1024
|
+
"went",
|
|
1025
|
+
"west",
|
|
1026
|
+
"what",
|
|
1027
|
+
"when",
|
|
1028
|
+
"whip",
|
|
1029
|
+
"whom",
|
|
1030
|
+
"wide",
|
|
1031
|
+
"wife",
|
|
1032
|
+
"wild",
|
|
1033
|
+
"will",
|
|
1034
|
+
"wilt",
|
|
1035
|
+
"wind",
|
|
1036
|
+
"wing",
|
|
1037
|
+
"wink",
|
|
1038
|
+
"wire",
|
|
1039
|
+
"wise",
|
|
1040
|
+
"wish",
|
|
1041
|
+
"with",
|
|
1042
|
+
"wolf",
|
|
1043
|
+
"wood",
|
|
1044
|
+
"wore",
|
|
1045
|
+
"worm",
|
|
1046
|
+
"wove",
|
|
1047
|
+
"wrap",
|
|
1048
|
+
"wren",
|
|
1049
|
+
"yard",
|
|
1050
|
+
"yarn",
|
|
1051
|
+
"yell",
|
|
1052
|
+
"your",
|
|
1053
|
+
"zinc",
|
|
1054
|
+
"zone",
|
|
1055
|
+
"zoom"
|
|
1056
|
+
];
|
|
1057
|
+
var WORD_COUNT = WORD_LIST.length;
|
|
1058
|
+
function getRandomWordIndex(listLength) {
|
|
1059
|
+
if (listLength <= 256) {
|
|
1060
|
+
const maxValid = 256 - 256 % listLength;
|
|
1061
|
+
const buf = new Uint8Array(1);
|
|
1062
|
+
let b;
|
|
1063
|
+
do {
|
|
1064
|
+
crypto.getRandomValues(buf);
|
|
1065
|
+
b = buf[0];
|
|
1066
|
+
} while (b >= maxValid);
|
|
1067
|
+
return b % listLength;
|
|
1068
|
+
} else {
|
|
1069
|
+
const RANGE40 = 1099511627776;
|
|
1070
|
+
const maxValid = Math.floor(RANGE40 / listLength) * listLength;
|
|
1071
|
+
const buf = new Uint8Array(5);
|
|
1072
|
+
let n;
|
|
1073
|
+
do {
|
|
1074
|
+
crypto.getRandomValues(buf);
|
|
1075
|
+
n = buf[0] * 4294967296 + ((buf[1] << 24 | buf[2] << 16 | buf[3] << 8 | buf[4]) >>> 0);
|
|
1076
|
+
} while (n >= maxValid);
|
|
1077
|
+
return n % listLength;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function generatePassphrase(options2 = {}) {
|
|
1081
|
+
const {
|
|
1082
|
+
words = 4,
|
|
1083
|
+
separator = "-",
|
|
1084
|
+
capitalize = false,
|
|
1085
|
+
appendDigit = false,
|
|
1086
|
+
customWords,
|
|
1087
|
+
prefix
|
|
1088
|
+
} = options2;
|
|
1089
|
+
if (words < 2) throw new Error("words must be at least 2.");
|
|
1090
|
+
const wordList = customWords ?? WORD_LIST;
|
|
1091
|
+
if (wordList.length < 2) throw new Error("Word list must contain at least 2 words.");
|
|
1092
|
+
const selected = [];
|
|
1093
|
+
for (let i = 0; i < words; i++) {
|
|
1094
|
+
const idx = getRandomWordIndex(wordList.length);
|
|
1095
|
+
let word = wordList[idx];
|
|
1096
|
+
if (capitalize) word = word.charAt(0).toUpperCase() + word.slice(1);
|
|
1097
|
+
selected.push(word);
|
|
1098
|
+
}
|
|
1099
|
+
let passphrase = selected.join(separator);
|
|
1100
|
+
if (appendDigit) {
|
|
1101
|
+
const digit = getRandomChar("0123456789");
|
|
1102
|
+
passphrase += digit;
|
|
1103
|
+
}
|
|
1104
|
+
if (prefix) passphrase = prefix + passphrase;
|
|
1105
|
+
const entropyBits = Math.log2(Math.pow(wordList.length, words)) + (appendDigit ? Math.log2(10) : 0);
|
|
1106
|
+
return {
|
|
1107
|
+
passphrase,
|
|
1108
|
+
wordCount: words,
|
|
1109
|
+
entropyBits: Math.round(entropyBits * 10) / 10
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// src/entropy.ts
|
|
1114
|
+
function estimateEntropy(options2 = {}) {
|
|
1115
|
+
const {
|
|
1116
|
+
length = 16,
|
|
1117
|
+
prefix = "",
|
|
1118
|
+
suffix = "",
|
|
1119
|
+
pattern,
|
|
1120
|
+
count: _count,
|
|
1121
|
+
startWith: _startWith,
|
|
1122
|
+
...charsetOptions
|
|
1123
|
+
} = options2;
|
|
1124
|
+
if (pattern) {
|
|
1125
|
+
return estimatePatternEntropy(pattern, charsetOptions);
|
|
1126
|
+
}
|
|
1127
|
+
const charset = buildCharset(charsetOptions);
|
|
1128
|
+
const charsetSize = charset.length;
|
|
1129
|
+
const effectiveLength = Math.max(0, length - prefix.length - suffix.length);
|
|
1130
|
+
if (effectiveLength === 0) {
|
|
1131
|
+
return {
|
|
1132
|
+
bits: 0,
|
|
1133
|
+
strength: "very-weak",
|
|
1134
|
+
charsetSize,
|
|
1135
|
+
effectiveLength: 0,
|
|
1136
|
+
combinations: "1"
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
const bitsPerChar = Math.log2(charsetSize);
|
|
1140
|
+
const bits = bitsPerChar * effectiveLength;
|
|
1141
|
+
return {
|
|
1142
|
+
bits: Math.round(bits * 100) / 100,
|
|
1143
|
+
strength: strengthLabel(bits),
|
|
1144
|
+
charsetSize,
|
|
1145
|
+
effectiveLength,
|
|
1146
|
+
combinations: approximateCombinations(charsetSize, effectiveLength)
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function estimatePatternEntropy(pattern, charsetOptions) {
|
|
1150
|
+
const fullCharset = buildCharset(charsetOptions);
|
|
1151
|
+
const alphaChars = [...fullCharset].filter((c) => /[a-zA-Z]/.test(c)).join("");
|
|
1152
|
+
const upperChars = [...fullCharset].filter((c) => /[A-Z]/.test(c)).join("");
|
|
1153
|
+
const lowerChars = [...fullCharset].filter((c) => /[a-z]/.test(c)).join("");
|
|
1154
|
+
const digitChars = "0123456789";
|
|
1155
|
+
const alphanumChars = [...fullCharset].filter((c) => /[a-zA-Z0-9]/.test(c)).join("");
|
|
1156
|
+
let totalBits = 0;
|
|
1157
|
+
let effectiveLength = 0;
|
|
1158
|
+
let i = 0;
|
|
1159
|
+
while (i < pattern.length) {
|
|
1160
|
+
const ch = pattern[i];
|
|
1161
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
1162
|
+
i += 2;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
let placeholderCharsetSize = 0;
|
|
1166
|
+
switch (ch) {
|
|
1167
|
+
case "#":
|
|
1168
|
+
placeholderCharsetSize = digitChars.length;
|
|
1169
|
+
effectiveLength++;
|
|
1170
|
+
break;
|
|
1171
|
+
case "A":
|
|
1172
|
+
placeholderCharsetSize = Math.max(upperChars.length, 1);
|
|
1173
|
+
effectiveLength++;
|
|
1174
|
+
break;
|
|
1175
|
+
case "a":
|
|
1176
|
+
placeholderCharsetSize = Math.max(lowerChars.length, 1);
|
|
1177
|
+
effectiveLength++;
|
|
1178
|
+
break;
|
|
1179
|
+
case "*":
|
|
1180
|
+
placeholderCharsetSize = fullCharset.length;
|
|
1181
|
+
effectiveLength++;
|
|
1182
|
+
break;
|
|
1183
|
+
case "?":
|
|
1184
|
+
placeholderCharsetSize = Math.max(alphanumChars.length, 1);
|
|
1185
|
+
effectiveLength++;
|
|
1186
|
+
break;
|
|
1187
|
+
default:
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
if (placeholderCharsetSize > 0) {
|
|
1191
|
+
totalBits += Math.log2(placeholderCharsetSize);
|
|
1192
|
+
}
|
|
1193
|
+
i++;
|
|
1194
|
+
}
|
|
1195
|
+
const _ = alphaChars;
|
|
1196
|
+
return {
|
|
1197
|
+
bits: Math.round(totalBits * 100) / 100,
|
|
1198
|
+
strength: strengthLabel(totalBits),
|
|
1199
|
+
charsetSize: fullCharset.length,
|
|
1200
|
+
effectiveLength,
|
|
1201
|
+
combinations: approximateCombinations(fullCharset.length, effectiveLength)
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function strengthLabel(bits) {
|
|
1205
|
+
if (bits < 28) return "very-weak";
|
|
1206
|
+
if (bits < 50) return "weak";
|
|
1207
|
+
if (bits < 72) return "fair";
|
|
1208
|
+
if (bits < 100) return "strong";
|
|
1209
|
+
return "very-strong";
|
|
1210
|
+
}
|
|
1211
|
+
function approximateCombinations(charsetSize, length) {
|
|
1212
|
+
const log10 = length * Math.log10(charsetSize);
|
|
1213
|
+
if (log10 > 15) {
|
|
1214
|
+
const exp = Math.floor(log10);
|
|
1215
|
+
const mantissa = Math.pow(10, log10 - exp);
|
|
1216
|
+
return `${mantissa.toFixed(2)}e+${exp}`;
|
|
1217
|
+
}
|
|
1218
|
+
return Math.round(Math.pow(charsetSize, length)).toString();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/presets.ts
|
|
1222
|
+
var PRESETS = {
|
|
1223
|
+
/**
|
|
1224
|
+
* API token / session key.
|
|
1225
|
+
* 32-char alphanumeric (no symbols), readable=false for max entropy.
|
|
1226
|
+
* ~190 bits of entropy.
|
|
1227
|
+
*/
|
|
1228
|
+
TOKEN: {
|
|
1229
|
+
length: 32,
|
|
1230
|
+
numeric: true,
|
|
1231
|
+
lowercase: true,
|
|
1232
|
+
uppercase: true,
|
|
1233
|
+
symbols: false
|
|
1234
|
+
},
|
|
1235
|
+
/**
|
|
1236
|
+
* Strong password.
|
|
1237
|
+
* 20 chars, all character types including symbols.
|
|
1238
|
+
* ~131 bits of entropy.
|
|
1239
|
+
*/
|
|
1240
|
+
PASSWORD: {
|
|
1241
|
+
length: 20,
|
|
1242
|
+
numeric: true,
|
|
1243
|
+
lowercase: true,
|
|
1244
|
+
uppercase: true,
|
|
1245
|
+
symbols: true
|
|
1246
|
+
},
|
|
1247
|
+
/**
|
|
1248
|
+
* Human-readable token — no ambiguous characters (0/O/l/I/1).
|
|
1249
|
+
* Safe to read aloud or transcribe from a screen.
|
|
1250
|
+
* 16 chars, ~93 bits of entropy.
|
|
1251
|
+
*/
|
|
1252
|
+
READABLE: {
|
|
1253
|
+
length: 16,
|
|
1254
|
+
numeric: true,
|
|
1255
|
+
lowercase: true,
|
|
1256
|
+
uppercase: true,
|
|
1257
|
+
symbols: false,
|
|
1258
|
+
readable: true
|
|
1259
|
+
},
|
|
1260
|
+
/**
|
|
1261
|
+
* URL-safe slug identifier.
|
|
1262
|
+
* Lowercase letters and digits only, starts with a letter.
|
|
1263
|
+
* 12 chars, ~62 bits of entropy.
|
|
1264
|
+
*/
|
|
1265
|
+
SLUG: {
|
|
1266
|
+
length: 12,
|
|
1267
|
+
numeric: true,
|
|
1268
|
+
lowercase: true,
|
|
1269
|
+
uppercase: false,
|
|
1270
|
+
symbols: false,
|
|
1271
|
+
startWith: "alphabet"
|
|
1272
|
+
},
|
|
1273
|
+
/**
|
|
1274
|
+
* Lowercase hex string.
|
|
1275
|
+
* Compatible with UUID hex components, color codes, hash representations.
|
|
1276
|
+
* 32 chars (128-bit equivalent), ~128 bits of entropy.
|
|
1277
|
+
*/
|
|
1278
|
+
HEX: {
|
|
1279
|
+
length: 32,
|
|
1280
|
+
charset: "0123456789abcdef"
|
|
1281
|
+
},
|
|
1282
|
+
/**
|
|
1283
|
+
* Numeric PIN code. 6 digits, starts with a non-zero digit.
|
|
1284
|
+
* ~17 bits of entropy — suitable for short-lived OTP codes.
|
|
1285
|
+
*/
|
|
1286
|
+
PIN: {
|
|
1287
|
+
length: 6,
|
|
1288
|
+
numeric: true,
|
|
1289
|
+
lowercase: false,
|
|
1290
|
+
uppercase: false,
|
|
1291
|
+
symbols: false,
|
|
1292
|
+
exclude: "0",
|
|
1293
|
+
startWith: "numeric"
|
|
1294
|
+
},
|
|
1295
|
+
/**
|
|
1296
|
+
* UUID-like string (not RFC 4122 compliant, but same visual format).
|
|
1297
|
+
* Uses `pattern` to produce 8-4-4-4-12 hex grouping.
|
|
1298
|
+
* ~122 bits of entropy (same as UUIDv4).
|
|
1299
|
+
*/
|
|
1300
|
+
UUID_LIKE: {
|
|
1301
|
+
pattern: "\\*\\*\\*\\*\\*\\*\\*\\*-\\*\\*\\*\\*-\\*\\*\\*\\*-\\*\\*\\*\\*-\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*",
|
|
1302
|
+
charset: "0123456789abcdef"
|
|
1303
|
+
},
|
|
1304
|
+
/**
|
|
1305
|
+
* Short alphanumeric ID — suitable for database record IDs.
|
|
1306
|
+
* 8 chars, uppercase alphanumeric, starts with a letter.
|
|
1307
|
+
* ~41 bits — good for IDs when combined with a namespace/prefix.
|
|
1308
|
+
*/
|
|
1309
|
+
SHORT_ID: {
|
|
1310
|
+
length: 8,
|
|
1311
|
+
numeric: true,
|
|
1312
|
+
lowercase: false,
|
|
1313
|
+
uppercase: true,
|
|
1314
|
+
symbols: false,
|
|
1315
|
+
startWith: "alphabet"
|
|
1316
|
+
},
|
|
1317
|
+
/**
|
|
1318
|
+
* Invitation / redemption code.
|
|
1319
|
+
* 16 chars, readable (no ambiguous chars), pattern grouped for readability.
|
|
1320
|
+
* @example 'KXPZ-9MR2-LQ4Y-8WVN'
|
|
1321
|
+
*/
|
|
1322
|
+
INVITE_CODE: {
|
|
1323
|
+
pattern: "AAAA-AAAA-AAAA-AAAA",
|
|
1324
|
+
readable: true
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
// src/index.ts
|
|
1329
|
+
function generateRandomString(options2 = {}) {
|
|
1330
|
+
const { count: count2 = 1, ...rest } = options2;
|
|
1331
|
+
if (count2 === 1) return generateOne(rest);
|
|
1332
|
+
return generateBatch(rest, count2);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// bin/strio.ts
|
|
1336
|
+
var args = process.argv.slice(2);
|
|
1337
|
+
function getFlag(name) {
|
|
1338
|
+
const i = args.indexOf(`--${name}`);
|
|
1339
|
+
return i !== -1 ? args[i + 1] : void 0;
|
|
1340
|
+
}
|
|
1341
|
+
function hasFlag(name) {
|
|
1342
|
+
return args.includes(`--${name}`);
|
|
1343
|
+
}
|
|
1344
|
+
if (hasFlag("help") || hasFlag("h")) {
|
|
1345
|
+
console.log(`
|
|
1346
|
+
@ppabari/strio \u2014 cryptographically secure string generation
|
|
1347
|
+
|
|
1348
|
+
USAGE
|
|
1349
|
+
npx @ppabari/strio [options]
|
|
1350
|
+
|
|
1351
|
+
GENERATION OPTIONS
|
|
1352
|
+
--length <n> String length (default: 16)
|
|
1353
|
+
--count <n> Generate N strings (one per line)
|
|
1354
|
+
--preset <name> Use a named preset (see PRESETS below)
|
|
1355
|
+
--pattern <str> Pattern mode: # digit, A uppercase, a lowercase, * any, ? alnum
|
|
1356
|
+
--charset <name|str> Named alias or raw charset string
|
|
1357
|
+
--no-numeric Exclude digits
|
|
1358
|
+
--no-lowercase Exclude lowercase letters
|
|
1359
|
+
--no-uppercase Exclude uppercase letters
|
|
1360
|
+
--symbols Include symbols
|
|
1361
|
+
--readable Exclude ambiguous chars (0 O l I 1)
|
|
1362
|
+
--exclude <chars> Exclude specific characters
|
|
1363
|
+
--prefix <str> Prepend a fixed string
|
|
1364
|
+
--suffix <str> Append a fixed string
|
|
1365
|
+
--start-with <type> First char: alphabet | numeric | any
|
|
1366
|
+
|
|
1367
|
+
SPECIAL MODES
|
|
1368
|
+
--passphrase Generate a passphrase instead
|
|
1369
|
+
--words <n> Number of words (default: 4)
|
|
1370
|
+
--separator <str> Word separator (default: -)
|
|
1371
|
+
--capitalize Capitalize each word
|
|
1372
|
+
--append-digit Append a random digit
|
|
1373
|
+
|
|
1374
|
+
--id Generate a short ID with checksum
|
|
1375
|
+
--prefix <str> ID prefix (e.g. usr \u2192 usr_...)
|
|
1376
|
+
--id-length <n> Random portion length (default: 12)
|
|
1377
|
+
|
|
1378
|
+
--expiring Generate a self-expiring token
|
|
1379
|
+
--ttl <seconds> Token TTL in seconds (default: 900)
|
|
1380
|
+
--payload <n> Payload length (default: 24)
|
|
1381
|
+
|
|
1382
|
+
--entropy Show entropy estimate only (no string generated)
|
|
1383
|
+
|
|
1384
|
+
PRESETS
|
|
1385
|
+
TOKEN 32-char alphanumeric API token
|
|
1386
|
+
PASSWORD 20-char with symbols
|
|
1387
|
+
READABLE 16-char, no ambiguous chars
|
|
1388
|
+
SLUG 12-char lowercase, starts with letter
|
|
1389
|
+
HEX 32-char hex
|
|
1390
|
+
PIN 6-digit PIN
|
|
1391
|
+
SHORT_ID 8-char uppercase, starts with letter
|
|
1392
|
+
INVITE_CODE AAAA-AAAA-AAAA-AAAA
|
|
1393
|
+
|
|
1394
|
+
CHARSET ALIASES
|
|
1395
|
+
base16 base32 base36 base58 base62 base64url hex alphanumeric crockford32
|
|
1396
|
+
|
|
1397
|
+
EXAMPLES
|
|
1398
|
+
npx @ppabari/strio --length 32
|
|
1399
|
+
npx @ppabari/strio --preset TOKEN --count 5
|
|
1400
|
+
npx @ppabari/strio --pattern "####-AAAA-####"
|
|
1401
|
+
npx @ppabari/strio --charset base58 --length 22
|
|
1402
|
+
npx @ppabari/strio --passphrase --words 6 --capitalize
|
|
1403
|
+
npx @ppabari/strio --id --prefix usr
|
|
1404
|
+
npx @ppabari/strio --expiring --ttl 300
|
|
1405
|
+
npx @ppabari/strio --entropy --preset PASSWORD
|
|
1406
|
+
`);
|
|
1407
|
+
process.exit(0);
|
|
1408
|
+
}
|
|
1409
|
+
if (hasFlag("passphrase")) {
|
|
1410
|
+
const words = parseInt(getFlag("words") ?? "4", 10);
|
|
1411
|
+
const separator = getFlag("separator") ?? "-";
|
|
1412
|
+
const capitalize = hasFlag("capitalize");
|
|
1413
|
+
const appendDigit = hasFlag("append-digit");
|
|
1414
|
+
const count2 = parseInt(getFlag("count") ?? "1", 10);
|
|
1415
|
+
for (let i = 0; i < count2; i++) {
|
|
1416
|
+
const { passphrase, entropyBits } = generatePassphrase({ words, separator, capitalize, appendDigit });
|
|
1417
|
+
if (hasFlag("verbose")) {
|
|
1418
|
+
console.log(`${passphrase} [${entropyBits} bits]`);
|
|
1419
|
+
} else {
|
|
1420
|
+
console.log(passphrase);
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
process.exit(0);
|
|
1424
|
+
}
|
|
1425
|
+
if (hasFlag("id")) {
|
|
1426
|
+
const prefix = getFlag("prefix");
|
|
1427
|
+
const randomLength = parseInt(getFlag("id-length") ?? "12", 10);
|
|
1428
|
+
const charset = getFlag("charset") ?? "base58";
|
|
1429
|
+
const count2 = parseInt(getFlag("count") ?? "1", 10);
|
|
1430
|
+
for (let i = 0; i < count2; i++) {
|
|
1431
|
+
console.log(generateId({ prefix, randomLength, charset }));
|
|
1432
|
+
}
|
|
1433
|
+
process.exit(0);
|
|
1434
|
+
}
|
|
1435
|
+
if (hasFlag("expiring")) {
|
|
1436
|
+
const ttlSeconds = parseInt(getFlag("ttl") ?? "900", 10);
|
|
1437
|
+
const payloadLength = parseInt(getFlag("payload") ?? "24", 10);
|
|
1438
|
+
const count2 = parseInt(getFlag("count") ?? "1", 10);
|
|
1439
|
+
for (let i = 0; i < count2; i++) {
|
|
1440
|
+
const { token, expiresAt } = generateExpiringToken({ ttlSeconds, payloadLength });
|
|
1441
|
+
if (hasFlag("verbose")) {
|
|
1442
|
+
console.log(`${token} [expires: ${expiresAt.toISOString()}]`);
|
|
1443
|
+
} else {
|
|
1444
|
+
console.log(token);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
process.exit(0);
|
|
1448
|
+
}
|
|
1449
|
+
var presetName = getFlag("preset");
|
|
1450
|
+
var preset = presetName ? PRESETS[presetName] : void 0;
|
|
1451
|
+
if (presetName && !preset) {
|
|
1452
|
+
console.error(`Unknown preset: ${presetName}. Available: ${Object.keys(PRESETS).join(", ")}`);
|
|
1453
|
+
process.exit(1);
|
|
1454
|
+
}
|
|
1455
|
+
var charsetArg = getFlag("charset");
|
|
1456
|
+
var resolvedCharset = charsetArg ? CHARSET_ALIASES[charsetArg] ?? charsetArg : void 0;
|
|
1457
|
+
var options = {
|
|
1458
|
+
...preset ?? {},
|
|
1459
|
+
...getFlag("length") ? { length: parseInt(getFlag("length"), 10) } : {},
|
|
1460
|
+
...hasFlag("no-numeric") ? { numeric: false } : {},
|
|
1461
|
+
...hasFlag("no-lowercase") ? { lowercase: false } : {},
|
|
1462
|
+
...hasFlag("no-uppercase") ? { uppercase: false } : {},
|
|
1463
|
+
...hasFlag("symbols") ? { symbols: true } : {},
|
|
1464
|
+
...hasFlag("readable") ? { readable: true } : {},
|
|
1465
|
+
...getFlag("exclude") ? { exclude: getFlag("exclude") } : {},
|
|
1466
|
+
...getFlag("prefix") ? { prefix: getFlag("prefix") } : {},
|
|
1467
|
+
...getFlag("suffix") ? { suffix: getFlag("suffix") } : {},
|
|
1468
|
+
...getFlag("start-with") ? { startWith: getFlag("start-with") } : {},
|
|
1469
|
+
...getFlag("pattern") ? { pattern: getFlag("pattern") } : {},
|
|
1470
|
+
...resolvedCharset ? { charset: resolvedCharset } : {}
|
|
1471
|
+
};
|
|
1472
|
+
if (hasFlag("entropy")) {
|
|
1473
|
+
const e = estimateEntropy(options);
|
|
1474
|
+
console.log(`Charset size : ${e.charsetSize} characters`);
|
|
1475
|
+
console.log(`Length : ${e.effectiveLength} chars (random portion)`);
|
|
1476
|
+
console.log(`Entropy : ${e.bits} bits`);
|
|
1477
|
+
console.log(`Strength : ${e.strength}`);
|
|
1478
|
+
console.log(`Combinations : ${e.combinations}`);
|
|
1479
|
+
process.exit(0);
|
|
1480
|
+
}
|
|
1481
|
+
var count = parseInt(getFlag("count") ?? "1", 10);
|
|
1482
|
+
try {
|
|
1483
|
+
if (count === 1) {
|
|
1484
|
+
console.log(generateRandomString(options));
|
|
1485
|
+
} else {
|
|
1486
|
+
const results = generateRandomString({ ...options, count });
|
|
1487
|
+
results.forEach((s) => console.log(s));
|
|
1488
|
+
}
|
|
1489
|
+
} catch (err) {
|
|
1490
|
+
console.error(`Error: ${err.message}`);
|
|
1491
|
+
process.exit(1);
|
|
1492
|
+
}
|