first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View 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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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`.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.