@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/dist/index.cjs ADDED
@@ -0,0 +1,1913 @@
1
+ 'use strict';
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, count) {
19
+ const len = charset.length;
20
+ const maxValid = 256 - 256 % len;
21
+ const result = [];
22
+ const bufSize = Math.ceil(count * 1.4) + 32;
23
+ const buf = new Uint8Array(bufSize);
24
+ while (result.length < count) {
25
+ crypto.getRandomValues(buf);
26
+ for (let i = 0; i < buf.length && result.length < count; 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
+ // Ambiguous characters that are easy to confuse when reading/transcribing
161
+ ambiguous: "0Ol1I"
162
+ };
163
+ function dedup(str) {
164
+ return [...new Set(str)].join("");
165
+ }
166
+ function buildCharset(options) {
167
+ let pool;
168
+ if (options.charset !== void 0) {
169
+ pool = dedup(resolveCharsetAlias(options.charset));
170
+ if (pool.length < 2) {
171
+ throw new Error(
172
+ "Custom charset must contain at least 2 unique characters."
173
+ );
174
+ }
175
+ return pool;
176
+ }
177
+ const {
178
+ numeric = true,
179
+ lowercase = true,
180
+ uppercase = true,
181
+ symbols = false
182
+ } = options;
183
+ pool = "";
184
+ if (numeric) pool += CHARSETS.numeric;
185
+ if (lowercase) pool += CHARSETS.lowercase;
186
+ if (uppercase) pool += CHARSETS.uppercase;
187
+ if (symbols) pool += CHARSETS.symbols;
188
+ if (!pool) {
189
+ throw new Error(
190
+ "At least one character type must be enabled (numeric, lowercase, uppercase, or symbols)."
191
+ );
192
+ }
193
+ if (options.readable) {
194
+ for (const ch of CHARSETS.ambiguous) {
195
+ pool = pool.split(ch).join("");
196
+ }
197
+ }
198
+ if (options.exclude) {
199
+ const excludeChars = typeof options.exclude === "string" ? options.exclude : options.exclude.join("");
200
+ for (const ch of excludeChars) {
201
+ pool = pool.split(ch).join("");
202
+ }
203
+ }
204
+ pool = dedup(pool);
205
+ if (pool.length < 2) {
206
+ throw new Error(
207
+ "After applying exclusions, the character pool has fewer than 2 unique characters. Loosen your exclude/readable settings or enable more character types."
208
+ );
209
+ }
210
+ return pool;
211
+ }
212
+ function alphabetSubset(fullPool) {
213
+ return [...fullPool].filter((ch) => /[a-zA-Z]/.test(ch)).join("");
214
+ }
215
+ function numericSubset(fullPool) {
216
+ return [...fullPool].filter((ch) => /[0-9]/.test(ch)).join("");
217
+ }
218
+
219
+ // src/generator.ts
220
+ function expandPattern(pattern, fullCharset) {
221
+ const upper = [...fullCharset].filter((c) => /[A-Z]/.test(c)).join("") || CHARSETS.uppercase;
222
+ const lower = [...fullCharset].filter((c) => /[a-z]/.test(c)).join("") || CHARSETS.lowercase;
223
+ const alphanum = [...fullCharset].filter((c) => /[a-zA-Z0-9]/.test(c)).join("") || CHARSETS.alphanumeric;
224
+ const result = [];
225
+ let i = 0;
226
+ while (i < pattern.length) {
227
+ const ch = pattern[i];
228
+ if (ch === "\\" && i + 1 < pattern.length) {
229
+ result.push(pattern[i + 1]);
230
+ i += 2;
231
+ continue;
232
+ }
233
+ switch (ch) {
234
+ case "#":
235
+ result.push(getRandomChar(CHARSETS.numeric));
236
+ break;
237
+ case "A":
238
+ result.push(getRandomChar(upper));
239
+ break;
240
+ case "a":
241
+ result.push(getRandomChar(lower));
242
+ break;
243
+ case "*":
244
+ result.push(getRandomChar(fullCharset));
245
+ break;
246
+ case "?":
247
+ result.push(getRandomChar(alphanum));
248
+ break;
249
+ default:
250
+ result.push(ch);
251
+ }
252
+ i++;
253
+ }
254
+ return result.join("");
255
+ }
256
+ function generateOne(options) {
257
+ const {
258
+ length = 16,
259
+ prefix = "",
260
+ suffix = "",
261
+ startWith = "any",
262
+ pattern,
263
+ seed,
264
+ ...charsetOptions
265
+ } = options;
266
+ const charset = buildCharset(charsetOptions);
267
+ if (seed !== void 0) {
268
+ if (pattern !== void 0) {
269
+ const patternLength = [...pattern].filter((_, i) => {
270
+ return true;
271
+ }).length;
272
+ const seededStr2 = generateSeeded({ seed, charset, length: patternLength });
273
+ return `${prefix}${seededStr2}${suffix}`;
274
+ }
275
+ const randomLength2 = length - prefix.length - suffix.length;
276
+ if (randomLength2 <= 0) return `${prefix}${suffix}`;
277
+ const seededStr = generateSeeded({ seed, charset, length: randomLength2 });
278
+ return `${prefix}${seededStr}${suffix}`;
279
+ }
280
+ if (pattern !== void 0) {
281
+ const expanded = expandPattern(pattern, charset);
282
+ return `${prefix}${expanded}${suffix}`;
283
+ }
284
+ const randomLength = length - prefix.length - suffix.length;
285
+ if (randomLength < 0) {
286
+ throw new Error(
287
+ `prefix ('${prefix}') + suffix ('${suffix}') is longer than the total length (${length}). Increase length or shorten the affixes.`
288
+ );
289
+ }
290
+ if (randomLength === 0) {
291
+ return `${prefix}${suffix}`;
292
+ }
293
+ let firstCharset;
294
+ switch (startWith) {
295
+ case "alphabet": {
296
+ const alpha = alphabetSubset(charset);
297
+ if (!alpha) {
298
+ throw new Error(
299
+ "startWith: 'alphabet' requires at least one of lowercase or uppercase to be enabled."
300
+ );
301
+ }
302
+ firstCharset = alpha;
303
+ break;
304
+ }
305
+ case "numeric": {
306
+ const num = numericSubset(charset);
307
+ if (!num) {
308
+ throw new Error(
309
+ "startWith: 'numeric' requires numeric to be enabled."
310
+ );
311
+ }
312
+ firstCharset = num;
313
+ break;
314
+ }
315
+ default:
316
+ firstCharset = charset;
317
+ }
318
+ const firstChar = getRandomChar(firstCharset);
319
+ if (randomLength === 1) {
320
+ return `${prefix}${firstChar}${suffix}`;
321
+ }
322
+ const rest = getRandomChars(charset, randomLength - 1).join("");
323
+ return `${prefix}${firstChar}${rest}${suffix}`;
324
+ }
325
+ function generateBatch(options, count) {
326
+ if (count <= 0) {
327
+ throw new Error("count must be a positive integer.");
328
+ }
329
+ if (!Number.isInteger(count)) {
330
+ throw new Error("count must be an integer.");
331
+ }
332
+ const results = /* @__PURE__ */ new Set();
333
+ let attempts = 0;
334
+ const maxAttempts = count * 10 + 1e3;
335
+ while (results.size < count) {
336
+ if (++attempts > maxAttempts) {
337
+ throw new Error(
338
+ `Could not generate ${count} unique strings after ${maxAttempts} attempts. The charset and length combination may not support this many unique values.`
339
+ );
340
+ }
341
+ results.add(generateOne(options));
342
+ }
343
+ return [...results];
344
+ }
345
+
346
+ // src/short-id.ts
347
+ function computeChecksum(payload, charset) {
348
+ const n = charset.length;
349
+ let sum = 0;
350
+ for (let i = 0; i < payload.length; i++) {
351
+ const idx = charset.indexOf(payload[i]);
352
+ if (idx === -1) return charset[0];
353
+ const weight = i % 2 === 0 ? 1 : 2;
354
+ sum += idx * weight;
355
+ }
356
+ return charset[sum % n];
357
+ }
358
+ function generateId(options = {}) {
359
+ const {
360
+ prefix,
361
+ separator = "_",
362
+ randomLength = 12,
363
+ charset: rawCharset = "base58",
364
+ checksum = true
365
+ } = options;
366
+ const resolvedCharset = resolveCharsetAlias(rawCharset);
367
+ const charset = buildCharset({ charset: resolvedCharset });
368
+ if (randomLength < 4) throw new Error("randomLength must be at least 4.");
369
+ const randomPart = getRandomChars(charset, randomLength).join("");
370
+ const checksumChar = checksum ? computeChecksum(randomPart, charset) : "";
371
+ const randomSection = randomPart + checksumChar;
372
+ if (prefix) {
373
+ return `${prefix}${separator}${randomSection}`;
374
+ }
375
+ return randomSection;
376
+ }
377
+ function validateId(id, options = {}) {
378
+ const {
379
+ prefix,
380
+ separator = "_",
381
+ charset: rawCharset = "base58",
382
+ checksum = true
383
+ } = options;
384
+ const errors = [];
385
+ const resolvedCharset = resolveCharsetAlias(rawCharset);
386
+ let payload = id;
387
+ if (prefix) {
388
+ const expectedStart = `${prefix}${separator}`;
389
+ if (!id.startsWith(expectedStart)) {
390
+ errors.push(`Expected prefix '${prefix}${separator}', got '${id.slice(0, expectedStart.length)}'.`);
391
+ return { valid: false, errors };
392
+ }
393
+ payload = id.slice(expectedStart.length);
394
+ }
395
+ const charsetSet = new Set(resolvedCharset);
396
+ const badChars = [...new Set(payload)].filter((c) => !charsetSet.has(c));
397
+ if (badChars.length > 0) {
398
+ errors.push(`Contains characters outside the expected charset: ${badChars.map((c) => JSON.stringify(c)).join(", ")}.`);
399
+ }
400
+ if (checksum && payload.length >= 2 && errors.length === 0) {
401
+ const randomPart = payload.slice(0, -1);
402
+ const providedChecksum = payload.slice(-1);
403
+ const expectedChecksum = computeChecksum(randomPart, resolvedCharset);
404
+ if (providedChecksum !== expectedChecksum) {
405
+ errors.push("Checksum mismatch \u2014 ID may be corrupted or mistyped.");
406
+ }
407
+ }
408
+ return { valid: errors.length === 0, errors };
409
+ }
410
+
411
+ // src/expiring-token.ts
412
+ var B62 = CHARSET_ALIASES["base62"];
413
+ var B62_LEN = B62.length;
414
+ var EXPIRY_CHARS = 8;
415
+ function encodeBase62(n, width) {
416
+ const digits = [];
417
+ let v = Math.floor(n);
418
+ for (let i = 0; i < width; i++) {
419
+ digits.unshift(B62[v % B62_LEN]);
420
+ v = Math.floor(v / B62_LEN);
421
+ }
422
+ return digits.join("");
423
+ }
424
+ function decodeBase62(s) {
425
+ let v = 0;
426
+ for (const ch of s) {
427
+ const idx = B62.indexOf(ch);
428
+ if (idx === -1) return -1;
429
+ v = v * B62_LEN + idx;
430
+ }
431
+ return v;
432
+ }
433
+ function generateExpiringToken(options = {}) {
434
+ const { ttlSeconds = 900, payloadLength = 24 } = options;
435
+ if (ttlSeconds <= 0) throw new Error("ttlSeconds must be a positive number.");
436
+ if (payloadLength < 8) throw new Error("payloadLength must be at least 8.");
437
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
438
+ const expiryEpoch = Math.floor(expiresAt.getTime() / 1e3);
439
+ const expiryPart = encodeBase62(expiryEpoch, EXPIRY_CHARS);
440
+ const payload = generateOne({
441
+ length: payloadLength,
442
+ charset: B62
443
+ });
444
+ const token = expiryPart + payload;
445
+ return {
446
+ token,
447
+ expiresAt,
448
+ length: token.length
449
+ };
450
+ }
451
+ function verifyToken(token) {
452
+ if (typeof token !== "string" || token.length < EXPIRY_CHARS + 8) {
453
+ return { valid: false, parsed: false, expired: false, expiresAt: null, secondsRemaining: null };
454
+ }
455
+ const expiryStr = token.slice(0, EXPIRY_CHARS);
456
+ const expiryEpoch = decodeBase62(expiryStr);
457
+ if (expiryEpoch === -1) {
458
+ return { valid: false, parsed: false, expired: false, expiresAt: null, secondsRemaining: null };
459
+ }
460
+ const expiresAt = new Date(expiryEpoch * 1e3);
461
+ const nowSeconds = Date.now() / 1e3;
462
+ const secondsRemaining = Math.round(expiryEpoch - nowSeconds);
463
+ const expired = secondsRemaining <= 0;
464
+ return {
465
+ valid: !expired,
466
+ parsed: true,
467
+ expired,
468
+ expiresAt,
469
+ secondsRemaining
470
+ };
471
+ }
472
+
473
+ // src/stream.ts
474
+ async function* randomStringStream(options = {}) {
475
+ generateOne(options);
476
+ while (true) {
477
+ yield generateOne(options);
478
+ }
479
+ }
480
+ async function* uniqueRandomStringStream(options = {}) {
481
+ const seen = /* @__PURE__ */ new Set();
482
+ generateOne(options);
483
+ while (true) {
484
+ const candidate = generateOne(options);
485
+ if (!seen.has(candidate)) {
486
+ seen.add(candidate);
487
+ yield candidate;
488
+ }
489
+ }
490
+ }
491
+ async function take(iterable, n) {
492
+ if (n <= 0) return [];
493
+ if (!Number.isInteger(n)) throw new Error("n must be a positive integer.");
494
+ const results = [];
495
+ for await (const value of iterable) {
496
+ results.push(value);
497
+ if (results.length >= n) break;
498
+ }
499
+ return results;
500
+ }
501
+ async function takeWhere(iterable, n, predicate) {
502
+ if (n <= 0) return [];
503
+ const results = [];
504
+ for await (const value of iterable) {
505
+ if (predicate(value)) {
506
+ results.push(value);
507
+ if (results.length >= n) break;
508
+ }
509
+ }
510
+ return results;
511
+ }
512
+
513
+ // src/passphrase.ts
514
+ var WORD_LIST = [
515
+ "able",
516
+ "acid",
517
+ "aged",
518
+ "also",
519
+ "area",
520
+ "army",
521
+ "away",
522
+ "back",
523
+ "ball",
524
+ "band",
525
+ "bank",
526
+ "base",
527
+ "bath",
528
+ "bear",
529
+ "beat",
530
+ "been",
531
+ "bell",
532
+ "best",
533
+ "bird",
534
+ "bite",
535
+ "blow",
536
+ "blue",
537
+ "boat",
538
+ "body",
539
+ "bomb",
540
+ "bond",
541
+ "bone",
542
+ "book",
543
+ "bore",
544
+ "born",
545
+ "both",
546
+ "bowl",
547
+ "burn",
548
+ "busy",
549
+ "call",
550
+ "calm",
551
+ "camp",
552
+ "card",
553
+ "care",
554
+ "cart",
555
+ "case",
556
+ "cash",
557
+ "cast",
558
+ "cave",
559
+ "cell",
560
+ "chat",
561
+ "chip",
562
+ "city",
563
+ "clam",
564
+ "clap",
565
+ "clay",
566
+ "clip",
567
+ "club",
568
+ "clue",
569
+ "coal",
570
+ "coat",
571
+ "code",
572
+ "coil",
573
+ "cold",
574
+ "come",
575
+ "cook",
576
+ "cool",
577
+ "copy",
578
+ "core",
579
+ "cork",
580
+ "corn",
581
+ "cost",
582
+ "cozy",
583
+ "crab",
584
+ "crew",
585
+ "crop",
586
+ "crow",
587
+ "curb",
588
+ "cure",
589
+ "dark",
590
+ "dart",
591
+ "data",
592
+ "date",
593
+ "dawn",
594
+ "deal",
595
+ "dear",
596
+ "deck",
597
+ "deed",
598
+ "deep",
599
+ "deft",
600
+ "dent",
601
+ "desk",
602
+ "dial",
603
+ "dice",
604
+ "diet",
605
+ "dime",
606
+ "dine",
607
+ "dirt",
608
+ "dish",
609
+ "disk",
610
+ "dock",
611
+ "dome",
612
+ "door",
613
+ "dose",
614
+ "dove",
615
+ "down",
616
+ "draw",
617
+ "drew",
618
+ "drip",
619
+ "drop",
620
+ "drum",
621
+ "dual",
622
+ "dune",
623
+ "dusk",
624
+ "dust",
625
+ "each",
626
+ "earl",
627
+ "earn",
628
+ "ease",
629
+ "east",
630
+ "edge",
631
+ "else",
632
+ "emit",
633
+ "envy",
634
+ "epic",
635
+ "even",
636
+ "exam",
637
+ "exit",
638
+ "expo",
639
+ "face",
640
+ "fact",
641
+ "fail",
642
+ "fair",
643
+ "fall",
644
+ "fame",
645
+ "farm",
646
+ "fast",
647
+ "fate",
648
+ "fawn",
649
+ "fear",
650
+ "feat",
651
+ "feed",
652
+ "feel",
653
+ "feet",
654
+ "fell",
655
+ "felt",
656
+ "fern",
657
+ "fest",
658
+ "film",
659
+ "find",
660
+ "fine",
661
+ "fire",
662
+ "firm",
663
+ "fish",
664
+ "fist",
665
+ "flag",
666
+ "flat",
667
+ "flaw",
668
+ "flew",
669
+ "flip",
670
+ "flow",
671
+ "foam",
672
+ "fold",
673
+ "folk",
674
+ "fond",
675
+ "font",
676
+ "food",
677
+ "fool",
678
+ "foot",
679
+ "ford",
680
+ "fork",
681
+ "form",
682
+ "fort",
683
+ "foul",
684
+ "four",
685
+ "free",
686
+ "frog",
687
+ "from",
688
+ "fuel",
689
+ "full",
690
+ "fund",
691
+ "fuse",
692
+ "gain",
693
+ "gale",
694
+ "game",
695
+ "gang",
696
+ "gate",
697
+ "gave",
698
+ "gaze",
699
+ "gear",
700
+ "gift",
701
+ "give",
702
+ "glad",
703
+ "glow",
704
+ "glue",
705
+ "goal",
706
+ "goat",
707
+ "gold",
708
+ "golf",
709
+ "gone",
710
+ "good",
711
+ "grab",
712
+ "gray",
713
+ "grew",
714
+ "grid",
715
+ "grin",
716
+ "grip",
717
+ "grow",
718
+ "gulf",
719
+ "gust",
720
+ "half",
721
+ "hall",
722
+ "halt",
723
+ "hand",
724
+ "hang",
725
+ "hard",
726
+ "hare",
727
+ "harm",
728
+ "harp",
729
+ "hash",
730
+ "haul",
731
+ "hawk",
732
+ "head",
733
+ "heal",
734
+ "heap",
735
+ "heat",
736
+ "heel",
737
+ "held",
738
+ "helm",
739
+ "help",
740
+ "herb",
741
+ "herd",
742
+ "hero",
743
+ "hide",
744
+ "high",
745
+ "hike",
746
+ "hill",
747
+ "hint",
748
+ "hire",
749
+ "hold",
750
+ "hole",
751
+ "home",
752
+ "hood",
753
+ "hook",
754
+ "hope",
755
+ "horn",
756
+ "hose",
757
+ "host",
758
+ "hour",
759
+ "huge",
760
+ "hulk",
761
+ "hull",
762
+ "hunt",
763
+ "hymn",
764
+ "idea",
765
+ "idle",
766
+ "inch",
767
+ "into",
768
+ "iron",
769
+ "isle",
770
+ "item",
771
+ "jade",
772
+ "jail",
773
+ "jest",
774
+ "join",
775
+ "joke",
776
+ "jolt",
777
+ "jump",
778
+ "just",
779
+ "keen",
780
+ "keep",
781
+ "kick",
782
+ "kind",
783
+ "king",
784
+ "knee",
785
+ "knew",
786
+ "knot",
787
+ "know",
788
+ "lace",
789
+ "lame",
790
+ "lamp",
791
+ "land",
792
+ "lane",
793
+ "lark",
794
+ "lash",
795
+ "last",
796
+ "late",
797
+ "lawn",
798
+ "lead",
799
+ "leaf",
800
+ "lean",
801
+ "leap",
802
+ "left",
803
+ "lens",
804
+ "lift",
805
+ "like",
806
+ "limb",
807
+ "lime",
808
+ "link",
809
+ "lion",
810
+ "list",
811
+ "live",
812
+ "load",
813
+ "loan",
814
+ "lock",
815
+ "loft",
816
+ "long",
817
+ "look",
818
+ "loop",
819
+ "lore",
820
+ "lose",
821
+ "lost",
822
+ "loud",
823
+ "love",
824
+ "luck",
825
+ "lure",
826
+ "lush",
827
+ "made",
828
+ "mail",
829
+ "main",
830
+ "make",
831
+ "mall",
832
+ "mane",
833
+ "many",
834
+ "mark",
835
+ "mass",
836
+ "mast",
837
+ "math",
838
+ "maze",
839
+ "mead",
840
+ "meal",
841
+ "mean",
842
+ "meet",
843
+ "melt",
844
+ "memo",
845
+ "menu",
846
+ "mesh",
847
+ "mild",
848
+ "mile",
849
+ "mill",
850
+ "mime",
851
+ "mind",
852
+ "mine",
853
+ "mint",
854
+ "miss",
855
+ "mist",
856
+ "mode",
857
+ "mold",
858
+ "mole",
859
+ "mood",
860
+ "moon",
861
+ "more",
862
+ "most",
863
+ "move",
864
+ "much",
865
+ "mule",
866
+ "myth",
867
+ "nail",
868
+ "name",
869
+ "neck",
870
+ "need",
871
+ "nest",
872
+ "next",
873
+ "nine",
874
+ "node",
875
+ "noel",
876
+ "norm",
877
+ "nose",
878
+ "note",
879
+ "noun",
880
+ "oath",
881
+ "once",
882
+ "only",
883
+ "open",
884
+ "oven",
885
+ "over",
886
+ "pack",
887
+ "page",
888
+ "pain",
889
+ "pair",
890
+ "palm",
891
+ "park",
892
+ "part",
893
+ "pass",
894
+ "past",
895
+ "path",
896
+ "peak",
897
+ "peel",
898
+ "peer",
899
+ "pelt",
900
+ "pest",
901
+ "pine",
902
+ "pink",
903
+ "pipe",
904
+ "plan",
905
+ "play",
906
+ "plot",
907
+ "plow",
908
+ "plug",
909
+ "plus",
910
+ "poem",
911
+ "poet",
912
+ "pole",
913
+ "poll",
914
+ "pond",
915
+ "pool",
916
+ "poor",
917
+ "port",
918
+ "pose",
919
+ "post",
920
+ "pour",
921
+ "prey",
922
+ "prop",
923
+ "pull",
924
+ "pump",
925
+ "pure",
926
+ "push",
927
+ "race",
928
+ "rack",
929
+ "rain",
930
+ "ramp",
931
+ "rang",
932
+ "rank",
933
+ "rate",
934
+ "read",
935
+ "real",
936
+ "reap",
937
+ "reed",
938
+ "reef",
939
+ "reel",
940
+ "rely",
941
+ "rest",
942
+ "rice",
943
+ "rich",
944
+ "ride",
945
+ "rift",
946
+ "ring",
947
+ "rise",
948
+ "risk",
949
+ "road",
950
+ "roam",
951
+ "roar",
952
+ "role",
953
+ "roll",
954
+ "roof",
955
+ "room",
956
+ "root",
957
+ "rope",
958
+ "rose",
959
+ "rove",
960
+ "ruin",
961
+ "rule",
962
+ "rush",
963
+ "safe",
964
+ "sage",
965
+ "sail",
966
+ "sake",
967
+ "salt",
968
+ "same",
969
+ "sand",
970
+ "sane",
971
+ "sang",
972
+ "sank",
973
+ "save",
974
+ "seal",
975
+ "seam",
976
+ "seat",
977
+ "seed",
978
+ "seek",
979
+ "seem",
980
+ "seep",
981
+ "self",
982
+ "sell",
983
+ "send",
984
+ "sent",
985
+ "shed",
986
+ "shin",
987
+ "ship",
988
+ "shop",
989
+ "shot",
990
+ "show",
991
+ "shut",
992
+ "sick",
993
+ "side",
994
+ "sift",
995
+ "sign",
996
+ "silk",
997
+ "sill",
998
+ "sing",
999
+ "sink",
1000
+ "site",
1001
+ "size",
1002
+ "skin",
1003
+ "slab",
1004
+ "slam",
1005
+ "slap",
1006
+ "slid",
1007
+ "slim",
1008
+ "slip",
1009
+ "slot",
1010
+ "slow",
1011
+ "slug",
1012
+ "snap",
1013
+ "snow",
1014
+ "soak",
1015
+ "soar",
1016
+ "sock",
1017
+ "soft",
1018
+ "soil",
1019
+ "sole",
1020
+ "some",
1021
+ "song",
1022
+ "soot",
1023
+ "sort",
1024
+ "soul",
1025
+ "soup",
1026
+ "sour",
1027
+ "span",
1028
+ "spar",
1029
+ "spin",
1030
+ "spot",
1031
+ "spur",
1032
+ "star",
1033
+ "stay",
1034
+ "stem",
1035
+ "step",
1036
+ "stir",
1037
+ "stop",
1038
+ "stub",
1039
+ "such",
1040
+ "suit",
1041
+ "sung",
1042
+ "sunk",
1043
+ "sure",
1044
+ "surf",
1045
+ "swap",
1046
+ "swim",
1047
+ "tack",
1048
+ "tale",
1049
+ "tall",
1050
+ "tame",
1051
+ "tank",
1052
+ "tape",
1053
+ "task",
1054
+ "team",
1055
+ "tear",
1056
+ "tell",
1057
+ "tend",
1058
+ "tent",
1059
+ "term",
1060
+ "test",
1061
+ "text",
1062
+ "than",
1063
+ "that",
1064
+ "them",
1065
+ "then",
1066
+ "they",
1067
+ "thin",
1068
+ "this",
1069
+ "tide",
1070
+ "tilt",
1071
+ "time",
1072
+ "tiny",
1073
+ "tire",
1074
+ "toad",
1075
+ "toll",
1076
+ "tone",
1077
+ "took",
1078
+ "tool",
1079
+ "tops",
1080
+ "tore",
1081
+ "torn",
1082
+ "toss",
1083
+ "tour",
1084
+ "town",
1085
+ "tray",
1086
+ "trim",
1087
+ "trio",
1088
+ "trip",
1089
+ "true",
1090
+ "tube",
1091
+ "tuck",
1092
+ "tuft",
1093
+ "tune",
1094
+ "turf",
1095
+ "turn",
1096
+ "tusk",
1097
+ "twin",
1098
+ "type",
1099
+ "unit",
1100
+ "upon",
1101
+ "used",
1102
+ "user",
1103
+ "vast",
1104
+ "veil",
1105
+ "very",
1106
+ "vest",
1107
+ "view",
1108
+ "vine",
1109
+ "void",
1110
+ "vote",
1111
+ "wade",
1112
+ "wage",
1113
+ "wake",
1114
+ "walk",
1115
+ "wall",
1116
+ "want",
1117
+ "ward",
1118
+ "warm",
1119
+ "warp",
1120
+ "wary",
1121
+ "wash",
1122
+ "wave",
1123
+ "weak",
1124
+ "weld",
1125
+ "well",
1126
+ "went",
1127
+ "west",
1128
+ "what",
1129
+ "when",
1130
+ "whip",
1131
+ "whom",
1132
+ "wide",
1133
+ "wife",
1134
+ "wild",
1135
+ "will",
1136
+ "wilt",
1137
+ "wind",
1138
+ "wing",
1139
+ "wink",
1140
+ "wire",
1141
+ "wise",
1142
+ "wish",
1143
+ "with",
1144
+ "wolf",
1145
+ "wood",
1146
+ "wore",
1147
+ "worm",
1148
+ "wove",
1149
+ "wrap",
1150
+ "wren",
1151
+ "yard",
1152
+ "yarn",
1153
+ "yell",
1154
+ "your",
1155
+ "zinc",
1156
+ "zone",
1157
+ "zoom"
1158
+ ];
1159
+ WORD_LIST.length;
1160
+ function getRandomWordIndex(listLength) {
1161
+ if (listLength <= 256) {
1162
+ const maxValid = 256 - 256 % listLength;
1163
+ const buf = new Uint8Array(1);
1164
+ let b;
1165
+ do {
1166
+ crypto.getRandomValues(buf);
1167
+ b = buf[0];
1168
+ } while (b >= maxValid);
1169
+ return b % listLength;
1170
+ } else {
1171
+ const RANGE40 = 1099511627776;
1172
+ const maxValid = Math.floor(RANGE40 / listLength) * listLength;
1173
+ const buf = new Uint8Array(5);
1174
+ let n;
1175
+ do {
1176
+ crypto.getRandomValues(buf);
1177
+ n = buf[0] * 4294967296 + ((buf[1] << 24 | buf[2] << 16 | buf[3] << 8 | buf[4]) >>> 0);
1178
+ } while (n >= maxValid);
1179
+ return n % listLength;
1180
+ }
1181
+ }
1182
+ function generatePassphrase(options = {}) {
1183
+ const {
1184
+ words = 4,
1185
+ separator = "-",
1186
+ capitalize = false,
1187
+ appendDigit = false,
1188
+ customWords,
1189
+ prefix
1190
+ } = options;
1191
+ if (words < 2) throw new Error("words must be at least 2.");
1192
+ const wordList = customWords ?? WORD_LIST;
1193
+ if (wordList.length < 2) throw new Error("Word list must contain at least 2 words.");
1194
+ const selected = [];
1195
+ for (let i = 0; i < words; i++) {
1196
+ const idx = getRandomWordIndex(wordList.length);
1197
+ let word = wordList[idx];
1198
+ if (capitalize) word = word.charAt(0).toUpperCase() + word.slice(1);
1199
+ selected.push(word);
1200
+ }
1201
+ let passphrase = selected.join(separator);
1202
+ if (appendDigit) {
1203
+ const digit = getRandomChar("0123456789");
1204
+ passphrase += digit;
1205
+ }
1206
+ if (prefix) passphrase = prefix + passphrase;
1207
+ const entropyBits = Math.log2(Math.pow(wordList.length, words)) + (appendDigit ? Math.log2(10) : 0);
1208
+ return {
1209
+ passphrase,
1210
+ wordCount: words,
1211
+ entropyBits: Math.round(entropyBits * 10) / 10
1212
+ };
1213
+ }
1214
+
1215
+ // src/entropy.ts
1216
+ function estimateEntropy(options = {}) {
1217
+ const {
1218
+ length = 16,
1219
+ prefix = "",
1220
+ suffix = "",
1221
+ pattern,
1222
+ count: _count,
1223
+ startWith: _startWith,
1224
+ ...charsetOptions
1225
+ } = options;
1226
+ if (pattern) {
1227
+ return estimatePatternEntropy(pattern, charsetOptions);
1228
+ }
1229
+ const charset = buildCharset(charsetOptions);
1230
+ const charsetSize = charset.length;
1231
+ const effectiveLength = Math.max(0, length - prefix.length - suffix.length);
1232
+ if (effectiveLength === 0) {
1233
+ return {
1234
+ bits: 0,
1235
+ strength: "very-weak",
1236
+ charsetSize,
1237
+ effectiveLength: 0,
1238
+ combinations: "1"
1239
+ };
1240
+ }
1241
+ const bitsPerChar = Math.log2(charsetSize);
1242
+ const bits = bitsPerChar * effectiveLength;
1243
+ return {
1244
+ bits: Math.round(bits * 100) / 100,
1245
+ strength: strengthLabel(bits),
1246
+ charsetSize,
1247
+ effectiveLength,
1248
+ combinations: approximateCombinations(charsetSize, effectiveLength)
1249
+ };
1250
+ }
1251
+ function estimatePatternEntropy(pattern, charsetOptions) {
1252
+ const fullCharset = buildCharset(charsetOptions);
1253
+ [...fullCharset].filter((c) => /[a-zA-Z]/.test(c)).join("");
1254
+ const upperChars = [...fullCharset].filter((c) => /[A-Z]/.test(c)).join("");
1255
+ const lowerChars = [...fullCharset].filter((c) => /[a-z]/.test(c)).join("");
1256
+ const digitChars = "0123456789";
1257
+ const alphanumChars = [...fullCharset].filter((c) => /[a-zA-Z0-9]/.test(c)).join("");
1258
+ let totalBits = 0;
1259
+ let effectiveLength = 0;
1260
+ let i = 0;
1261
+ while (i < pattern.length) {
1262
+ const ch = pattern[i];
1263
+ if (ch === "\\" && i + 1 < pattern.length) {
1264
+ i += 2;
1265
+ continue;
1266
+ }
1267
+ let placeholderCharsetSize = 0;
1268
+ switch (ch) {
1269
+ case "#":
1270
+ placeholderCharsetSize = digitChars.length;
1271
+ effectiveLength++;
1272
+ break;
1273
+ case "A":
1274
+ placeholderCharsetSize = Math.max(upperChars.length, 1);
1275
+ effectiveLength++;
1276
+ break;
1277
+ case "a":
1278
+ placeholderCharsetSize = Math.max(lowerChars.length, 1);
1279
+ effectiveLength++;
1280
+ break;
1281
+ case "*":
1282
+ placeholderCharsetSize = fullCharset.length;
1283
+ effectiveLength++;
1284
+ break;
1285
+ case "?":
1286
+ placeholderCharsetSize = Math.max(alphanumChars.length, 1);
1287
+ effectiveLength++;
1288
+ break;
1289
+ }
1290
+ if (placeholderCharsetSize > 0) {
1291
+ totalBits += Math.log2(placeholderCharsetSize);
1292
+ }
1293
+ i++;
1294
+ }
1295
+ return {
1296
+ bits: Math.round(totalBits * 100) / 100,
1297
+ strength: strengthLabel(totalBits),
1298
+ charsetSize: fullCharset.length,
1299
+ effectiveLength,
1300
+ combinations: approximateCombinations(fullCharset.length, effectiveLength)
1301
+ };
1302
+ }
1303
+ function strengthLabel(bits) {
1304
+ if (bits < 28) return "very-weak";
1305
+ if (bits < 50) return "weak";
1306
+ if (bits < 72) return "fair";
1307
+ if (bits < 100) return "strong";
1308
+ return "very-strong";
1309
+ }
1310
+ function approximateCombinations(charsetSize, length) {
1311
+ const log10 = length * Math.log10(charsetSize);
1312
+ if (log10 > 15) {
1313
+ const exp = Math.floor(log10);
1314
+ const mantissa = Math.pow(10, log10 - exp);
1315
+ return `${mantissa.toFixed(2)}e+${exp}`;
1316
+ }
1317
+ return Math.round(Math.pow(charsetSize, length)).toString();
1318
+ }
1319
+
1320
+ // src/validate.ts
1321
+ var DIGIT_RE = /[0-9]/;
1322
+ var LOWER_RE = /[a-z]/;
1323
+ var UPPER_RE = /[A-Z]/;
1324
+ var SYMBOL_RE = /[!@#$%^&*()_+\[\]{}<>?]/;
1325
+ function validateRandomString(str, options = {}) {
1326
+ const errors = [];
1327
+ const {
1328
+ minLength,
1329
+ maxLength,
1330
+ length,
1331
+ requireNumeric,
1332
+ requireLowercase,
1333
+ requireUppercase,
1334
+ requireSymbols,
1335
+ charset,
1336
+ pattern
1337
+ } = options;
1338
+ if (length !== void 0 && str.length !== length) {
1339
+ errors.push(`Length must be exactly ${length}, got ${str.length}.`);
1340
+ }
1341
+ if (minLength !== void 0 && str.length < minLength) {
1342
+ errors.push(`Length must be at least ${minLength}, got ${str.length}.`);
1343
+ }
1344
+ if (maxLength !== void 0 && str.length > maxLength) {
1345
+ errors.push(`Length must be at most ${maxLength}, got ${str.length}.`);
1346
+ }
1347
+ if (requireNumeric && !DIGIT_RE.test(str)) {
1348
+ errors.push("Must contain at least one numeric digit.");
1349
+ }
1350
+ if (requireLowercase && !LOWER_RE.test(str)) {
1351
+ errors.push("Must contain at least one lowercase letter.");
1352
+ }
1353
+ if (requireUppercase && !UPPER_RE.test(str)) {
1354
+ errors.push("Must contain at least one uppercase letter.");
1355
+ }
1356
+ if (requireSymbols && !SYMBOL_RE.test(str)) {
1357
+ errors.push("Must contain at least one symbol.");
1358
+ }
1359
+ if (charset !== void 0) {
1360
+ const charsetSet = new Set(charset);
1361
+ const invalidChars = [...new Set(str)].filter((ch) => !charsetSet.has(ch));
1362
+ if (invalidChars.length > 0) {
1363
+ errors.push(
1364
+ `Contains characters not in the allowed charset: ${invalidChars.map((c) => JSON.stringify(c)).join(", ")}.`
1365
+ );
1366
+ }
1367
+ }
1368
+ if (pattern !== void 0) {
1369
+ const patternErrors = validatePattern(str, pattern);
1370
+ errors.push(...patternErrors);
1371
+ }
1372
+ return { valid: errors.length === 0, errors };
1373
+ }
1374
+ function validatePattern(str, pattern) {
1375
+ const errors = [];
1376
+ const expandedPattern = [];
1377
+ let i = 0;
1378
+ while (i < pattern.length) {
1379
+ const ch = pattern[i];
1380
+ if (ch === "\\" && i + 1 < pattern.length) {
1381
+ expandedPattern.push({ type: "literal", value: pattern[i + 1] });
1382
+ i += 2;
1383
+ } else if (["#", "A", "a", "*", "?"].includes(ch)) {
1384
+ expandedPattern.push({ type: "placeholder", value: ch });
1385
+ i++;
1386
+ } else {
1387
+ expandedPattern.push({ type: "literal", value: ch });
1388
+ i++;
1389
+ }
1390
+ }
1391
+ if (str.length !== expandedPattern.length) {
1392
+ errors.push(
1393
+ `Pattern expects length ${expandedPattern.length}, got ${str.length}.`
1394
+ );
1395
+ return errors;
1396
+ }
1397
+ for (let pos = 0; pos < expandedPattern.length; pos++) {
1398
+ const token = expandedPattern[pos];
1399
+ const ch = str[pos];
1400
+ if (token.type === "literal") {
1401
+ if (ch !== token.value) {
1402
+ errors.push(
1403
+ `Position ${pos}: expected literal '${token.value}', got '${ch}'.`
1404
+ );
1405
+ }
1406
+ } else {
1407
+ switch (token.value) {
1408
+ case "#":
1409
+ if (!DIGIT_RE.test(ch)) {
1410
+ errors.push(`Position ${pos}: '#' expects a digit, got '${ch}'.`);
1411
+ }
1412
+ break;
1413
+ case "A":
1414
+ if (!/[A-Z]/.test(ch)) {
1415
+ errors.push(`Position ${pos}: 'A' expects an uppercase letter, got '${ch}'.`);
1416
+ }
1417
+ break;
1418
+ case "a":
1419
+ if (!LOWER_RE.test(ch)) {
1420
+ errors.push(`Position ${pos}: 'a' expects a lowercase letter, got '${ch}'.`);
1421
+ }
1422
+ break;
1423
+ case "?":
1424
+ if (!/[a-zA-Z0-9]/.test(ch)) {
1425
+ errors.push(`Position ${pos}: '?' expects an alphanumeric character, got '${ch}'.`);
1426
+ }
1427
+ break;
1428
+ }
1429
+ }
1430
+ }
1431
+ return errors;
1432
+ }
1433
+
1434
+ // src/presets.ts
1435
+ var PRESETS = {
1436
+ /**
1437
+ * API token / session key.
1438
+ * 32-char alphanumeric (no symbols), readable=false for max entropy.
1439
+ * ~190 bits of entropy.
1440
+ */
1441
+ TOKEN: {
1442
+ length: 32,
1443
+ numeric: true,
1444
+ lowercase: true,
1445
+ uppercase: true,
1446
+ symbols: false
1447
+ },
1448
+ /**
1449
+ * Strong password.
1450
+ * 20 chars, all character types including symbols.
1451
+ * ~131 bits of entropy.
1452
+ */
1453
+ PASSWORD: {
1454
+ length: 20,
1455
+ numeric: true,
1456
+ lowercase: true,
1457
+ uppercase: true,
1458
+ symbols: true
1459
+ },
1460
+ /**
1461
+ * Human-readable token — no ambiguous characters (0/O/l/I/1).
1462
+ * Safe to read aloud or transcribe from a screen.
1463
+ * 16 chars, ~93 bits of entropy.
1464
+ */
1465
+ READABLE: {
1466
+ length: 16,
1467
+ numeric: true,
1468
+ lowercase: true,
1469
+ uppercase: true,
1470
+ symbols: false,
1471
+ readable: true
1472
+ },
1473
+ /**
1474
+ * URL-safe slug identifier.
1475
+ * Lowercase letters and digits only, starts with a letter.
1476
+ * 12 chars, ~62 bits of entropy.
1477
+ */
1478
+ SLUG: {
1479
+ length: 12,
1480
+ numeric: true,
1481
+ lowercase: true,
1482
+ uppercase: false,
1483
+ symbols: false,
1484
+ startWith: "alphabet"
1485
+ },
1486
+ /**
1487
+ * Lowercase hex string.
1488
+ * Compatible with UUID hex components, color codes, hash representations.
1489
+ * 32 chars (128-bit equivalent), ~128 bits of entropy.
1490
+ */
1491
+ HEX: {
1492
+ length: 32,
1493
+ charset: "0123456789abcdef"
1494
+ },
1495
+ /**
1496
+ * Numeric PIN code. 6 digits, starts with a non-zero digit.
1497
+ * ~17 bits of entropy — suitable for short-lived OTP codes.
1498
+ */
1499
+ PIN: {
1500
+ length: 6,
1501
+ numeric: true,
1502
+ lowercase: false,
1503
+ uppercase: false,
1504
+ symbols: false,
1505
+ exclude: "0",
1506
+ startWith: "numeric"
1507
+ },
1508
+ /**
1509
+ * UUID-like string (not RFC 4122 compliant, but same visual format).
1510
+ * Uses `pattern` to produce 8-4-4-4-12 hex grouping.
1511
+ * ~122 bits of entropy (same as UUIDv4).
1512
+ */
1513
+ UUID_LIKE: {
1514
+ pattern: "\\*\\*\\*\\*\\*\\*\\*\\*-\\*\\*\\*\\*-\\*\\*\\*\\*-\\*\\*\\*\\*-\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*\\*",
1515
+ charset: "0123456789abcdef"
1516
+ },
1517
+ /**
1518
+ * Short alphanumeric ID — suitable for database record IDs.
1519
+ * 8 chars, uppercase alphanumeric, starts with a letter.
1520
+ * ~41 bits — good for IDs when combined with a namespace/prefix.
1521
+ */
1522
+ SHORT_ID: {
1523
+ length: 8,
1524
+ numeric: true,
1525
+ lowercase: false,
1526
+ uppercase: true,
1527
+ symbols: false,
1528
+ startWith: "alphabet"
1529
+ },
1530
+ /**
1531
+ * Invitation / redemption code.
1532
+ * 16 chars, readable (no ambiguous chars), pattern grouped for readability.
1533
+ * @example 'KXPZ-9MR2-LQ4Y-8WVN'
1534
+ */
1535
+ INVITE_CODE: {
1536
+ pattern: "AAAA-AAAA-AAAA-AAAA",
1537
+ readable: true
1538
+ }
1539
+ };
1540
+
1541
+ // src/zod.ts
1542
+ function randomStringSchema(options = {}, zod) {
1543
+ let z;
1544
+ if (zod) {
1545
+ z = zod;
1546
+ } else {
1547
+ try {
1548
+ const req = typeof globalThis !== "undefined" && globalThis["require"];
1549
+ z = req ? req("zod") : (() => {
1550
+ throw new Error("no require");
1551
+ })();
1552
+ } catch {
1553
+ throw new Error(
1554
+ "@ppabari/strio: Zod is not installed. Run `npm install zod` or pass the `z` object explicitly: `randomStringSchema(options, z)`."
1555
+ );
1556
+ }
1557
+ }
1558
+ const { description, ...validateOpts } = options;
1559
+ let schema = z.string().refine(
1560
+ (val) => {
1561
+ const result = validateRandomString(val, validateOpts);
1562
+ return result.valid;
1563
+ },
1564
+ {
1565
+ message: buildMessage(validateOpts)
1566
+ }
1567
+ );
1568
+ if (description) {
1569
+ schema = schema.describe(description);
1570
+ }
1571
+ return schema;
1572
+ }
1573
+ function buildMessage(opts) {
1574
+ const parts = [];
1575
+ if (opts.length !== void 0) parts.push(`exactly ${opts.length} characters`);
1576
+ if (opts.minLength !== void 0) parts.push(`at least ${opts.minLength} characters`);
1577
+ if (opts.maxLength !== void 0) parts.push(`at most ${opts.maxLength} characters`);
1578
+ if (opts.requireNumeric) parts.push("at least one digit");
1579
+ if (opts.requireLowercase) parts.push("at least one lowercase letter");
1580
+ if (opts.requireUppercase) parts.push("at least one uppercase letter");
1581
+ if (opts.requireSymbols) parts.push("at least one symbol");
1582
+ if (opts.charset) parts.push(`only characters from '${opts.charset}'`);
1583
+ if (opts.pattern) parts.push(`matching pattern '${opts.pattern}'`);
1584
+ return parts.length > 0 ? `String must contain ${parts.join(", ")}.` : "Invalid string format.";
1585
+ }
1586
+
1587
+ // src/secrets.ts
1588
+ function randomBytes(length) {
1589
+ if (length <= 0 || !Number.isInteger(length)) {
1590
+ throw new RangeError(`length must be a positive integer, got ${length}.`);
1591
+ }
1592
+ const buf = new Uint8Array(length);
1593
+ crypto.getRandomValues(buf);
1594
+ return buf;
1595
+ }
1596
+ function bytesToHex(bytes) {
1597
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1598
+ }
1599
+ function bytesToBase64(bytes) {
1600
+ return btoa(String.fromCharCode(...bytes));
1601
+ }
1602
+ function bytesToBase64Url(bytes) {
1603
+ return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1604
+ }
1605
+ function generateBytes(length) {
1606
+ return randomBytes(length);
1607
+ }
1608
+ function generateHexKey(bits = 256) {
1609
+ if (bits <= 0 || bits % 8 !== 0) {
1610
+ throw new RangeError(`bits must be a positive multiple of 8, got ${bits}.`);
1611
+ }
1612
+ return bytesToHex(randomBytes(bits / 8));
1613
+ }
1614
+ function generateBase64Key(bits = 256, variant = "url-safe-no-pad") {
1615
+ if (bits <= 0 || bits % 8 !== 0) {
1616
+ throw new RangeError(`bits must be a positive multiple of 8, got ${bits}.`);
1617
+ }
1618
+ const bytes = randomBytes(bits / 8);
1619
+ switch (variant) {
1620
+ case "standard":
1621
+ return bytesToBase64(bytes);
1622
+ case "url-safe":
1623
+ return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_");
1624
+ case "url-safe-no-pad":
1625
+ return bytesToBase64Url(bytes);
1626
+ default:
1627
+ throw new TypeError(`Unknown variant '${variant}'. Use 'standard', 'url-safe', or 'url-safe-no-pad'.`);
1628
+ }
1629
+ }
1630
+ var JWT_MIN_BITS = {
1631
+ HS256: 256,
1632
+ HS384: 384,
1633
+ HS512: 512
1634
+ };
1635
+ function generateJwtSecret(options = {}) {
1636
+ const {
1637
+ algorithm = "HS256",
1638
+ format = "base64url",
1639
+ bits: bitsOverride
1640
+ } = options;
1641
+ const minBits = JWT_MIN_BITS[algorithm];
1642
+ if (!minBits) {
1643
+ throw new TypeError(
1644
+ `Unknown algorithm '${algorithm}'. Supported: ${Object.keys(JWT_MIN_BITS).join(", ")}.`
1645
+ );
1646
+ }
1647
+ if (bitsOverride !== void 0 && bitsOverride < minBits) {
1648
+ throw new RangeError(
1649
+ `${algorithm} requires at least ${minBits} bits. Got ${bitsOverride}. Use bits >= ${minBits} or omit the bits option.`
1650
+ );
1651
+ }
1652
+ const bits = bitsOverride ?? minBits;
1653
+ if (bits % 8 !== 0) {
1654
+ throw new RangeError(`bits must be a multiple of 8, got ${bits}.`);
1655
+ }
1656
+ const bytes = randomBytes(bits / 8);
1657
+ let secret;
1658
+ switch (format) {
1659
+ case "base64url":
1660
+ secret = bytesToBase64Url(bytes);
1661
+ break;
1662
+ case "hex":
1663
+ secret = bytesToHex(bytes);
1664
+ break;
1665
+ case "utf8-like": {
1666
+ const B622 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
1667
+ const charsNeeded = Math.ceil(bits / Math.log2(62));
1668
+ const resultChars = [];
1669
+ const maxValid = 256 - 256 % 62;
1670
+ const drawBuf = new Uint8Array(charsNeeded * 2);
1671
+ crypto.getRandomValues(drawBuf);
1672
+ for (const b of drawBuf) {
1673
+ if (b < maxValid) {
1674
+ resultChars.push(B622[b % 62]);
1675
+ if (resultChars.length >= charsNeeded) break;
1676
+ }
1677
+ }
1678
+ secret = resultChars.join("");
1679
+ break;
1680
+ }
1681
+ default:
1682
+ throw new TypeError(`Unknown format '${format}'.`);
1683
+ }
1684
+ const example = format === "base64url" ? `jwt.sign(payload, '${secret.slice(0, 8)}...', { algorithm: '${algorithm}' })` : `jwt.sign(payload, Buffer.from('${secret.slice(0, 8)}...', '${format === "hex" ? "hex" : "utf8"}'), { algorithm: '${algorithm}' })`;
1685
+ return { secret, algorithm, bits, format, example };
1686
+ }
1687
+ function generateApiKey(options = {}) {
1688
+ const {
1689
+ type = "sk",
1690
+ environment,
1691
+ bits = 256,
1692
+ charset = "base62",
1693
+ separator = "_"
1694
+ } = options;
1695
+ if (bits <= 0 || bits % 8 !== 0) {
1696
+ throw new RangeError(`bits must be a positive multiple of 8, got ${bits}.`);
1697
+ }
1698
+ const CHARSETS2 = {
1699
+ base62: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
1700
+ base58: "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
1701
+ hex: "0123456789abcdef"
1702
+ };
1703
+ const cs = CHARSETS2[charset] ?? charset;
1704
+ if (cs.length < 2) throw new TypeError(`charset '${charset}' resolves to fewer than 2 unique characters.`);
1705
+ const charsNeeded = Math.ceil(bits / Math.log2(cs.length));
1706
+ const maxValid = 256 - 256 % cs.length;
1707
+ const randomChars = [];
1708
+ const buf = new Uint8Array(charsNeeded * 2 + 64);
1709
+ while (randomChars.length < charsNeeded) {
1710
+ crypto.getRandomValues(buf);
1711
+ for (const b of buf) {
1712
+ if (b < maxValid) {
1713
+ randomChars.push(cs[b % cs.length]);
1714
+ if (randomChars.length >= charsNeeded) break;
1715
+ }
1716
+ }
1717
+ }
1718
+ const randomPortion = randomChars.join("");
1719
+ const prefixParts = [type, environment].filter(Boolean);
1720
+ const prefix = prefixParts.join(separator);
1721
+ const key = prefix + separator + randomPortion;
1722
+ return { key, prefix, randomLength: charsNeeded, bits };
1723
+ }
1724
+ function generateOtp(options = {}) {
1725
+ const { digits = 6, allowLeadingZero = true } = options;
1726
+ if (digits < 1 || digits > 10) {
1727
+ throw new RangeError(`digits must be between 1 and 10, got ${digits}.`);
1728
+ }
1729
+ const max = Math.pow(10, digits);
1730
+ let n;
1731
+ if (digits <= 9) {
1732
+ const maxValid = Math.floor(4294967296 / max) * max;
1733
+ const buf = new Uint8Array(4);
1734
+ do {
1735
+ crypto.getRandomValues(buf);
1736
+ n = (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
1737
+ } while (n >= maxValid);
1738
+ } else {
1739
+ const RANGE40 = 1099511627776;
1740
+ const maxValid40 = Math.floor(RANGE40 / max) * max;
1741
+ const buf = new Uint8Array(5);
1742
+ do {
1743
+ crypto.getRandomValues(buf);
1744
+ n = buf[0] * 4294967296 + ((buf[1] << 24 | buf[2] << 16 | buf[3] << 8 | buf[4]) >>> 0);
1745
+ } while (n >= maxValid40);
1746
+ }
1747
+ let value = n % max;
1748
+ if (!allowLeadingZero && digits > 1) {
1749
+ const minValue = Math.pow(10, digits - 1);
1750
+ if (value < minValue) {
1751
+ const range = max - minValue;
1752
+ if (digits <= 9) {
1753
+ const maxValid = Math.floor(4294967296 / range) * range;
1754
+ const buf2 = new Uint8Array(4);
1755
+ let m;
1756
+ do {
1757
+ crypto.getRandomValues(buf2);
1758
+ m = (buf2[0] << 24 | buf2[1] << 16 | buf2[2] << 8 | buf2[3]) >>> 0;
1759
+ } while (m >= maxValid);
1760
+ value = minValue + m % range;
1761
+ } else {
1762
+ const RANGE40 = 1099511627776;
1763
+ const maxValid40 = Math.floor(RANGE40 / range) * range;
1764
+ const buf2 = new Uint8Array(5);
1765
+ let m;
1766
+ do {
1767
+ crypto.getRandomValues(buf2);
1768
+ m = buf2[0] * 4294967296 + ((buf2[1] << 24 | buf2[2] << 16 | buf2[3] << 8 | buf2[4]) >>> 0);
1769
+ } while (m >= maxValid40);
1770
+ value = minValue + m % range;
1771
+ }
1772
+ }
1773
+ }
1774
+ const code = value.toString().padStart(digits, "0");
1775
+ return { code, value, digits };
1776
+ }
1777
+ function maskSecret(secret, options = {}) {
1778
+ if (!secret) return "";
1779
+ const {
1780
+ visibleStart = 0,
1781
+ visibleEnd = 4,
1782
+ maskChar = "*",
1783
+ minMaskLength = 4,
1784
+ maxMaskLength = 8,
1785
+ knownPrefixes = [
1786
+ "strio_live_",
1787
+ "strio_test_",
1788
+ "strio_dev_",
1789
+ "myapp_live_",
1790
+ "myapp_test_",
1791
+ "myapp_dev_",
1792
+ "tok_",
1793
+ "api_",
1794
+ "key_"
1795
+ ]
1796
+ } = options;
1797
+ let prefix = "";
1798
+ let payload = secret;
1799
+ for (const kp of knownPrefixes) {
1800
+ if (secret.startsWith(kp)) {
1801
+ prefix = kp;
1802
+ payload = secret.slice(kp.length);
1803
+ break;
1804
+ }
1805
+ }
1806
+ const totalLen = payload.length;
1807
+ const showStart = Math.min(visibleStart, totalLen);
1808
+ const showEnd = Math.min(visibleEnd, totalLen - showStart);
1809
+ const hiddenLen = totalLen - showStart - showEnd;
1810
+ if (hiddenLen <= 0) {
1811
+ return prefix + maskChar.repeat(minMaskLength);
1812
+ }
1813
+ const maskLen = Math.max(minMaskLength, Math.min(maxMaskLength, hiddenLen));
1814
+ const mask = maskChar.repeat(maskLen);
1815
+ const startPart = showStart > 0 ? payload.slice(0, showStart) : "";
1816
+ const endPart = showEnd > 0 ? payload.slice(totalLen - showEnd) : "";
1817
+ return `${prefix}${startPart}${mask}...${endPart}`;
1818
+ }
1819
+ function timingSafeEqual(a, b) {
1820
+ const encoder = new TextEncoder();
1821
+ const bufA = typeof a === "string" ? encoder.encode(a) : a;
1822
+ const bufB = typeof b === "string" ? encoder.encode(b) : b;
1823
+ const len = Math.max(bufA.length, bufB.length);
1824
+ const padA = new Uint8Array(len);
1825
+ const padB = new Uint8Array(len);
1826
+ padA.set(bufA);
1827
+ padB.set(bufB);
1828
+ let diff = bufA.length === bufB.length ? 0 : 1;
1829
+ for (let i = 0; i < len; i++) {
1830
+ diff |= padA[i] ^ padB[i];
1831
+ }
1832
+ return diff === 0;
1833
+ }
1834
+ function generateCookieSecret(bits = 256) {
1835
+ if (bits < 128) throw new RangeError("Cookie secret should be at least 128 bits.");
1836
+ return generateBase64Key(bits, "url-safe-no-pad");
1837
+ }
1838
+ function generateAesKey(keySize = 256, format = "hex") {
1839
+ const bytes = randomBytes(keySize / 8);
1840
+ if (format === "bytes") return bytes;
1841
+ if (format === "hex") return bytesToHex(bytes);
1842
+ return bytesToBase64Url(bytes);
1843
+ }
1844
+ function generateNextAuthSecret() {
1845
+ return generateBase64Key(256, "url-safe-no-pad");
1846
+ }
1847
+ function generateDjangoSecretKey() {
1848
+ const DJANGO_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)ABCDEFGHIJKLMNOPQRSTUVWXYZ";
1849
+ const length = 64;
1850
+ const maxValid = 256 - 256 % DJANGO_CHARS.length;
1851
+ const chars = [];
1852
+ const buf = new Uint8Array(length * 2);
1853
+ while (chars.length < length) {
1854
+ crypto.getRandomValues(buf);
1855
+ for (const b of buf) {
1856
+ if (b < maxValid) {
1857
+ chars.push(DJANGO_CHARS[b % DJANGO_CHARS.length]);
1858
+ if (chars.length >= length) break;
1859
+ }
1860
+ }
1861
+ }
1862
+ return chars.join("");
1863
+ }
1864
+ function generateRailsSecretKeyBase() {
1865
+ return generateHexKey(512);
1866
+ }
1867
+
1868
+ // src/index.ts
1869
+ function generateRandomString(options = {}) {
1870
+ const { count = 1, ...rest } = options;
1871
+ if (count === 1) return generateOne(rest);
1872
+ return generateBatch(rest, count);
1873
+ }
1874
+ async function generateRandomStringAsync(options = {}) {
1875
+ await Promise.resolve();
1876
+ const { count = 1, ...rest } = options;
1877
+ if (count === 1) return generateOne(rest);
1878
+ return generateBatch(rest, count);
1879
+ }
1880
+
1881
+ exports.BUILT_IN_WORD_LIST = WORD_LIST;
1882
+ exports.CHARSET_ALIASES = CHARSET_ALIASES;
1883
+ exports.PRESETS = PRESETS;
1884
+ exports.estimateEntropy = estimateEntropy;
1885
+ exports.generateAesKey = generateAesKey;
1886
+ exports.generateApiKey = generateApiKey;
1887
+ exports.generateBase64Key = generateBase64Key;
1888
+ exports.generateBytes = generateBytes;
1889
+ exports.generateCookieSecret = generateCookieSecret;
1890
+ exports.generateDjangoSecretKey = generateDjangoSecretKey;
1891
+ exports.generateExpiringToken = generateExpiringToken;
1892
+ exports.generateHexKey = generateHexKey;
1893
+ exports.generateId = generateId;
1894
+ exports.generateJwtSecret = generateJwtSecret;
1895
+ exports.generateNextAuthSecret = generateNextAuthSecret;
1896
+ exports.generateOtp = generateOtp;
1897
+ exports.generatePassphrase = generatePassphrase;
1898
+ exports.generateRailsSecretKeyBase = generateRailsSecretKeyBase;
1899
+ exports.generateRandomString = generateRandomString;
1900
+ exports.generateRandomStringAsync = generateRandomStringAsync;
1901
+ exports.maskSecret = maskSecret;
1902
+ exports.randomStringSchema = randomStringSchema;
1903
+ exports.randomStringStream = randomStringStream;
1904
+ exports.resolveCharsetAlias = resolveCharsetAlias;
1905
+ exports.take = take;
1906
+ exports.takeWhere = takeWhere;
1907
+ exports.timingSafeEqual = timingSafeEqual;
1908
+ exports.uniqueRandomStringStream = uniqueRandomStringStream;
1909
+ exports.validateId = validateId;
1910
+ exports.validateRandomString = validateRandomString;
1911
+ exports.verifyToken = verifyToken;
1912
+ //# sourceMappingURL=index.cjs.map
1913
+ //# sourceMappingURL=index.cjs.map