@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 ADDED
@@ -0,0 +1,85 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@ppabari/strio` will be documented here.
4
+
5
+ This project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ---
8
+
9
+ ## [1.0.0] — 2025
10
+
11
+ ### Added
12
+
13
+ **Core generation**
14
+ - `generateRandomString(options)` — cryptographically secure string generation, sync
15
+ - `generateRandomStringAsync(options)` — non-blocking async variant
16
+ - `length`, `numeric`, `lowercase`, `uppercase`, `symbols`, `readable`, `exclude`, `charset`, `startWith`, `prefix`, `suffix`, `pattern`, `count` options
17
+ - `seed` option for deterministic/reproducible output (tests and fixtures)
18
+
19
+ **Named charset aliases**
20
+ - `charset: 'base58'` — Bitcoin alphabet, no ambiguous chars
21
+ - `charset: 'base62'` — max-density alphanumeric
22
+ - `charset: 'base64url'` — URL-safe, no padding
23
+ - `charset: 'base32'`, `'base36'`, `'crockford32'`, `'hex'`, `'alpha'`, `'numeric'`
24
+ - Full list exported as `CHARSET_ALIASES`
25
+
26
+ **Presets**
27
+ - `PRESETS.TOKEN` — 32-char API token
28
+ - `PRESETS.PASSWORD` — 20-char with symbols
29
+ - `PRESETS.READABLE` — no ambiguous characters
30
+ - `PRESETS.SLUG` — lowercase URL slug
31
+ - `PRESETS.HEX` — 32-char hex
32
+ - `PRESETS.PIN` — 6-digit, no leading zero
33
+ - `PRESETS.SHORT_ID` — 8-char uppercase
34
+ - `PRESETS.INVITE_CODE` — `AAAA-AAAA-AAAA-AAAA` format
35
+
36
+ **Short IDs**
37
+ - `generateId(options)` — prefixed IDs with Luhn-style checksum
38
+ - `validateId(id, options)` — detects single-character transcription errors
39
+
40
+ **Expiring tokens**
41
+ - `generateExpiringToken(options)` — self-expiring tokens with base62-encoded TTL
42
+ - `verifyToken(token)` — parse and validate expiry without a database
43
+
44
+ **Stream / iterator API**
45
+ - `randomStringStream(options)` — infinite async generator
46
+ - `uniqueRandomStringStream(options)` — deduplicating async generator
47
+ - `take(iterable, n)` — collect N items from any async iterable
48
+ - `takeWhere(iterable, n, predicate)` — filtered collection
49
+
50
+ **Passphrase generator**
51
+ - `generatePassphrase(options)` — 512-word built-in list, separator, capitalize, appendDigit
52
+ - `BUILT_IN_WORD_LIST` — exported for reference or customisation
53
+
54
+ **Secrets & key generation**
55
+ - `generateBytes(n)` — raw `Uint8Array` for Web Crypto and Node crypto
56
+ - `generateHexKey(bits)` — lowercase hex key (AES, HMAC, Redis)
57
+ - `generateBase64Key(bits, variant)` — standard / url-safe / url-safe-no-pad
58
+ - `generateJwtSecret(options)` — HS256/384/512 with RFC 7518 minimum enforcement
59
+ - `generateApiKey(options)` — structured `type_env_random` API keys
60
+ - `generateOtp(options)` — numeric OTP codes (1–10 digits), bias-free
61
+ - `maskSecret(secret, options)` — safe logging with prefix preservation
62
+ - `timingSafeEqual(a, b)` — constant-time comparison (prevents timing attacks)
63
+ - `generateCookieSecret()` — for express-session, iron-session, lucia
64
+ - `generateAesKey(keySize, format)` — AES-128/192/256 in hex, base64, or bytes
65
+ - `generateNextAuthSecret()` — `AUTH_SECRET` for Next.js / Auth.js
66
+ - `generateDjangoSecretKey()` — compatible with Django's `SECRET_KEY`
67
+ - `generateRailsSecretKeyBase()` — 512-bit hex for Rails
68
+
69
+ **Utilities**
70
+ - `estimateEntropy(options)` — bits, strength label, charset size, combinations
71
+ - `validateRandomString(str, options)` — length, charset, required types, pattern
72
+ - `randomStringSchema(options, z)` — Zod integration (optional peer dep)
73
+
74
+ **CLI**
75
+ - `npx @ppabari/strio` — generate strings from the terminal
76
+ - `--preset`, `--length`, `--count`, `--charset`, `--pattern` and more
77
+ - `--passphrase`, `--id`, `--expiring`, `--entropy` modes
78
+ - `--help` for full reference
79
+
80
+ **Infrastructure**
81
+ - Dual ESM + CJS build via tsup
82
+ - Full TypeScript types exported
83
+ - Vitest test suite
84
+ - GitHub Actions CI (Node 18 / 20 / 22)
85
+ - Zero runtime dependencies
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Parth Pabari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,565 @@
1
+ # @ppabari/strio
2
+
3
+ Cryptographically secure string generation for Node.js and browsers.
4
+ Tokens · IDs · Passphrases · Patterns · Expiring tokens — bias-free, zero dependencies, fully typed.
5
+
6
+ [![npm](https://img.shields.io/npm/v/@ppabari/strio)](https://www.npmjs.com/package/@ppabari/strio)
7
+ [![license](https://img.shields.io/npm/l/@ppabari/strio)](./LICENSE)
8
+ [![tests](https://img.shields.io/github/actions/workflow/status/ppabari/strio/ci.yml?label=tests)](https://github.com/ppabari/strio/actions)
9
+
10
+ ---
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @ppabari/strio
16
+ # or
17
+ pnpm add @ppabari/strio
18
+ # or
19
+ yarn add @ppabari/strio
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```ts
25
+ import {
26
+ generateRandomString,
27
+ generateId,
28
+ generateExpiringToken,
29
+ generatePassphrase,
30
+ PRESETS,
31
+ } from '@ppabari/strio';
32
+
33
+ // 32-char API token
34
+ generateRandomString({ length: 32 });
35
+
36
+ // Named preset
37
+ generateRandomString(PRESETS.TOKEN);
38
+
39
+ // Prefixed record ID with checksum
40
+ generateId({ prefix: 'usr' }); // → 'usr_K3xP9mQr2L4Xc'
41
+
42
+ // Magic-link token, expires in 15 min
43
+ const { token, expiresAt } = generateExpiringToken({ ttlSeconds: 900 });
44
+
45
+ // Human-memorable passphrase
46
+ generatePassphrase({ words: 4 }); // → 'stone-river-proud-flame'
47
+ ```
48
+
49
+ ---
50
+
51
+ ## CLI
52
+
53
+ ```bash
54
+ npx @ppabari/strio # 16-char default
55
+ npx @ppabari/strio --length 32
56
+ npx @ppabari/strio --preset TOKEN
57
+ npx @ppabari/strio --preset PASSWORD --count 5
58
+ npx @ppabari/strio --pattern "####-AAAA-####"
59
+ npx @ppabari/strio --charset base58 --length 22
60
+ npx @ppabari/strio --passphrase --words 5 --capitalize
61
+ npx @ppabari/strio --id --prefix usr
62
+ npx @ppabari/strio --expiring --ttl 900
63
+ npx @ppabari/strio --entropy --preset PASSWORD
64
+ npx @ppabari/strio --help
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Core API
70
+
71
+ ### `generateRandomString(options?)`
72
+
73
+ ```ts
74
+ generateRandomString({
75
+ length: 16, // total length (default: 16)
76
+ numeric: true, // include 0–9
77
+ lowercase: true, // include a–z
78
+ uppercase: true, // include A–Z
79
+ symbols: false, // include !@#$%^&*...
80
+ readable: false, // exclude ambiguous: 0 O l I 1
81
+ exclude: 'aeiou', // exclude specific chars (string or array)
82
+ charset: 'base62', // named alias OR raw char string (overrides flags)
83
+ startWith: 'any', // 'any' | 'alphabet' | 'numeric'
84
+ prefix: 'tok_', // prepend fixed string (random portion shrinks)
85
+ suffix: '_v2', // append fixed string
86
+ pattern: '####-AAAA',// pattern mode (see below)
87
+ count: 1, // >1 returns string[]
88
+ seed: 'fixture-42', // deterministic mode (NOT secure — tests only)
89
+ });
90
+ ```
91
+
92
+ Returns `string` when `count` is 1; `string[]` when `count > 1`.
93
+
94
+ ### `generateRandomStringAsync(options?)`
95
+
96
+ Same as above, returns a `Promise`. Non-blocking for server use.
97
+
98
+ ---
99
+
100
+ ## Named Charset Aliases
101
+
102
+ Pass any alias as the `charset` option — no need to copy-paste character sets:
103
+
104
+ ```ts
105
+ generateRandomString({ charset: 'base58', length: 22 });
106
+ generateRandomString({ charset: 'base62', length: 32 });
107
+ generateRandomString({ charset: 'base64url', length: 24 });
108
+ ```
109
+
110
+ | Alias | Characters | Use for |
111
+ |---|---|---|
112
+ | `base16` / `hex` | `0-9a-f` | Hashes, color codes |
113
+ | `base32` | `A-Z 2-7` | OTP secrets, case-insensitive tokens |
114
+ | `base36` | `0-9a-z` | Short URLs, number conversions |
115
+ | `base58` | No `0/O/I/l` | Bitcoin-style, human-safe tokens |
116
+ | `base62` | `0-9A-Za-z` | Max-density alphanumeric |
117
+ | `base64url` | `0-9A-Za-z-_` | JWT components, URL tokens |
118
+ | `crockford32` | No `I/L/O/U` | Serial numbers, redemption codes |
119
+ | `alphanumeric` | Alias for `base62` | |
120
+ | `alpha` | `A-Za-z` | Letters only |
121
+ | `numeric` | `0-9` | Digits only |
122
+
123
+ ---
124
+
125
+ ## Presets
126
+
127
+ ```ts
128
+ import { generateRandomString, PRESETS } from '@ppabari/strio';
129
+
130
+ generateRandomString(PRESETS.TOKEN); // 32-char alphanumeric
131
+ generateRandomString(PRESETS.PASSWORD); // 20-char with symbols
132
+ generateRandomString(PRESETS.READABLE); // 16-char, no ambiguous chars
133
+ generateRandomString(PRESETS.SLUG); // 12-char lowercase slug
134
+ generateRandomString(PRESETS.HEX); // 32-char hex
135
+ generateRandomString(PRESETS.PIN); // 6-digit PIN, no leading zero
136
+ generateRandomString(PRESETS.SHORT_ID); // 8-char uppercase, starts with letter
137
+ generateRandomString(PRESETS.INVITE_CODE); // 'KXPZ-9MR2-LQ4Y-8WVN'
138
+ ```
139
+
140
+ Spread to customise:
141
+
142
+ ```ts
143
+ generateRandomString({ ...PRESETS.TOKEN, length: 64 });
144
+ generateRandomString({ ...PRESETS.TOKEN, count: 10 });
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Pattern Mode
150
+
151
+ ```ts
152
+ // Placeholders: # digit A uppercase a lowercase * any ? alphanumeric
153
+ generateRandomString({ pattern: '####-AAAA-####' }); // '4821-KXPZ-0937'
154
+ generateRandomString({ pattern: '(###) ###-####' }); // '(415) 629-0837'
155
+ generateRandomString({ pattern: 'usr_************' }); // 'usr_k3Xp9mQr2LzY'
156
+ generateRandomString({ pattern: '\\#\\A-literal' }); // '#A-literal'
157
+ ```
158
+
159
+ ---
160
+
161
+ ## Short IDs with Checksum
162
+
163
+ Collision-resistant IDs with a Luhn-style checksum character — detects single-character transcription errors without a database lookup.
164
+
165
+ ```ts
166
+ import { generateId, validateId } from '@ppabari/strio';
167
+
168
+ const id = generateId({ prefix: 'usr' });
169
+ // → 'usr_K3xP9mQr2L4Xc'
170
+
171
+ const id2 = generateId({ prefix: 'inv', randomLength: 8 });
172
+ // → 'inv_K3xP9mQrc'
173
+
174
+ // Validate integrity
175
+ const result = validateId(id, { prefix: 'usr' });
176
+ result.valid; // true
177
+
178
+ // Detect a typo
179
+ const typo = 'usr_K3xP9mQr2L4Xa'; // last char changed
180
+ validateId(typo, { prefix: 'usr' }).valid; // false — checksum mismatch
181
+
182
+ // Options
183
+ generateId({
184
+ prefix: 'org', // prepended with separator
185
+ separator: '-', // default '_'
186
+ randomLength: 12, // random portion length (default 12)
187
+ charset: 'base58', // alias or raw string (default 'base58')
188
+ checksum: true, // append checksum char (default true)
189
+ });
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Expiring Tokens
195
+
196
+ Self-expiring tokens for password resets, magic links, and OTP fallbacks.
197
+ No HMAC — the expiry is encoded directly in the token. Combine with a server-side store if you need revocation.
198
+
199
+ ```ts
200
+ import { generateExpiringToken, verifyToken } from '@ppabari/strio';
201
+
202
+ // Generate
203
+ const { token, expiresAt } = generateExpiringToken({
204
+ ttlSeconds: 900, // 15 minutes (default)
205
+ payloadLength: 24, // random portion length (default 24)
206
+ });
207
+
208
+ // Token format: [8-char base62 expiry][random payload]
209
+ // Total length: 8 + payloadLength
210
+
211
+ // Verify
212
+ const result = verifyToken(token);
213
+ result.valid; // true / false
214
+ result.expired; // boolean
215
+ result.expiresAt; // Date | null
216
+ result.secondsRemaining; // number | null
217
+
218
+ if (!result.valid) {
219
+ if (result.expired) throw new Error('Token expired');
220
+ throw new Error('Invalid token format');
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Stream / Iterator API
227
+
228
+ Memory-efficient bulk generation — yields one string at a time without allocating everything upfront.
229
+
230
+ ```ts
231
+ import { randomStringStream, uniqueRandomStringStream, take, takeWhere } from '@ppabari/strio';
232
+
233
+ // Infinite stream — use take() or break to stop
234
+ for await (const token of randomStringStream({ length: 24 })) {
235
+ await db.insert({ token });
236
+ if (done) break;
237
+ }
238
+
239
+ // Collect N items
240
+ const tokens = await take(randomStringStream({ length: 24 }), 10_000);
241
+
242
+ // Unique-only stream (deduplicates in memory)
243
+ const ids = await take(uniqueRandomStringStream({ length: 8, charset: 'base36' }), 1_000);
244
+
245
+ // Filter stream
246
+ const alphaStart = await takeWhere(
247
+ randomStringStream({ length: 12 }),
248
+ 50,
249
+ t => /^[a-zA-Z]/.test(t)
250
+ );
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Passphrase Generator
256
+
257
+ ```ts
258
+ import { generatePassphrase } from '@ppabari/strio';
259
+
260
+ const { passphrase, wordCount, entropyBits } = generatePassphrase({
261
+ words: 4, // number of words (default 4)
262
+ separator: '-', // word separator (default '-')
263
+ capitalize: false, // capitalize first letter of each word
264
+ appendDigit: false, // append a random digit
265
+ customWords: [...], // custom word list (optional)
266
+ });
267
+
268
+ // Examples
269
+ generatePassphrase();
270
+ // → 'stone-river-proud-flame' (~51 bits)
271
+
272
+ generatePassphrase({ words: 5, separator: ' ', capitalize: true });
273
+ // → 'Stone River Proud Flame Beach' (~63 bits)
274
+
275
+ generatePassphrase({ words: 4, appendDigit: true });
276
+ // → 'stone-river-proud-flame7'
277
+ ```
278
+
279
+ **Entropy by word count** (512-word list):
280
+
281
+ | Words | Entropy |
282
+ |-------|---------|
283
+ | 3 | ~27 bits |
284
+ | 4 | ~36 bits |
285
+ | 5 | ~45 bits |
286
+ | 6 | ~54 bits |
287
+
288
+ ---
289
+
290
+ ## Seeded / Deterministic Mode
291
+
292
+ For tests, snapshots, and demos. Same seed + options = same string every time.
293
+
294
+ ```ts
295
+ generateRandomString({ length: 16, seed: 'test-fixture-42' });
296
+ // Always: 'N4fHqR7kVp2wXmYs'
297
+
298
+ // Great for snapshot tests:
299
+ expect(generateRandomString({ length: 12, seed: 'user-test' }))
300
+ .toMatchInlineSnapshot('"7kVp2wXmYs4f"');
301
+ ```
302
+
303
+ > ⚠️ **Not cryptographically secure.** Uses xoshiro128** PRNG. Never use seeded output as a real token, password, or secret.
304
+
305
+ ---
306
+
307
+ ## Zod Integration
308
+
309
+ ```ts
310
+ import { z } from 'zod';
311
+ import { randomStringSchema } from '@ppabari/strio';
312
+ // or: import { randomStringSchema } from '@ppabari/strio/zod';
313
+
314
+ const tokenSchema = randomStringSchema({
315
+ length: 32,
316
+ charset: 'base62',
317
+ description: 'API token',
318
+ }, z);
319
+
320
+ // In a Zod object schema
321
+ const UserSchema = z.object({
322
+ id: randomStringSchema({ length: 12, charset: 'base58' }, z),
323
+ apiKey: randomStringSchema({ length: 32 }, z),
324
+ pin: randomStringSchema({ length: 6, requireNumeric: true }, z),
325
+ });
326
+
327
+ // Validates correctly
328
+ tokenSchema.parse('k3Xp9mQr2LzYw7NvTq8sHfGbJ5cD4eAK'); // ✓
329
+ tokenSchema.parse('short'); // ZodError: String must contain exactly 32 characters
330
+ ```
331
+
332
+ > Zod is an optional peer dependency — install it separately: `npm install zod`
333
+
334
+ ---
335
+
336
+ ## Entropy Estimation
337
+
338
+ ```ts
339
+ import { estimateEntropy } from '@ppabari/strio';
340
+
341
+ const e = estimateEntropy({ length: 32 });
342
+ e.bits; // 190.54
343
+ e.strength; // 'very-strong'
344
+ e.charsetSize; // 62
345
+ e.combinations; // '2.27e+57'
346
+
347
+ estimateEntropy(PRESETS.PASSWORD).bits; // ~131
348
+ ```
349
+
350
+ | Bits | Strength |
351
+ |------|----------|
352
+ | < 28 | `very-weak` |
353
+ | 28–49 | `weak` |
354
+ | 50–71 | `fair` |
355
+ | 72–99 | `strong` |
356
+ | ≥ 100 | `very-strong` |
357
+
358
+ ---
359
+
360
+ ## String Validation
361
+
362
+ ```ts
363
+ import { validateRandomString } from '@ppabari/strio';
364
+
365
+ const result = validateRandomString('Token123!', {
366
+ minLength: 8,
367
+ requireNumeric: true,
368
+ requireUppercase: true,
369
+ requireSymbols: true,
370
+ charset: '...', // all chars must be in this set
371
+ pattern: '####-AAAA', // must match pattern
372
+ });
373
+
374
+ result.valid; // true / false
375
+ result.errors; // string[] of failure messages
376
+ ```
377
+
378
+ ---
379
+
380
+ ## Secrets & Key Generation
381
+
382
+ Everything you need to generate production-ready secrets for auth, encryption, and framework config.
383
+
384
+ ### Raw Bytes
385
+
386
+ ```ts
387
+ import { generateBytes } from '@ppabari/strio';
388
+
389
+ const key = generateBytes(32); // Uint8Array — 256-bit AES key material
390
+ const iv = generateBytes(12); // AES-GCM IV
391
+ const salt = generateBytes(16); // bcrypt / PBKDF2 salt
392
+
393
+ // Use directly with Web Crypto:
394
+ await crypto.subtle.importKey('raw', generateBytes(32), 'AES-GCM', false, ['encrypt']);
395
+ ```
396
+
397
+ ### Hex Keys
398
+
399
+ ```ts
400
+ import { generateHexKey } from '@ppabari/strio';
401
+
402
+ generateHexKey() // → 64-char hex (256-bit, default)
403
+ generateHexKey(128) // → 32-char hex (AES-128)
404
+ generateHexKey(512) // → 128-char hex (HMAC-SHA512)
405
+
406
+ // Node.js crypto:
407
+ const cipher = createCipheriv('aes-256-gcm', Buffer.from(generateHexKey(256), 'hex'), iv);
408
+ ```
409
+
410
+ ### Base64 Keys
411
+
412
+ ```ts
413
+ import { generateBase64Key } from '@ppabari/strio';
414
+
415
+ generateBase64Key() // url-safe, no padding (default)
416
+ generateBase64Key(256, 'standard') // standard base64 with = padding
417
+ generateBase64Key(256, 'url-safe') // url-safe with = padding
418
+ generateBase64Key(256, 'url-safe-no-pad') // url-safe, no padding
419
+ ```
420
+
421
+ ### JWT Secrets
422
+
423
+ Enforces RFC 7518 minimum key lengths automatically.
424
+
425
+ ```ts
426
+ import { generateJwtSecret } from '@ppabari/strio';
427
+
428
+ // HS256 (default) — 256-bit minimum
429
+ const { secret } = generateJwtSecret();
430
+ jwt.sign(payload, secret, { algorithm: 'HS256' });
431
+
432
+ // HS512 — 512-bit minimum
433
+ const { secret } = generateJwtSecret({ algorithm: 'HS512' });
434
+
435
+ // Hex format (for jose / @auth/core importing as oct key)
436
+ const { secret } = generateJwtSecret({ format: 'hex' });
437
+
438
+ // Returns: { secret, algorithm, bits, format, example }
439
+ ```
440
+
441
+ | Algorithm | Min bits | Output chars (base64url) |
442
+ |-----------|----------|--------------------------|
443
+ | `HS256` | 256 | 43 |
444
+ | `HS384` | 384 | 64 |
445
+ | `HS512` | 512 | 86 |
446
+
447
+ ### Structured API Keys
448
+
449
+ Stripe / GitHub / OpenAI style — `type_environment_randomPortion`.
450
+
451
+ ```ts
452
+ import { generateApiKey } from '@ppabari/strio';
453
+
454
+ generateApiKey({ type: 'myapp', environment: 'live' })
455
+ // → { key: 'myapp_live_K3xP9mQr2LzYw7NvTq8sHfGbJ5cD4eAK3xP9...', prefix: 'sk_live', ... }
456
+
457
+ generateApiKey({ type: 'tok', bits: 128 })
458
+ // → { key: 'tok_K3xP9mQr2LzYw7Nv', ... }
459
+
460
+ generateApiKey({ type: 'pk', environment: 'test', charset: 'hex' })
461
+ // → { key: 'pub_test_a3f7c2b9...', ... }
462
+ ```
463
+
464
+ ### Numeric OTP
465
+
466
+ ```ts
467
+ import { generateOtp } from '@ppabari/strio';
468
+
469
+ const { code } = generateOtp(); // → '483920'
470
+ const { code } = generateOtp({ digits: 8 }); // → '04839201'
471
+ const { code } = generateOtp({ allowLeadingZero: false }); // → '583920' (never '0...')
472
+
473
+ // Returns: { code: string, value: number, digits: number }
474
+ await sms.send(phone, `Your verification code is ${code}`);
475
+ ```
476
+
477
+ ### Secret Masking
478
+
479
+ Safe for logs, UIs, and debug output.
480
+
481
+ ```ts
482
+ import { maskSecret } from '@ppabari/strio';
483
+
484
+ maskSecret('strio_live_K3xP9mQr2LzYw7NvTq8sHfGb')
485
+ // → 'myapp_live_****...fGb' (pass knownPrefixes option for your own prefixes)
486
+
487
+ maskSecret('supersecretpassword')
488
+ // → '****...word'
489
+
490
+ maskSecret('mytoken', { visibleEnd: 6, maskChar: '•' })
491
+ // → '••••...oken'
492
+ ```
493
+
494
+ Pass your own prefixes via `knownPrefixes` option. Defaults include `strio_live_`, `strio_test_`, `tok_`, `api_`, `key_` and similar.
495
+
496
+ ### Constant-Time Comparison
497
+
498
+ **Always use this instead of `===` when comparing tokens server-side.** Prevents timing attacks.
499
+
500
+ ```ts
501
+ import { timingSafeEqual } from '@ppabari/strio';
502
+
503
+ // String comparison
504
+ if (!timingSafeEqual(incomingToken, storedToken)) {
505
+ throw new Error('Invalid token');
506
+ }
507
+
508
+ // Uint8Array comparison (e.g. HMAC digests)
509
+ const valid = timingSafeEqual(computedHmac, expectedHmac);
510
+ ```
511
+
512
+ ### Framework Convenience Helpers
513
+
514
+ ```ts
515
+ import {
516
+ generateCookieSecret,
517
+ generateAesKey,
518
+ generateNextAuthSecret,
519
+ generateDjangoSecretKey,
520
+ generateRailsSecretKeyBase,
521
+ } from '@ppabari/strio';
522
+
523
+ // express-session / iron-session / lucia
524
+ app.use(session({ secret: generateCookieSecret() }));
525
+
526
+ // AES-GCM / AES-CBC key
527
+ const key = generateAesKey(); // → 64-char hex (256-bit)
528
+ const key = generateAesKey(128); // → 32-char hex (128-bit)
529
+ const key = generateAesKey(256, 'bytes'); // → Uint8Array
530
+
531
+ // Next.js / NextAuth / Auth.js — paste into .env
532
+ console.log(`AUTH_SECRET="${generateNextAuthSecret()}"`);
533
+
534
+ // Django — paste into settings.py
535
+ console.log(`SECRET_KEY="${generateDjangoSecretKey()}"`);
536
+
537
+ // Rails — paste into credentials.yml
538
+ console.log(`secret_key_base: ${generateRailsSecretKeyBase()}`);
539
+ ```
540
+
541
+ > All secret generation functions use `crypto.getRandomValues()` — the same cryptographically secure engine as the rest of strio.
542
+
543
+ ---
544
+
545
+ ## Security
546
+
547
+ **Why not `Math.random()`?** It's a PRNG — deterministic, predictable if seeded. Never use for tokens or secrets.
548
+
549
+ **Crypto source:** `crypto.getRandomValues()` (Web Crypto API) — draws from the OS CSPRNG (`/dev/urandom` on Linux, `CryptGenRandom` on Windows). Native in Node.js ≥18, all modern browsers, Deno, and Cloudflare Workers.
550
+
551
+ **Bias-free:** Naive `byte % charsetSize` introduces modulo bias when 256 isn't evenly divisible by the charset size. strio uses **rejection sampling** — bytes in the biased range are discarded and redrawn. Expected overhead: < 1 extra byte per character for any charset ≤128 chars.
552
+
553
+ ---
554
+
555
+ ## Requirements
556
+
557
+ - Node.js ≥ 18
558
+ - Modern browsers (Chrome 37+, Firefox 34+, Safari 11+)
559
+ - Deno, Bun, Cloudflare Workers, and other Web Crypto runtimes
560
+
561
+ ---
562
+
563
+ ## License
564
+
565
+ MIT © Parth Pabari