first commit
This commit is contained in:
163
docs/security-stack/security/password-generator/README.md
Normal file
163
docs/security-stack/security/password-generator/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# @suaveplan/password-generator - Cryptographically-strong password and passphrase generator — char-class, pronounceable, EFF Diceware, PIN, entropy estimator. Zero browser/Node deps.
|
||||
|
||||
## Summary
|
||||
|
||||
Cryptographically-strong password and passphrase generator — char-class,
|
||||
pronounceable, EFF Diceware, and PIN modes with bias-free RNG, real entropy
|
||||
estimates, and injectable `Rng` for deterministic replay. Universal
|
||||
(zero browser / Node / React deps).
|
||||
|
||||
The package owns the surface end-to-end: bundled EFF Long (7776) and EFF
|
||||
Short 1 (1296) wordlists, Lemire-style rejection-sampling `randomInt` for
|
||||
unbiased integer draws, Fisher-Yates shuffle, char-class quotas, configurable
|
||||
passphrase decoration, an entropy estimator, and a `assertStrength` guard.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [charsets](./src/charsets/charsets.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [charsets](./src/charsets/charsets.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [errors](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [codes](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - pronounceable — `generatePronounceablePassword(opts…
|
||||
- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - pronounceable — `generatePronounceablePassword(opts…
|
||||
- [pronounceable](./src/password/pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [default-rng](./src/random/default-rng.md) — `defaultRng()` builds the default `Rng` backed by `@suaveplan/crypto-utils.randomBytes`, which delegates to `crypto.get…
|
||||
- [fisher-yates](./src/random/fisher-yates.md) — `shuffleInPlace(array, rng)` performs an in-place Fisher–Yates / Knuth shuffle parameterised over the bias-free `random…
|
||||
- [random-int](./src/random/random-int.md) — `randomInt(rng, max)` returns a uniformly-distributed integer in the half-open interval `[0, max)` for any `max ∈ [1, 2…
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [eff-long](./src/wordlists/eff-long.md) — Bundles the EFF Long Wordlist as a frozen `readonly string[]` so callers who only want passphrase generation pull this…
|
||||
- [eff-short](./src/wordlists/eff-short.md) — Bundles EFF's Short Wordlist #1 — 1296 entries (4-dice / 6⁴), ≈ 10.34 bits/word, every entry ≤ 5 ASCII characters. Sour…
|
||||
- [registry](./src/wordlists/registry.md) — The wordlist registry exposes a small plugin API — `registerWordlist`, `getWordlist`, `listWordlists` — that lets calle…
|
||||
<!-- end:auto-docs --> - [charsets](./src/charsets/charsets.md) — <!-- begin:auto-docs --> _(no submodules)_- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_- pronounceable — `generatePronounceablePassword(opts…
|
||||
- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_- pronounceable — `generatePronounceablePassword(opts…
|
||||
- [pronounceable](./src/password/pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-rng](./src/random/default-rng.md) — `defaultRng()` builds the default `Rng` backed by `@suaveplan/crypto-utils.randomBytes`, which delegates to `crypto.get…
|
||||
- [fisher-yates](./src/random/fisher-yates.md) — `shuffleInPlace(array, rng)` performs an in-place Fisher–Yates / Knuth shuffle parameterised over the bias-free `random…
|
||||
- [random-int](./src/random/random-int.md) — `randomInt(rng, max)` returns a uniformly-distributed integer in the half-open interval `[0, max)` for any `max ∈ [1, 2…
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [eff-long](./src/wordlists/eff-long.md) — Bundles the EFF Long Wordlist as a frozen `readonly string[]` so callers who only want passphrase generation pull this…
|
||||
- [eff-short](./src/wordlists/eff-short.md) — Bundles EFF's Short Wordlist #1 — 1296 entries (4-dice / 6⁴), ≈ 10.34 bits/word, every entry ≤ 5 ASCII characters. Sour…
|
||||
- [registry](./src/wordlists/registry.md) — The wordlist registry exposes a small plugin API — `registerWordlist`, `getWordlist`, `listWordlists` — that lets calle…
|
||||
- [charsets](./src/charsets/charsets.md) — <!-- begin:auto-docs --> _(no submodules)_- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_- pronounceable — `generatePronounceablePassword(opts…
|
||||
- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_- pronounceable — `generatePronounceablePassword(opts…
|
||||
- [pronounceable](./src/password/pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-rng](./src/random/default-rng.md) — `defaultRng()` builds the default `Rng` backed by `@suaveplan/crypto-utils.randomBytes`, which delegates to `crypto.get…
|
||||
- [fisher-yates](./src/random/fisher-yates.md) — `shuffleInPlace(array, rng)` performs an in-place Fisher–Yates / Knuth shuffle parameterised over the bias-free `random…
|
||||
- [random-int](./src/random/random-int.md) — `randomInt(rng, max)` returns a uniformly-distributed integer in the half-open interval `[0, max)` for any `max ∈ [1, 2…
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [eff-long](./src/wordlists/eff-long.md) — Bundles the EFF Long Wordlist as a frozen `readonly string[]` so callers who only want passphrase generation pull this…
|
||||
- [eff-short](./src/wordlists/eff-short.md) — Bundles EFF's Short Wordlist #1 — 1296 entries (4-dice / 6⁴), ≈ 10.34 bits/word, every entry ≤ 5 ASCII characters. Sour…
|
||||
- [registry](./src/wordlists/registry.md) — The wordlist registry exposes a small plugin API — `registerWordlist`, `getWordlist`, `listWordlists` — that lets calle…
|
||||
- [charsets](./src/charsets/charsets.md) — <!-- begin:auto-docs --> _(no submodules)_- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_- pronounceable — `generatePronounceablePassword(opts…
|
||||
- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_- pronounceable — `generatePronounceablePassword(opts…
|
||||
- [pronounceable](./src/password/pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-rng](./src/random/default-rng.md) — `defaultRng()` builds the default `Rng` backed by `@suaveplan/crypto-utils.randomBytes`, which delegates to `crypto.get…
|
||||
- [fisher-yates](./src/random/fisher-yates.md) — `shuffleInPlace(array, rng)` performs an in-place Fisher–Yates / Knuth shuffle parameterised over the bias-free `random…
|
||||
- [random-int](./src/random/random-int.md) — `randomInt(rng, max)` returns a uniformly-distributed integer in the half-open interval `[0, max)` for any `max ∈ [1, 2…
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [eff-long](./src/wordlists/eff-long.md) — Bundles the EFF Long Wordlist as a frozen `readonly string[]` so callers who only want passphrase generation pull this…
|
||||
- [eff-short](./src/wordlists/eff-short.md) — Bundles EFF's Short Wordlist #1 — 1296 entries (4-dice / 6⁴), ≈ 10.34 bits/word, every entry ≤ 5 ASCII characters. Sour…
|
||||
- [registry](./src/wordlists/registry.md) — The wordlist registry exposes a small plugin API — `registerWordlist`, `getWordlist`, `listWordlists` — that lets calle…
|
||||
- [charsets](./src/charsets/charsets.md) — <!-- begin:auto-docs --> _(no submodules)_- [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_ - [entropy](./src/entropy/entropy.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [codes](./src/errors/codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_ - [passphrase](./src/passphrase/passphrase.md) — <!-- begin:auto-docs --> _(no submodules)_- [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_ - [password](./src/password/password.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pronounceable](./src/password/pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
- [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pin](./src/pin/pin.md) — <!-- begin:auto-docs --> _(no submodules)_- [default-rng](./src/random/default-rng.md) — `defaultRng()` builds the default `Rng` backed by `@suaveplan/crypto-utils.randomBytes`, which delegates to `crypto.get…
|
||||
- [fisher-yates](./src/random/fisher-yates.md) — `shuffleInPlace(array, rng)` performs an in-place Fisher–Yates / Knuth shuffle parameterised over the bias-free `random…
|
||||
- [random-int](./src/random/random-int.md) — `randomInt(rng, max)` returns a uniformly-distributed integer in the half-open interval `[0, max)` for any `max ∈ [1, 2…
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [eff-long](./src/wordlists/eff-long.md) — Bundles the EFF Long Wordlist as a frozen `readonly string[]` so callers who only want passphrase generation pull this…
|
||||
- [eff-short](./src/wordlists/eff-short.md) — Bundles EFF's Short Wordlist #1 — 1296 entries (4-dice / 6⁴), ≈ 10.34 bits/word, every entry ≤ 5 ASCII characters. Sour…
|
||||
- [registry](./src/wordlists/registry.md) — The wordlist registry exposes a small plugin API — `registerWordlist`, `getWordlist`, `listWordlists` — that lets calle…
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/password-generator
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```ts
|
||||
import {
|
||||
assertStrength,
|
||||
estimateEntropy,
|
||||
generatePassphrase,
|
||||
generatePassword,
|
||||
generatePin,
|
||||
} from "@suaveplan/password-generator";
|
||||
|
||||
// Char-class password
|
||||
const pw = generatePassword({
|
||||
length: 16,
|
||||
requireLowercase: true,
|
||||
requireUppercase: true,
|
||||
requireDigits: true,
|
||||
requireSymbols: true,
|
||||
});
|
||||
|
||||
// 6-word EFF Long passphrase (≈ 77.5 bits)
|
||||
const phrase = generatePassphrase({});
|
||||
|
||||
// 6-digit PIN
|
||||
const pin = generatePin(6);
|
||||
|
||||
// Strength check
|
||||
assertStrength(pw, 80); // throws PWGEN.STRENGTH_INSUFFICIENT if < 80 bits
|
||||
const { bits, charsetSize } = estimateEntropy(pw);
|
||||
```
|
||||
|
||||
## Tree-Shaking
|
||||
|
||||
Each major surface ships at its own subpath so consumers pull only what they
|
||||
need:
|
||||
|
||||
```ts
|
||||
import { generatePassword } from "@suaveplan/password-generator/password";
|
||||
import { generatePassphrase } from "@suaveplan/password-generator/passphrase";
|
||||
import { generatePin } from "@suaveplan/password-generator/pin";
|
||||
import { EFF_LONG_WORDLIST } from "@suaveplan/password-generator/wordlists/eff-long";
|
||||
import { EFF_SHORT_WORDLIST } from "@suaveplan/password-generator/wordlists/eff-short";
|
||||
```
|
||||
|
||||
A consumer that imports only `./password` does not pull the 85 KB EFF Long
|
||||
list. A consumer that imports only `./pin` pulls neither wordlist.
|
||||
|
||||
## Modules
|
||||
|
||||
- [`random/random-int`](./src/random/random-int.md) — bias-free `randomInt`
|
||||
via Lemire rejection sampling.
|
||||
- [`random/fisher-yates`](./src/random/fisher-yates.md) — Fisher-Yates shuffle
|
||||
built on the unbiased picker.
|
||||
- [`random/default-rng`](./src/random/default-rng.md) — default `Rng` wrapping
|
||||
`@suaveplan/crypto-utils.randomBytes`.
|
||||
- [`charsets/charsets`](./src/charsets/charsets.md) — `LOWERCASE` / `UPPERCASE`
|
||||
/ `DIGITS` / `SYMBOLS` / `AMBIGUOUS` / `LOOKALIKES` + `buildEffectiveCharset`.
|
||||
- [`password/password`](./src/password/password.md) — char-class generator with
|
||||
required-class quotas.
|
||||
- [`password/pronounceable`](./src/password/pronounceable.md) — CVC-pattern
|
||||
pronounceable generator.
|
||||
- [`passphrase/passphrase`](./src/passphrase/passphrase.md) — EFF Diceware
|
||||
passphrase generator.
|
||||
- [`pin/pin`](./src/pin/pin.md) — uniform digit-only PIN generator.
|
||||
- [`entropy/entropy`](./src/entropy/entropy.md) — entropy estimator and
|
||||
`assertStrength` guard.
|
||||
- [`wordlists/eff-long`](./src/wordlists/eff-long.md) — bundled EFF Long
|
||||
Wordlist (7776 entries).
|
||||
- [`wordlists/eff-short`](./src/wordlists/eff-short.md) — bundled EFF Short
|
||||
Wordlist 1 (1296 entries).
|
||||
- [`wordlists/registry`](./src/wordlists/registry.md) — plugin API for caller-
|
||||
supplied wordlists.
|
||||
- [`errors/codes`](./src/errors/codes.md) — `PWGEN_CODES` registry and
|
||||
`PwgenError` class.
|
||||
- [`schemas/schemas`](./src/schemas/schemas.md) — Zod boundary schemas.
|
||||
- [`observability/metrics`](./src/observability/metrics.md) — OpenTelemetry
|
||||
histograms and counters.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# `charsets`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the canonical character-class constants used by every generator in the package — `LOWERCASE`, `UPPERCASE`, `DIGITS`, `SYMBOLS`, plus the curated `AMBIGUOUS` and `LOOKALIKES` sets that callers subtract to avoid characters confusable in fixed-width fonts. Exposing each class as a frozen `ReadonlySet<string>` lets callers compose custom unions (`new Set([...LOWERCASE, ...DIGITS])`) without depending on any private representation.
|
||||
|
||||
The `buildEffectiveCharset(opts)` helper merges the enabled classes and subtracts `opts.exclude` to produce the deduplicated string array a generator can index into via `randomInt`. The four built-in classes are pairwise disjoint by construction, so insertion-order union is enough to deduplicate — adding a `Set`-tracked check would be dead code.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `ReadonlySet<string>` for each class — safe to share across modules.
|
||||
- `AMBIGUOUS` (`0Ol1I`) and `LOOKALIKES` curated by frequency of human misreads.
|
||||
- `SYMBOLS` drawn from the OWASP-recommended printable range; excludes shell-quoting hazards (backtick, backslash, whitespace).
|
||||
- `buildEffectiveCharset(opts)` accepts string / array / `Set` exclusions.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AMBIGUOUS,
|
||||
buildEffectiveCharset,
|
||||
} from "@suaveplan/password-generator";
|
||||
|
||||
const charset = buildEffectiveCharset({ exclude: AMBIGUOUS });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DIGITS,
|
||||
LOWERCASE,
|
||||
UPPERCASE,
|
||||
buildEffectiveCharset,
|
||||
} from "@suaveplan/password-generator";
|
||||
|
||||
// Hex charset only (digits + a-f) via two passes
|
||||
const hexLower = [
|
||||
...buildEffectiveCharset({ uppercase: false, symbols: false }),
|
||||
].filter((c) => DIGITS.has(c) || "abcdef".includes(c));
|
||||
|
||||
// "Vault-friendly" passwords: drop ambiguous + shell-hostile chars
|
||||
const safe = buildEffectiveCharset({ exclude: "0Ol1I`\\\"' " });
|
||||
const charset = safe.filter((c) => !UPPERCASE.has(c) || LOWERCASE.has(c.toLowerCase()));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constants
|
||||
|
||||
- `LOWERCASE: ReadonlySet<string>` — `a–z` (26 entries).
|
||||
- `UPPERCASE: ReadonlySet<string>` — `A–Z` (26 entries).
|
||||
- `DIGITS: ReadonlySet<string>` — `0–9` (10 entries).
|
||||
- `SYMBOLS: ReadonlySet<string>` — `!@#$%^&*()-_=+[]{};:,.<>/?` (25 entries).
|
||||
- `AMBIGUOUS: ReadonlySet<string>` — `0Ol1I`.
|
||||
- `LOOKALIKES: ReadonlySet<string>` — `0OQ1lI5S2Z8B`.
|
||||
|
||||
### `buildEffectiveCharset(opts?: CharsetOptions): string[]`
|
||||
|
||||
Builds the deduplicated union of enabled classes minus `opts.exclude`. Returns a fresh array on every call; insertion order is lowercase → uppercase → digits → symbols.
|
||||
|
||||
- `lowercase` / `uppercase` / `digits` / `symbols` — each defaults to `true`.
|
||||
- `exclude` — `string` (per-character), `readonly string[]`, or `ReadonlySet<string>`; defaults to none.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Every set is frozen at module load via `Object.freeze`, which only freezes the `Set` object itself (not the underlying internal storage). The intent is to surface accidental `.add` / `.delete` calls at call sites; consumers who need to add or remove characters must clone the set first.
|
||||
|
||||
`SYMBOLS` intentionally omits whitespace, the backslash, double-quote, and backtick. Those characters routinely break shell interpolations and CSV exports; including them would force every caller to filter them out before interpolating the generated password into a deployment script. Callers who want them can compose `new Set([...SYMBOLS, "\\\""])` themselves — the omission is conservative-by-default but trivially reversible.
|
||||
@@ -0,0 +1,73 @@
|
||||
# `entropy`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`estimateEntropy(password, opts?)` returns the upper-bound entropy of a candidate password as `log2(charsetSize) * password.length`, where `charsetSize` is the cardinality of the union of every detected built-in class (lowercase / uppercase / digits / symbols) plus any caller-supplied `opts.extraClasses` whose characters appear in the password. `estimatePassphraseEntropy(words, wordlistSize)` returns `log2(wordlistSize) * words.length` for Diceware-style passphrases. `assertStrength(password, minBits, opts?)` consumes `estimateEntropy` and throws `PWGEN.STRENGTH_INSUFFICIENT` when the computed bits are below the floor.
|
||||
|
||||
The entropy formulas assume uniform sampling and ignore patterns — `"AAAAAAAA"` is reported as the same entropy as `"hG83mP9k"` if both contain the same charset classes. Pattern-aware scoring (zxcvbn-style) is intentionally out of scope and lives in the separate `@suaveplan/password-strength` package.
|
||||
|
||||
## Features
|
||||
|
||||
- Detects class membership in a single pass over the password.
|
||||
- `extraClasses` opt-in lets callers report custom-charset passwords (emoji unions, hex-only PINs).
|
||||
- `assertStrength` throws on insufficient entropy with a structured error code.
|
||||
- Zod-validated `EntropyOptions`; mismatch → `PWGEN.INVALID_OPTIONS`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { estimateEntropy } from "@suaveplan/password-generator";
|
||||
|
||||
const { bits, charsetSize } = estimateEntropy("Abc123Xy");
|
||||
// charsetSize === 62, bits === log2(62) * 8 ≈ 47.6
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
assertStrength,
|
||||
estimateEntropy,
|
||||
estimatePassphraseEntropy,
|
||||
} from "@suaveplan/password-generator";
|
||||
|
||||
// Server-side guard before persisting a vault item
|
||||
function persistVaultItem(pw: string): void {
|
||||
assertStrength(pw, 80); // throws PWGEN.STRENGTH_INSUFFICIENT if < 80 bits
|
||||
// ... persist
|
||||
}
|
||||
|
||||
// Custom emoji-augmented class
|
||||
const e = estimateEntropy("emoji👍2025", { extraClasses: ["👍🎉🚀"] });
|
||||
|
||||
// Diceware reporting
|
||||
const words = ["correct", "horse", "battery", "staple", "alpha", "beta"];
|
||||
const passEntropy = estimatePassphraseEntropy(words, 7776); // ≈ 77.55 bits
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `estimateEntropy(password: string, opts?: EntropyOptions): EntropyEstimate`
|
||||
|
||||
Returns `{ bits, charsetSize }`. `bits` is `0` when the password is empty.
|
||||
|
||||
### `estimatePassphraseEntropy(words: readonly string[], wordlistSize: number): number`
|
||||
|
||||
Returns `log2(wordlistSize) * words.length`. Throws `PWGEN.INVALID_OPTIONS` for non-positive integer `wordlistSize`.
|
||||
|
||||
### `assertStrength(password: string, minBits: number, opts?: EntropyOptions): void`
|
||||
|
||||
Returns `void` on success. Throws `PwgenError(PWGEN.STRENGTH_INSUFFICIENT)` when the computed bits are strictly less than `minBits`.
|
||||
|
||||
### `EntropyOptions`
|
||||
|
||||
- `extraClasses?: readonly string[]` — additional character classes the password may draw from.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Class detection is a single forward pass over the password, tracking four boolean flags for the built-in classes plus a `Set<number>` of seen `extraClasses` indices. The `charsetSize` accumulator only adds a class's cardinality when the first matching character is seen, which keeps the cost `O(password.length * extraClasses.length)` worst-case.
|
||||
|
||||
The formula deliberately reports an _upper bound_. A consumer comparing this number to a cracking-time chart should treat it as a ceiling: real-world passwords contain dictionary patterns, keyboard walks, and date stamps that reduce the effective entropy substantially. Use `@suaveplan/password-strength` for adversarial-aware estimates. `assertStrength` is a deliberately blunt instrument — it answers "does this password meet the floor on the most generous estimator?" — and is intended as a server-side guardrail against drastically weak inputs, not a substitute for proper strength scoring.
|
||||
@@ -0,0 +1,81 @@
|
||||
# `errors/codes`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Defines the stable error-code registry for `@suaveplan/password-generator` via `defineCodes("PWGEN", [...])` from `@suaveplan/error`. Every code names exactly one failure class so callers can branch on `error.code` rather than parsing message strings. The companion `PwgenError` class is the single concrete subtype thrown from this package, and `pwgenError(code, message, options)` is a convenience constructor that prefixes the message with the stable code for log scrapers.
|
||||
|
||||
The registry is intentionally minimal — six codes, no overlap, every code thrown from at least one production call site (verified by `codes.test.ts`). Adding a code without a thrown call site is banned per `AGENTS.md` §2c.
|
||||
|
||||
## Features
|
||||
|
||||
- Six stable codes: `INVALID_OPTIONS`, `OPTIONS_INFEASIBLE`, `WORDLIST_TOO_SHORT`, `WORDLIST_NOT_REGISTERED`, `STRENGTH_INSUFFICIENT`, `RNG_FAILED`.
|
||||
- `PwgenError` extends `SuaveplanError<PwgenErrorCode>` for instanceof-friendly catching.
|
||||
- `pwgenError(code, message, { cause })` helper that auto-prefixes the message.
|
||||
- Frozen registry — direct property assignment throws in strict mode.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PwgenError, pwgenError } from "@suaveplan/password-generator/errors";
|
||||
|
||||
try {
|
||||
// ...
|
||||
} catch (e) {
|
||||
if (e instanceof PwgenError && e.code === "PWGEN.INVALID_OPTIONS") {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
PWGEN_CODES,
|
||||
PwgenError,
|
||||
pwgenError,
|
||||
} from "@suaveplan/password-generator/errors";
|
||||
|
||||
// Catch and re-throw with structural context preserved
|
||||
try {
|
||||
throw pwgenError("PWGEN.RNG_FAILED", "downstream RNG bug", {
|
||||
cause: new Error("entropy source returned non-uint32"),
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof PwgenError) {
|
||||
console.log(e.code, e.cause);
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerate every code for an error-mapping table in i18n bundles
|
||||
for (const code of Object.values(PWGEN_CODES)) {
|
||||
console.log(code);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PWGEN_CODES`
|
||||
|
||||
Frozen record mapping each reason → fully-qualified code string. For example `PWGEN_CODES.RNG_FAILED === "PWGEN.RNG_FAILED"`.
|
||||
|
||||
### `PwgenErrorCode`
|
||||
|
||||
Literal-typed union of every registered code.
|
||||
|
||||
### `class PwgenError extends SuaveplanError<PwgenErrorCode>`
|
||||
|
||||
Construct with `(code, message, options?)`. Options may carry `cause: Error`.
|
||||
|
||||
### `pwgenError(code, message, options?): PwgenError`
|
||||
|
||||
Convenience constructor that prefixes the message with the code string. Non-`Error` `cause` values are silently dropped (per `SuaveplanError` contract).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry shape is dictated by `defineCodes` from `@suaveplan/error` — the return type is `{ readonly [K in Reasons[number]]:`PWGEN.${K}`}`, so TypeScript narrows `PWGEN_CODES.INVALID_OPTIONS` to the literal string `"PWGEN.INVALID_OPTIONS"`. This is what makes `if (e.code === "PWGEN.INVALID_OPTIONS")` typecheck without a `string` widening.
|
||||
|
||||
Three codes (`OPTIONS_INFEASIBLE`, `WORDLIST_TOO_SHORT`, `STRENGTH_INSUFFICIENT`) are sufficient to express every failure callers care about at first glance; the remaining three (`INVALID_OPTIONS`, `WORDLIST_NOT_REGISTERED`, `RNG_FAILED`) cover bug-shaped errors that should surface during development and never reach production. Catching `PwgenError` and branching on `code` lets call sites distinguish "user typed an invalid option" (recoverable, surface a form-validation message) from "RNG is broken" (panic, log, escalate).
|
||||
@@ -0,0 +1,75 @@
|
||||
# `observability/metrics`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Wires the package's OpenTelemetry instruments — `pwgen.generate_duration_ms{mode}` and `pwgen.entropy_bits{mode}` histograms, plus a `pwgen.errors_total{code}` counter — and exposes thin `recordGeneration` / `recordError` helpers every production call site uses. Instruments are obtained from `metrics.getMeter("@suaveplan/password-generator")` and lazy-initialised on first use so the active `MeterProvider` is always honoured (a global provider installed at startup overrides the package's no-op default).
|
||||
|
||||
The `mode` attribute is bounded to four documented values — `"random"`, `"pronounceable"`, `"passphrase"`, `"pin"` — so cardinality on every dashboard stays flat regardless of call-site volume. The `code` attribute on the counter mirrors the stable `PWGEN.*` registry, so a single counter view shows the full failure-mode mix.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy-init pattern with `_resetInstruments()` test hook.
|
||||
- Three instruments: two histograms (`generate_duration_ms`, `entropy_bits`) + one counter (`errors_total`).
|
||||
- Low-cardinality `mode` and `code` attributes.
|
||||
- `recordGeneration(mode, durationMs, bits)` rolls duration + entropy into one call.
|
||||
- `recordError(code)` accepts the raw `PWGEN.*` code string.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { recordError, recordGeneration } from "@suaveplan/password-generator";
|
||||
|
||||
const start = performance.now();
|
||||
const pw = "abc...";
|
||||
recordGeneration("random", performance.now() - start, 95.2);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
import { _resetInstruments } from "@suaveplan/password-generator";
|
||||
|
||||
// Install an in-memory provider for tests, then exercise the package.
|
||||
const calls: { name: string; value: number; attrs: Record<string, string> }[] = [];
|
||||
metrics.setGlobalMeterProvider({
|
||||
getMeter: () => ({
|
||||
createCounter: (name: string) => ({
|
||||
add: (v: number, attrs: Record<string, string> = {}) =>
|
||||
calls.push({ name, value: v, attrs }),
|
||||
}),
|
||||
createHistogram: (name: string) => ({
|
||||
record: (v: number, attrs: Record<string, string> = {}) =>
|
||||
calls.push({ name, value: v, attrs }),
|
||||
}),
|
||||
}),
|
||||
} as never);
|
||||
_resetInstruments(); // Force lazy-init to use the fake provider next call
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `recordGeneration(mode: GenerateMode, durationMs: number, bits: number): void`
|
||||
|
||||
Records the per-call duration on `pwgen.generate_duration_ms` and the entropy on `pwgen.entropy_bits`, both tagged with `mode`.
|
||||
|
||||
### `recordError(code: string): void`
|
||||
|
||||
Increments `pwgen.errors_total` by 1 with `{ code }`.
|
||||
|
||||
### `_resetInstruments(): void`
|
||||
|
||||
Test-only hook that clears the lazy instrument cache. Call before installing a fake provider in tests.
|
||||
|
||||
### `generateDurationMs` / `entropyBits` / `errorsTotal`
|
||||
|
||||
Public facades on the underlying instruments — call `.record(value, { mode })` or `.add(value, { code })` for direct emission.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Lazy initialisation is critical: if instruments were created at module load, they would bind to whatever `MeterProvider` was global at that moment (almost always the no-op default), and a test installing a real provider afterwards would see no data. The `??`-style lazy pattern means every call to `getInstruments` walks the active provider's `getMeter` lookup until the first non-no-op call is made; from that point the cached references are reused for the lifetime of the process (or until `_resetInstruments()` clears them).
|
||||
|
||||
The four-value `mode` enum keeps per-mode cardinality bounded; adding a fifth mode would require a deliberate edit here and a refresh of dashboards. Resist the temptation to thread `password.length` or per-character `class` attributes into the histogram — those would explode cardinality and break the cost model OpenTelemetry backends assume.
|
||||
@@ -0,0 +1,70 @@
|
||||
# `passphrase`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`generatePassphrase(opts, rng?)` produces an EFF-style Diceware passphrase by drawing `length` words uniformly (via the bias-free `randomInt`) from the wordlist resolved via `opts.wordlist`. The default — 6 words from the EFF Long Wordlist joined with `-` — gives ≈ 77.55 bits of entropy, comfortably above the 75-bit floor recommended for symmetric-key-equivalent secrets. Diceware-style passphrases are dramatically more memorable than random char-class passwords at equivalent entropy, which is why the vault item editor in `@suaveplan/vault` defaults to this mode for the "Generate password" surface.
|
||||
|
||||
The function honours configurable separator, three capitalisation modes (`none` / `firstChar` / `randomWord`), and optional `addDigits` / `addSymbols` injections at random positions in the joined output. The decoration knobs exist so the same generator can produce strings that satisfy downstream "must contain a digit" composition rules without abandoning the Diceware backbone.
|
||||
|
||||
## Features
|
||||
|
||||
- Bundled EFF Long (7776) and EFF Short 1 (1296) wordlists via tree-shakeable subpath imports.
|
||||
- Caller-supplied `string[]` wordlists allowed past the 256-entry min-size guard.
|
||||
- Configurable separator (`default "-"`).
|
||||
- `capitalize: "none" | "firstChar" | "randomWord"`.
|
||||
- `addDigits` / `addSymbols` injection at randomised positions.
|
||||
- Deterministic under seeded `Rng`.
|
||||
- Zod-validated; mismatch → `PWGEN.INVALID_OPTIONS`.
|
||||
- Emits `pwgen.generate_duration_ms{mode=passphrase}` and `pwgen.entropy_bits{mode=passphrase}` histograms.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generatePassphrase } from "@suaveplan/password-generator";
|
||||
|
||||
const pp = generatePassphrase({}); // 6 words, EFF Long, joined with "-"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { generatePassphrase } from "@suaveplan/password-generator";
|
||||
|
||||
// 5 short words capitalised at random, two trailing digits, one symbol
|
||||
const pp = generatePassphrase({
|
||||
length: 5,
|
||||
wordlist: "short",
|
||||
capitalize: "randomWord",
|
||||
addDigits: 2,
|
||||
addSymbols: 1,
|
||||
separator: " ",
|
||||
});
|
||||
|
||||
// Caller-supplied themed wordlist (must be ≥ 256 entries)
|
||||
const dnd = [/* ... 300 fantasy nouns ... */];
|
||||
const themed = generatePassphrase({ wordlist: dnd, length: 4 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generatePassphrase(opts?: GeneratePassphraseOptions, rng?: Rng): string`
|
||||
|
||||
- `opts.length` (default `6`): number of words to draw.
|
||||
- `opts.wordlist` (default `"long"`): `"long"` / `"short"` / registry name / inline `string[]`.
|
||||
- `opts.separator` (default `"-"`): string inserted between words before decoration.
|
||||
- `opts.capitalize` (default `"none"`): `"none"` / `"firstChar"` / `"randomWord"`.
|
||||
- `opts.addDigits` / `opts.addSymbols` (default `0`): count of characters injected at randomised positions.
|
||||
- `rng`: defaults to `defaultRng()`.
|
||||
|
||||
Throws `PwgenError(PWGEN.INVALID_OPTIONS)` on Zod mismatch, `PwgenError(PWGEN.WORDLIST_TOO_SHORT)` for inline arrays below 256 entries, and `PwgenError(PWGEN.WORDLIST_NOT_REGISTERED)` for unknown registry names.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Wordlist resolution lazily routes through `getWordlist(name)` so the registry's min-size guard applies uniformly — there's no special-case for the bundled EFF lists. Inline `string[]` wordlists bypass the registry but go through the same 256-entry floor inline, so a caller passing a tiny array sees the same `PWGEN.WORDLIST_TOO_SHORT` they would see from `registerWordlist`.
|
||||
|
||||
Decoration is applied _after_ joining: `addDigits` / `addSymbols` inserts characters at uniformly-random positions in the already-joined string. This means a digit can land mid-word (e.g. `hor5se-rifle`) — that's intentional because inserting only at word boundaries would create a small-cardinality position distribution that's easier to fingerprint. Per-class injection uses bias-free `randomInt` against the `DIGITS` / `SYMBOLS` sets defined in the `charsets` module.
|
||||
|
||||
Entropy reporting uses `estimatePassphraseEntropy(words, wordlistSize)` against the _picked_ words (not the post-decoration string), so the reported figure reflects the Diceware contribution. Decoration adds a small amount of entropy on top of that, but the package does not currently roll that into the reported figure to keep the formula stable across runs.
|
||||
@@ -0,0 +1,97 @@
|
||||
# `password`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs --> - pronounceable — `generatePronounceablePassword(opts…
|
||||
- [pronounceable](./pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
- pronounceable — `generatePronounceablePassword(opts…
|
||||
|
||||
- [pronounceable](./pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
|
||||
- pronounceable — `generatePronounceablePassword(opts…
|
||||
|
||||
- [pronounceable](./pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
|
||||
- pronounceable — `generatePronounceablePassword(opts…
|
||||
|
||||
- [pronounceable](./pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
- [pronounceable](./pronounceable.md) — `generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for should…
|
||||
|
||||
## Purpose
|
||||
|
||||
`generatePassword(opts, rng?)` produces a `length`-character password drawn uniformly from the union of enabled character classes minus any caller-supplied exclusions. Required-class quotas (`requireLowercase`, `requireUppercase`, `requireDigits`, `requireSymbols`) are satisfied without leaking position information: one randomly-chosen character from each required class is placed first, the remainder filled from the union, and the entire buffer Fisher-Yates-shuffled so the required-class characters are not biased to the head of the string. NIST SP 800-63B §5.1.1.2 deprecates required-class composition rules — they reduce effective entropy and trade resistance to cracking for a stronger memorability constraint — but downstream sites still mandate them, so the generator supports per-class quotas while documenting the cost.
|
||||
|
||||
## Features
|
||||
|
||||
- Bias-free draws via `randomInt` (Lemire 2018 rejection sampling).
|
||||
- Per-class enablement (`lowercase`/`uppercase`/`digits`/`symbols`) and per-class required quotas.
|
||||
- Arbitrary `exclude` set (string / `string[]` / `Set<string>`).
|
||||
- Deterministic under a seeded `Rng` for replay tests.
|
||||
- Zod-validated `opts`; mismatch → `PWGEN.INVALID_OPTIONS`.
|
||||
- Rejects infeasible options (`length < requiredCount` or empty effective charset) with `PWGEN.OPTIONS_INFEASIBLE`.
|
||||
- Emits `pwgen.generate_duration_ms{mode=random}` and `pwgen.entropy_bits{mode=random}` on success.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generatePassword } from "@suaveplan/password-generator";
|
||||
|
||||
const pw = generatePassword({ length: 16 });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AMBIGUOUS,
|
||||
generatePassword,
|
||||
} from "@suaveplan/password-generator";
|
||||
|
||||
// 32-char password that satisfies every class and avoids confusable chars
|
||||
const pw = generatePassword({
|
||||
length: 32,
|
||||
requireLowercase: true,
|
||||
requireUppercase: true,
|
||||
requireDigits: true,
|
||||
requireSymbols: true,
|
||||
exclude: AMBIGUOUS,
|
||||
});
|
||||
|
||||
// Deterministic replay under a seeded Rng
|
||||
import type { Rng } from "@suaveplan/types";
|
||||
function seeded(seed: number): Rng {
|
||||
let s = seed >>> 0;
|
||||
return {
|
||||
nextUint32(): number {
|
||||
s = (s + 0x6d2b79f5) >>> 0;
|
||||
let t = s;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
},
|
||||
nextFloat(): number {
|
||||
return this.nextUint32() / 0x1_0000_0000;
|
||||
},
|
||||
};
|
||||
}
|
||||
const replayed = generatePassword({ length: 24 }, seeded(42));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generatePassword(opts: GeneratePasswordOptions, rng?: Rng): string`
|
||||
|
||||
- `opts.length`: positive integer total length.
|
||||
- `opts.lowercase` / `opts.uppercase` / `opts.digits` / `opts.symbols`: defaults to `true`; pass `false` to omit the class from the union.
|
||||
- `opts.requireLowercase` / `opts.requireUppercase` / `opts.requireDigits` / `opts.requireSymbols`: defaults to `false`; when `true`, at least one character from the class is guaranteed in the output.
|
||||
- `opts.exclude`: characters to subtract from the union after class merge.
|
||||
- `opts.mode`: ignored by `generatePassword`; see `pronounceable.ts` for the `pronounceable` mode.
|
||||
- `rng`: defaults to `defaultRng()`.
|
||||
|
||||
Throws `PwgenError(PWGEN.INVALID_OPTIONS)` on Zod mismatch, `PwgenError(PWGEN.OPTIONS_INFEASIBLE)` when `length < requiredCount` or when the effective charset is empty after exclusions.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The required-class placement is a two-phase fill: phase 1 pushes one randomly-chosen character per enabled required class into the buffer; phase 2 fills the remainder from the full effective charset. The buffer is then shuffled in-place via `shuffleInPlace` so the required-class characters end up at uniformly-distributed positions across `[0, length)`. Without the shuffle, the leading `requiredCount` characters would be deterministic-by-position, which is observable to an attacker and weakens the resistance of the generated password.
|
||||
|
||||
Effective-charset construction goes through `buildEffectiveCharset`, which performs class-merge + exclusion arithmetic. If any required class becomes empty after exclusions (e.g. `requireDigits: true` with `exclude: "0123456789"`) the generator throws `PWGEN.OPTIONS_INFEASIBLE` instead of producing an output that silently violates the contract. Entropy is reported via `estimateEntropy` immediately before recording — this is an upper-bound `log2(charset) * length` estimate; pattern-aware scoring lives in `@suaveplan/password-strength`.
|
||||
@@ -0,0 +1,72 @@
|
||||
# `pronounceable`
|
||||
|
||||
## Purpose
|
||||
|
||||
`generatePronounceablePassword(opts, rng?)` emits CVC-pattern (consonant–vowel–consonant) syllables suitable for shoulder-surf-friendly UX contexts where the user must read the password aloud or type it from memory in a short window. The output alternates consonant-frequency-weighted picks from a 21-character consonant set and 5-character vowel set, drawn via the bias-free `randomInt` primitive. Pronounceable passwords trade entropy for memorability: per-character entropy is ≈ log2(21·5)/2 ≈ 4.7 bits versus ≈ 6.55 bits for a full 94-character symbol set, so callers SHOULD pair this mode with a longer `length` or reserve it strictly for UX contexts where the user re-types the password by hand.
|
||||
|
||||
The consonant ordering is derived from Norvig's 2013 English-corpus letter frequencies. Picks are uniform over the 21-character consonant set (not frequency-biased), but the choice of which consonants appear at all reflects common English digrams — `q` and `x` are present but produce visibly less "English-shaped" syllables than `t` and `n`.
|
||||
|
||||
## Features
|
||||
|
||||
- 3-character CVC syllables (`[bcdfghjklmnpqrstvwxyz][aeiou][bcdfghjklmnpqrstvwxyz]`).
|
||||
- Truncates the final syllable when `length % 3 !== 0`.
|
||||
- Deterministic under a seeded `Rng`.
|
||||
- Throws `PWGEN.INVALID_OPTIONS` for `length < 1` or non-integer `length`.
|
||||
- Emits `pwgen.generate_duration_ms{mode=pronounceable}` and `pwgen.entropy_bits{mode=pronounceable}` on success.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generatePronounceablePassword } from "@suaveplan/password-generator";
|
||||
|
||||
const pw = generatePronounceablePassword({ length: 12, mode: "pronounceable" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { Rng } from "@suaveplan/types";
|
||||
import { generatePronounceablePassword } from "@suaveplan/password-generator";
|
||||
|
||||
function seeded(seed: number): Rng {
|
||||
let s = seed >>> 0;
|
||||
return {
|
||||
nextUint32(): number {
|
||||
s = (s + 0x6d2b79f5) >>> 0;
|
||||
let t = s;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
},
|
||||
nextFloat(): number {
|
||||
return this.nextUint32() / 0x1_0000_0000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Pair with longer length to recover entropy lost to the CVC constraint.
|
||||
// 18 chars ≈ 18 * 4.7 ≈ 85 bits.
|
||||
const pw = generatePronounceablePassword(
|
||||
{ length: 18, mode: "pronounceable" },
|
||||
seeded(1234),
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generatePronounceablePassword(opts: GeneratePasswordOptions & { mode: "pronounceable" }, rng?: Rng): string`
|
||||
|
||||
- `opts.length`: positive integer total length. The last syllable is truncated to fit.
|
||||
- `opts.mode`: must be `"pronounceable"`; type discriminator only.
|
||||
- `rng`: defaults to `defaultRng()`.
|
||||
|
||||
### Constants
|
||||
|
||||
- `PRONOUNCEABLE_CONSONANTS: string` — the 21-character consonant set ordered by English frequency.
|
||||
- `PRONOUNCEABLE_VOWELS: string` — `aeiou`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The generator never reaches Markov-state complexity — instead the CVC pattern is encoded directly in the position index. At output index `i`, `idx = i % 3` selects between the consonant set (`idx === 0` or `idx === 2`) and the vowel set (`idx === 1`). This sidesteps the digram-table lookups Markov-style generators perform and produces strictly uniform draws within each class.
|
||||
|
||||
Frequency-weighted picks (where common consonants like `t` and `n` would appear more often than rare ones like `q` and `x`) would make output more English-shaped at the cost of further entropy reduction. The trade-off was decided in favour of uniform per-class picks because the package's `assertStrength` measurement assumes the entropy formula `log2(charset) * length`, which only holds under uniform sampling. Switching to frequency-weighted picks would require carrying a separate empirical entropy estimate per locale.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `pin`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`generatePin(length, rng?)` returns a `length`-character PIN drawn uniformly from `[0-9]` via the bias-free `randomInt(rng, 10)` primitive. PINs are intentionally a separate surface from `generatePassword` because the constraint set is narrower (only digits, no class quotas, no exclusion arithmetic) and call-site UX is different — banking apps, SIM lock screens, and confirmation flows want a tight `generatePin(6)` rather than a `generatePassword({ length: 6, lowercase: false, uppercase: false, symbols: false })` configuration cliff.
|
||||
|
||||
The per-digit entropy is `log2(10) ≈ 3.32` bits, so a 6-digit PIN carries ≈ 19.9 bits and a 12-digit PIN ≈ 39.9 bits. PINs SHOULD be paired with rate-limiting or lockout policies at the call site; this generator is responsible only for producing the digits, not for protecting the channel that submits them.
|
||||
|
||||
## Features
|
||||
|
||||
- Bias-free uniform draws over `[0, 10)`.
|
||||
- Throws `PWGEN.OPTIONS_INFEASIBLE` for `length < 1` or non-integer `length`.
|
||||
- Deterministic under a seeded `Rng`.
|
||||
- Emits `pwgen.generate_duration_ms{mode=pin}` and `pwgen.entropy_bits{mode=pin}` on success.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generatePin } from "@suaveplan/password-generator";
|
||||
|
||||
const pin = generatePin(6); // "049213"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { Rng } from "@suaveplan/types";
|
||||
import { generatePin } from "@suaveplan/password-generator";
|
||||
|
||||
function seeded(seed: number): Rng {
|
||||
let s = seed >>> 0;
|
||||
return {
|
||||
nextUint32(): number {
|
||||
s = (s + 0x6d2b79f5) >>> 0;
|
||||
let t = s;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
},
|
||||
nextFloat(): number {
|
||||
return this.nextUint32() / 0x1_0000_0000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Replay a known PIN for tests
|
||||
const replayed = generatePin(12, seeded(42));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generatePin(length: number, rng?: Rng): string`
|
||||
|
||||
- `length`: positive integer total digit count.
|
||||
- `rng`: defaults to `defaultRng()`.
|
||||
|
||||
Throws `PwgenError(PWGEN.OPTIONS_INFEASIBLE)` when `length < 1` or `length` is non-integer.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The function loops `length` times calling `randomInt(rng, 10).toString()` and concatenates the result. Building the output via `String#concat` rather than `Array.join` keeps the hot path allocation-light — for the typical 4-to-12-digit range the JIT keeps the buffer in registers without intermediate array creation.
|
||||
|
||||
Distribution conformance is tested by drawing 100 000 digits and asserting each of the 10 buckets falls within ±10% of the expected `total / 10`. The χ² statistical test owned by `randomInt` is what guarantees uniform per-bucket distribution; the PIN-specific test exists only to catch a regression where `generatePin` introduces its own bias on top of `randomInt`.
|
||||
@@ -0,0 +1,54 @@
|
||||
# `default-rng`
|
||||
|
||||
## Purpose
|
||||
|
||||
`defaultRng()` builds the default `Rng` backed by `@suaveplan/crypto-utils.randomBytes`, which delegates to `crypto.getRandomValues` and is therefore cryptographically strong on every supported runtime (Bun, Node ≥ 20, Deno, the browser). The function is invoked lazily by each generator when the caller does not supply their own `Rng`, which means consumers that always pass a deterministic seeded `Rng` (tests, replay logs) never reach `crypto.getRandomValues` at all and tree-shakers can drop this module.
|
||||
|
||||
Keeping the default in a separate file (rather than inline at every generator's call site) gives a single canonical place to swap entropy sources — for example to wire a hardware RNG adapter — without touching the generators themselves.
|
||||
|
||||
## Features
|
||||
|
||||
- Wraps `randomBytes(4)` to produce a uint32 per `nextUint32` call.
|
||||
- Optional `nextBytes(n)` exposed for the rare consumer that wants raw entropy.
|
||||
- Optional `nextFloat()` computed from `nextUint32() / 2^32`.
|
||||
- Zero module-scope state — every call to `defaultRng()` returns a fresh `Rng`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { defaultRng } from "@suaveplan/password-generator/random";
|
||||
|
||||
const rng = defaultRng();
|
||||
const u = rng.nextUint32(); // ∈ [0, 2^32)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { randomInt, defaultRng } from "@suaveplan/password-generator/random";
|
||||
import { generatePassword } from "@suaveplan/password-generator/password";
|
||||
|
||||
// The two snippets below are equivalent — generatePassword falls back to
|
||||
// defaultRng() when no rng is supplied.
|
||||
generatePassword({ length: 16 });
|
||||
generatePassword({ length: 16 }, defaultRng());
|
||||
|
||||
// Explicit rng is preferable inside loops so the same instance is reused
|
||||
// across many calls without re-importing crypto each time.
|
||||
const rng = defaultRng();
|
||||
for (let i = 0; i < 100; i++) {
|
||||
randomInt(rng, 7776);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `defaultRng(): Rng`
|
||||
|
||||
Builds a fresh `Rng` whose `nextUint32` reads four bytes from `@suaveplan/crypto-utils.randomBytes` and assembles them into a big-endian uint32. The returned object also implements the optional `nextFloat` (uint32 normalised into `[0, 1)`) and `nextBytes` (direct passthrough to `randomBytes(n)`) members of the `Rng` contract.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The bit layout for `nextUint32` is `(b0 << 24) | (b1 << 16) | (b2 << 8) | b3`, with the final `>>> 0` to coerce JavaScript's 32-bit signed arithmetic back into an unsigned integer. The same big-endian ordering is consistent across runtimes — `crypto.getRandomValues` makes no endianness promises, but since every byte is independent and uniform the assembly order does not affect the distribution.
|
||||
|
||||
`nextFloat` is intentionally implemented as `nextUint32() / 2^32` rather than `Math.random()` so the package contains _zero_ references to `Math.random` (rule 8 of the source-tree hygiene gate). The float resolution is 32-bit; consumers that need 53-bit precision should derive it from two `nextUint32` draws themselves.
|
||||
@@ -0,0 +1,64 @@
|
||||
# `fisher-yates`
|
||||
|
||||
## Purpose
|
||||
|
||||
`shuffleInPlace(array, rng)` performs an in-place Fisher–Yates / Knuth shuffle parameterised over the bias-free `randomInt` primitive. The shuffle is the load-bearing step that prevents required-class characters from being biased to the head of `generatePassword`'s output buffer — without it, the first N characters of every generated password would always be the required-class quotas in declaration order, leaking information about the generator's option set to anyone who inspects the produced string.
|
||||
|
||||
The standard Knuth analysis guarantees that every permutation of `array` appears with equal probability, _provided_ the underlying integer draws are uniform. Pairing the algorithm with the bias-free `randomInt` rather than `Math.floor(Math.random() * n)` is what makes that guarantee hold for arbitrary `array.length`.
|
||||
|
||||
## Features
|
||||
|
||||
- Cryptographically-unbiased permutations via Lemire-style rejection sampling.
|
||||
- In-place mutation; returns the same array reference for fluent chaining.
|
||||
- No-op for arrays of length 0 or 1.
|
||||
- Pure given `rng`; deterministic under a seeded `Rng`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { defaultRng, shuffleInPlace } from "@suaveplan/password-generator/random";
|
||||
|
||||
const arr = [1, 2, 3, 4, 5];
|
||||
shuffleInPlace(arr, defaultRng());
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { Rng } from "@suaveplan/types";
|
||||
import { shuffleInPlace } from "@suaveplan/password-generator/random";
|
||||
|
||||
function seededRng(seed: number): Rng {
|
||||
let s = seed >>> 0;
|
||||
return {
|
||||
nextUint32(): number {
|
||||
s = (s + 0x6d2b79f5) >>> 0;
|
||||
let t = s;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
},
|
||||
nextFloat(): number {
|
||||
return this.nextUint32() / 0x1_0000_0000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Reproducible card-deck shuffle for replay logs
|
||||
const deck = [...Array(52).keys()];
|
||||
shuffleInPlace(deck, seededRng(123));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `shuffleInPlace<T>(array: T[], rng: Rng): T[]`
|
||||
|
||||
- `array`: mutated in-place. The reference is returned so consumers can chain.
|
||||
- `rng`: any `Rng`; only `nextUint32` is used (via `randomInt`).
|
||||
- Returns: the same `array` reference.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The implementation walks `i` from `array.length - 1` down to `1`. At each step it draws `j = randomInt(rng, i + 1)` and swaps positions `i` and `j`. Bias-free uniform draws are critical: a biased `randomInt` would skew certain permutations and the standard distributional argument would fail.
|
||||
|
||||
The shuffle complexity is `O(array.length)` with one `randomInt` call per element except the last. Memory complexity is `O(1)` beyond the input array. Returning the same array reference lets callers compose `shuffleInPlace(...).map(...)` without an explicit local variable; consumers who need an immutable shuffle should clone the input first.
|
||||
@@ -0,0 +1,68 @@
|
||||
# `random-int`
|
||||
|
||||
## Purpose
|
||||
|
||||
`randomInt(rng, max)` returns a uniformly-distributed integer in the half-open interval `[0, max)` for any `max ∈ [1, 2^32)` by drawing uint32 values from the supplied `Rng` and rejecting draws above the threshold `2^32 - (2^32 mod max)`. This is the load-bearing primitive every generator in the package relies on for unbiased sampling — replacing it with the naïve `Math.floor(rng() * max)` shortcut would silently bias output for non-power-of-two `max`, the very common case for 7776-entry word lists and 10-entry digit picks.
|
||||
|
||||
The implementation follows Daniel Lemire's 2018 _Fast Random Integer Generation in an Interval_ (https://arxiv.org/abs/1805.10941) §3 ("Nearly Divisionless"). Expected per-call draws are below 2 in the worst case; for the EFF Long list (`max = 7776`) the rejection probability is < 0.0000019 per draw, so the bounded retry budget (1024 attempts) acts purely as a guard against a pathologically broken `Rng`.
|
||||
|
||||
## Features
|
||||
|
||||
- Strictly uniform output across `[0, max)`.
|
||||
- Rejection-sampling loop with a 1024-attempt safety bound.
|
||||
- Throws `PWGEN.RNG_FAILED` for invalid `max` or pathological RNGs.
|
||||
- Pure function; no I/O, no module-scope state.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { defaultRng } from "@suaveplan/password-generator/random";
|
||||
import { randomInt } from "@suaveplan/password-generator/random";
|
||||
|
||||
const rng = defaultRng();
|
||||
const v = randomInt(rng, 7776); // ∈ [0, 7776)
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type { Rng } from "@suaveplan/types";
|
||||
import { randomInt } from "@suaveplan/password-generator/random";
|
||||
|
||||
function seededRng(seed: number): Rng {
|
||||
let s = seed >>> 0;
|
||||
return {
|
||||
nextUint32(): number {
|
||||
s = (s + 0x6d2b79f5) >>> 0;
|
||||
let t = s;
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return (t ^ (t >>> 14)) >>> 0;
|
||||
},
|
||||
nextFloat(): number {
|
||||
return this.nextUint32() / 0x1_0000_0000;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Deterministic replay for tests
|
||||
const rng = seededRng(42);
|
||||
const a = randomInt(rng, 256);
|
||||
const b = randomInt(rng, 256);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `randomInt(rng: Rng, max: number): number`
|
||||
|
||||
Returns a uniform integer in `[0, max)`.
|
||||
|
||||
- `rng`: any `Rng` from `@suaveplan/types`. Only `nextUint32` is used.
|
||||
- `max`: must satisfy `Number.isInteger(max) && 1 <= max < 2^32`.
|
||||
- Throws `PwgenError(PWGEN.RNG_FAILED)` when `max` is out of range or when the rejection loop exhausts its 1024-attempt budget.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The threshold `T = 2^32 - (2^32 mod max)` is the largest multiple of `max` below `2^32`. Any uint32 in `[0, T)` is uniformly distributed across the `T / max` complete copies of `[0, max)` it covers; values in `[T, 2^32)` are rejected and the next draw is taken. This makes the output strictly uniform at the cost of an expected `< 2` calls to `rng.nextUint32` for typical `max`.
|
||||
|
||||
The retry budget guards against an `Rng` that always returns values above the threshold — without the bound, a broken RNG would loop forever. 1024 attempts is far beyond any non-pathological RNG: even with `max = 7776` the probability of 1024 consecutive rejections is below `(7776 / 2^32)^1024`, indistinguishable from zero. Reaching the bound therefore proves the supplied `Rng` is broken and the function throws `PWGEN.RNG_FAILED` instead of hanging.
|
||||
@@ -0,0 +1,60 @@
|
||||
# `schemas`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
Owns the Zod boundary schemas that every public generator function uses to validate its `opts` argument. Schema mismatch throws `PwgenError(PWGEN.INVALID_OPTIONS)` with the original `ZodError` attached as `cause`, so callers can introspect the failing field path while keeping the typed error code stable for logging.
|
||||
|
||||
Strict-mode schemas reject unknown fields rather than silently dropping them — typos like `requrieDigits` (with a transposed `i`) surface immediately instead of being ignored at runtime. Three schemas are exported: `GeneratePasswordOptionsSchema`, `GeneratePassphraseOptionsSchema`, and `EntropyOptionsSchema`.
|
||||
|
||||
## Features
|
||||
|
||||
- Strict-object schemas reject unknown fields.
|
||||
- Positive-integer guards on `length`-shaped fields.
|
||||
- Discriminated `wordlist` accepts `"long"`, `"short"`, an inline `string[]`, or any registry name.
|
||||
- All three schemas are invoked from at least one production call site (the conformance script verifies this).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { GeneratePasswordOptionsSchema } from "@suaveplan/password-generator/schemas";
|
||||
|
||||
const result = GeneratePasswordOptionsSchema.safeParse({ length: 16 });
|
||||
if (!result.success) throw result.error;
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
EntropyOptionsSchema,
|
||||
GeneratePassphraseOptionsSchema,
|
||||
} from "@suaveplan/password-generator/schemas";
|
||||
|
||||
// Pre-validate user-supplied JSON before forwarding to the generator
|
||||
function fromJson(raw: unknown) {
|
||||
const parsed = GeneratePassphraseOptionsSchema.parse(raw);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
// Compose with downstream schemas
|
||||
import z from "zod";
|
||||
const apiBody = z.object({
|
||||
mode: z.literal("passphrase"),
|
||||
opts: GeneratePassphraseOptionsSchema,
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `GeneratePasswordOptionsSchema` — strict object mirroring `GeneratePasswordOptions`.
|
||||
- `GeneratePassphraseOptionsSchema` — strict object mirroring `GeneratePassphraseOptions`. `length` is optional in the schema (the generator applies the default `6`).
|
||||
- `EntropyOptionsSchema` — strict object mirroring `EntropyOptions`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The schemas live in a single feature module to keep the conformance script's `src/schemas/` discovery simple — the script walks that directory and verifies every exported `*Schema` is invoked via `.parse(` or `.safeParse(` somewhere outside the directory. This pattern also matches the layout the `cbor`, `validation`, and `vault` packages already use, so reviewers don't need to context-switch.
|
||||
|
||||
Strict-object semantics are non-negotiable for boundary surfaces — Zod's default `passthrough` would let typos silently pass through to the generator and corrupt observability (a `requireDigit` typo with a missing `s` would not enable required-digit quotas but `recordError` would never fire). The trade-off — slightly more verbose error messages when adding new options forces a schema update in the same commit — is the entire point of the gate.
|
||||
@@ -0,0 +1,48 @@
|
||||
# `eff-long` — EFF Long Wordlist
|
||||
|
||||
## Purpose
|
||||
|
||||
Bundles the EFF Long Wordlist as a frozen `readonly string[]` so callers who only want passphrase generation pull this 85 KB asset and nothing else. The list contains exactly 7776 entries (5-dice / 6⁵), giving ≈ 12.92 bits of entropy per word and 77.55 bits for a 6-word passphrase. Source: https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt (Electronic Frontier Foundation, released under CC BY 3.0 US).
|
||||
|
||||
The wordlist is exposed via two surfaces: the registry name `"eff-long"` (via `getWordlist`) and the dedicated subpath import `@suaveplan/password-generator/wordlists/eff-long`. The subpath import lets a tree-shaker drop the array entirely from a bundle that only generates char-class passwords or PINs.
|
||||
|
||||
## Features
|
||||
|
||||
- Exactly 7776 ASCII entries.
|
||||
- Deduplicated and prefix-disambiguated within the first three characters (per EFF's selection criteria).
|
||||
- Each word is 3–9 characters and contains only `[a-z-]`.
|
||||
- Frozen array — safe to share across modules.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EFF_LONG_WORDLIST } from "@suaveplan/password-generator/wordlists/eff-long";
|
||||
|
||||
console.log(EFF_LONG_WORDLIST[0]); // "abacus"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { generatePassphrase } from "@suaveplan/password-generator";
|
||||
import { EFF_LONG_WORDLIST } from "@suaveplan/password-generator/wordlists/eff-long";
|
||||
|
||||
// Verify a passphrase came from the EFF Long list
|
||||
const pp = generatePassphrase({});
|
||||
const longSet = new Set(EFF_LONG_WORDLIST);
|
||||
for (const w of pp.split("-")) {
|
||||
if (!longSet.has(w) && !w.includes("-")) throw new Error("not from EFF Long");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EFF_LONG_WORDLIST: readonly string[]`
|
||||
|
||||
Frozen array of 7776 lowercase ASCII strings. Mutation attempts throw at runtime in strict mode.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The wordlist is embedded verbatim from the EFF source file. Re-generating it (for example to localise to a non-English language) requires verifying the prefix-disambiguation property: every pair of entries differs within the first three characters, which makes typed passphrases auto-completable without ambiguity in EFF's documented dice-rolling protocol. The package does not re-verify this property at runtime because the array is constant — it's the responsibility of any caller who re-bundles a translated list to maintain the property.
|
||||
|
||||
Bundle cost: roughly 85 KB minified UTF-8 (the array literal expansion). The `sideEffects: false` flag on the package plus the `preserveModules: true` Vite output ensure that an import of `@suaveplan/password-generator/password` does not transitively pull in this module — verified by the rollup tree-shaker against the build output.
|
||||
@@ -0,0 +1,43 @@
|
||||
# `eff-short` — EFF Short Wordlist 1
|
||||
|
||||
## Purpose
|
||||
|
||||
Bundles EFF's Short Wordlist #1 — 1296 entries (4-dice / 6⁴), ≈ 10.34 bits/word, every entry ≤ 5 ASCII characters. Source: https://www.eff.org/files/2016/09/08/eff_short_wordlist_1.txt (Electronic Frontier Foundation, released under CC BY 3.0 US). The short list is optimised for memorability and typing speed at the cost of fewer bits per word; an 8-word short-list passphrase (~82.7 bits) is comparable in entropy to a 6.4-word long-list passphrase but markedly faster to type.
|
||||
|
||||
Exposed via the registry name `"eff-short"` and the dedicated subpath `@suaveplan/password-generator/wordlists/eff-short` so a consumer who only wants the short list does not pull the ~85 KB long list.
|
||||
|
||||
## Features
|
||||
|
||||
- Exactly 1296 ASCII entries.
|
||||
- Every entry is ≤ 5 characters; words are easy to spell.
|
||||
- Deduplicated; only `[a-z-]` characters.
|
||||
- Frozen array.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { EFF_SHORT_WORDLIST } from "@suaveplan/password-generator/wordlists/eff-short";
|
||||
|
||||
console.log(EFF_SHORT_WORDLIST.length); // 1296
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { generatePassphrase } from "@suaveplan/password-generator";
|
||||
|
||||
// 8 short words for ≈ 82.7 bits — comparable to a 6-word EFF Long passphrase
|
||||
const pp = generatePassphrase({ wordlist: "short", length: 8 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `EFF_SHORT_WORDLIST: readonly string[]`
|
||||
|
||||
Frozen array of 1296 lowercase ASCII strings.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The short list was constructed by EFF specifically for "shorter words that are easier to type". Per-word entropy is `log2(1296) ≈ 10.34` bits versus `log2(7776) ≈ 12.92` for the long list — to match a 6-word long-list passphrase's 77.55 bits, callers need 7.5 (round up to 8) short-list words. The trade-off favours short-list passphrases when the user must type the phrase on a constrained surface like a mobile virtual keyboard, where the per-word typing-time penalty dominates the per-word entropy gain.
|
||||
|
||||
EFF also publishes a Short Wordlist 2 with autocompleting prefixes; this package intentionally bundles only Wordlist 1 to keep the runtime surface minimal. Wordlist 2 is available as a caller-registered list via `registerWordlist`; future versions of this package may add a dedicated subpath if there is demand.
|
||||
@@ -0,0 +1,66 @@
|
||||
# `wordlists/registry`
|
||||
|
||||
## Purpose
|
||||
|
||||
The wordlist registry exposes a small plugin API — `registerWordlist`, `getWordlist`, `listWordlists` — that lets callers add localised or themed wordlists to the same name-space the bundled EFF lists use. `generatePassphrase` resolves `opts.wordlist` through this registry, so any caller-registered list gets the same treatment as the built-in lists with zero changes to the passphrase generator. The registry enforces a 256-entry minimum (8 bits per word) so a 16-word passphrase built from any registered list crosses the 128-bit symmetric-key floor.
|
||||
|
||||
The registry is pre-populated with `"eff-long"` and `"eff-short"`. Calling `registerWordlist("eff-long", customList)` overwrites the bundled list — useful for caller-supplied localised variants of the EFF lists.
|
||||
|
||||
## Features
|
||||
|
||||
- Pre-populated with `"eff-long"` and `"eff-short"`.
|
||||
- 256-entry minimum enforced at registration.
|
||||
- Unknown names throw a typed `PWGEN.WORDLIST_NOT_REGISTERED` error.
|
||||
- Re-registration overwrites the previous entry (deliberate, for localisation).
|
||||
- `listWordlists()` for UI surfaces that need to render available options.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
getWordlist,
|
||||
listWordlists,
|
||||
registerWordlist,
|
||||
} from "@suaveplan/password-generator/wordlists/registry";
|
||||
|
||||
const dnd = [/* ... 300 fantasy nouns ... */];
|
||||
registerWordlist("dnd-spells", dnd);
|
||||
console.log(listWordlists()); // ["eff-long", "eff-short", "dnd-spells"]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { generatePassphrase } from "@suaveplan/password-generator";
|
||||
import { registerWordlist } from "@suaveplan/password-generator/wordlists/registry";
|
||||
|
||||
// Localise the long list to Italian (must still be ≥ 256 entries; EFF
|
||||
// publishes localised lists at https://www.eff.org/dice).
|
||||
const italianLong = await loadItalianEffLong();
|
||||
registerWordlist("eff-long-it", italianLong);
|
||||
const pp = generatePassphrase({ wordlist: "eff-long-it", length: 6 });
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `registerWordlist(name: string, words: readonly string[]): void`
|
||||
|
||||
Throws `PwgenError(PWGEN.WORDLIST_TOO_SHORT)` when `words.length < 256` (the `MIN_WORDLIST_SIZE` constant).
|
||||
|
||||
### `getWordlist(name: string): readonly string[]`
|
||||
|
||||
Throws `PwgenError(PWGEN.WORDLIST_NOT_REGISTERED)` for unknown names.
|
||||
|
||||
### `listWordlists(): string[]`
|
||||
|
||||
Returns every registered name, in insertion order.
|
||||
|
||||
### `MIN_WORDLIST_SIZE: 256`
|
||||
|
||||
Exported so callers can show "needs at least 256 entries" in upload UIs.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The registry is a single module-level `Map<string, readonly string[]>` keyed by name. There is intentionally no per-realm or per-call-site isolation — registration is global because the use case (localising the bundled lists, adding a themed list) is fundamentally a deployment-time decision, not per-request state. Callers that need stricter isolation should pass an inline `string[]` to `generatePassphrase` instead, which bypasses the registry entirely.
|
||||
|
||||
The 256-entry floor (`log2(256) = 8` bits/word) is set so that 10-word passphrases against the smallest acceptable list still reach 80 bits, which is above NIST SP 800-63B's symmetric-key-equivalent floor for moderate-strength contexts. EFF's Short Wordlist 1 (1296 entries) comfortably exceeds this — the floor is enforced primarily to protect against caller-supplied "wordlists" that are actually tiny themed lists too short to provide meaningful entropy.
|
||||
Reference in New Issue
Block a user