@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
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
|
+
[](https://www.npmjs.com/package/@ppabari/strio)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](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
|