Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
# @suaveplan/password-strength - zxcvbn-style password strength estimator — pattern-matching + dictionary cracker model per Wheeler 2016. Zero browser/Node/React deps.
## Summary
zxcvbn-style password strength estimator — pattern-matching + dictionary
cracker model per Wheeler 2016. Universal; zero browser/Node/React
deps. Re-implementation of the open-source `dropbox/zxcvbn` algorithm in
style: Zod boundaries, OTel histograms + counters, structured
`PWSTRENGTH.*` error codes, 100% coverage, multilingual dictionary
subpaths, runtime dictionary swap, observable streaming variant, and a
hard 200-character DoS guard.
<!-- begin:auto-docs -->
## Documentation tree
- [assess](./src/assess/assess.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [assess-reactive](./src/assess/assess-reactive.md) — Debounce + cancel-on-newer-input wrapper around `assess` for UI strength meters that update as the user types. Each new…
- [assess](./src/assess/assess.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [dicts](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- wikipedia — <!-- begin:auto-docs --> _(no submodules…
- [de](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [wikipedia](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [english-wikipedia](./src/dicts/en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./src/dicts/en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./src/dicts/en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [wikipedia](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [fr](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [wikipedia](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [ru](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [wikipedia](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [zh-Hans](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [wikipedia](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [load-dictionaries](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- wikipedia — <!-- begin:auto-docs --> _(no submodules…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [graphs](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- azerty — <!-- begin:auto-docs --> _(no submodules)_…
- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [validate-graph](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- azerty — <!-- begin:auto-docs --> _(no submodules)_…
- [matchers](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- date — <!-- begin:auto-docs --> _(no submodules)_ <!…
- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - l33t-table — Provides the default character → lette…
- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs --> - l33t-table — Provides the default character → lette…
- [l33t-table](./src/matchers/dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->- date — <!-- begin:auto-docs --> _(no submodules)_ <!…
- [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 -->
- [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 -->
- [crack-times](./src/scoring/crack-times.md) — Maps a `guesses` count to seconds-to-crack under the four canonical attack scenarios from Wheeler 2016 section 5: onlin…
- [estimate-guesses](./src/scoring/estimate-guesses.md) — Implements the Wheeler 2016 section 4 closed-form guess estimates per pattern type: dictionary (rank × uppercase × l33t…
- [most-guessable-match-sequence](./src/scoring/most-guessable-match-sequence.md) — Picks the lowest-`Π guesses × k!` cover of the password from the union of pattern matches plus implicit bruteforce-char…
- [score](./src/scoring/score.md) — Maps a `guesses` count to a coarse integer score per Wheeler 2016 section 5. This score (`0`–`4`) is what most consumer…
<!-- end:auto-docs --> - [assess-reactive](./src/assess/assess-reactive.md) — Debounce + cancel-on-newer-input wrapper around `assess` for UI strength meters that update as the user types. Each new…
- [assess](./src/assess/assess.md) — <!-- begin:auto-docs --> _(no submodules)_- [dicts](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- [de](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./src/dicts/en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./src/dicts/en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./src/dicts/en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_- [graphs](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- [matchers](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [l33t-table](./src/matchers/dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/matchers/types.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)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [crack-times](./src/scoring/crack-times.md) — Maps a `guesses` count to seconds-to-crack under the four canonical attack scenarios from Wheeler 2016 section 5: onlin…
- [estimate-guesses](./src/scoring/estimate-guesses.md) — Implements the Wheeler 2016 section 4 closed-form guess estimates per pattern type: dictionary (rank × uppercase × l33t…
- [most-guessable-match-sequence](./src/scoring/most-guessable-match-sequence.md) — Picks the lowest-`Π guesses × k!` cover of the password from the union of pattern matches plus implicit bruteforce-char…
- [score](./src/scoring/score.md) — Maps a `guesses` count to a coarse integer score per Wheeler 2016 section 5. This score (`0`–`4`) is what most consumer…
- [assess-reactive](./src/assess/assess-reactive.md) — Debounce + cancel-on-newer-input wrapper around `assess` for UI strength meters that update as the user types. Each new…
- [assess](./src/assess/assess.md) — <!-- begin:auto-docs --> _(no submodules)_- [dicts](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- wikipedia — <!-- begin:auto-…
- [de](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./src/dicts/en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./src/dicts/en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./src/dicts/en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- wikipedia — <!-- begin:auto-…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_- [graphs](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- azerty — <!-- begin:auto-doc…
- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- azerty — <!-- begin:auto-doc…
- [matchers](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_- date — <!-- begin:auto-docs…
- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [l33t-table](./src/matchers/dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_- date — <!-- begin:auto-docs…
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [crack-times](./src/scoring/crack-times.md) — Maps a `guesses` count to seconds-to-crack under the four canonical attack scenarios from Wheeler 2016 section 5: onlin…
- [estimate-guesses](./src/scoring/estimate-guesses.md) — Implements the Wheeler 2016 section 4 closed-form guess estimates per pattern type: dictionary (rank × uppercase × l33t…
- [most-guessable-match-sequence](./src/scoring/most-guessable-match-sequence.md) — Picks the lowest-`Π guesses × k!` cover of the password from the union of pattern matches plus implicit bruteforce-char…
- [score](./src/scoring/score.md) — Maps a `guesses` count to a coarse integer score per Wheeler 2016 section 5. This score (`0`–`4`) is what most consumer…
- [assess-reactive](./src/assess/assess-reactive.md) — Debounce + cancel-on-newer-input wrapper around `assess` for UI strength meters that update as the user types. Each new…
- [assess](./src/assess/assess.md) — <!-- begin:auto-docs --> _(no submodules)_- [dicts](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- wikipedia — <!-- begin:auto-docs --> _(no submodule…
- [de](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./src/dicts/en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./src/dicts/en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./src/dicts/en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- wikipedia — <!-- begin:auto-docs --> _(no submodule…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_- [graphs](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- azerty — <!-- begin:auto-docs --> _(no submodules)_…
- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- azerty — <!-- begin:auto-docs --> _(no submodules)_…
- [matchers](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_- date — <!-- begin:auto-docs --> _(no submodules)_ <…
- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [l33t-table](./src/matchers/dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_- date — <!-- 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)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [crack-times](./src/scoring/crack-times.md) — Maps a `guesses` count to seconds-to-crack under the four canonical attack scenarios from Wheeler 2016 section 5: onlin…
- [estimate-guesses](./src/scoring/estimate-guesses.md) — Implements the Wheeler 2016 section 4 closed-form guess estimates per pattern type: dictionary (rank × uppercase × l33t…
- [most-guessable-match-sequence](./src/scoring/most-guessable-match-sequence.md) — Picks the lowest-`Π guesses × k!` cover of the password from the union of pattern matches plus implicit bruteforce-char…
- [score](./src/scoring/score.md) — Maps a `guesses` count to a coarse integer score per Wheeler 2016 section 5. This score (`0`–`4`) is what most consumer…
- [assess-reactive](./src/assess/assess-reactive.md) — Debounce + cancel-on-newer-input wrapper around `assess` for UI strength meters that update as the user types. Each new…
- [assess](./src/assess/assess.md) — <!-- begin:auto-docs --> _(no submodules)_- [dicts](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_ - [de](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./src/dicts/de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [english-wikipedia](./src/dicts/en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./src/dicts/en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./src/dicts/en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./src/dicts/es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [fr](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./src/dicts/fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [ru](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./src/dicts/ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [zh-Hans](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./src/dicts/zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [load-dictionaries](./src/dicts/load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_ - [feedback](./src/feedback/feedback.md) — <!-- begin:auto-docs --> _(no submodules)_- [graphs](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_ - [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [azerty](./src/graphs/azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dvorak](./src/graphs/dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_ - [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [qwerty](./src/graphs/qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [validate-graph](./src/graphs/validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- [matchers](./src/matchers/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_ - [date](./src/matchers/date/date.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dictionary](./src/matchers/dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_ - [l33t-table](./src/matchers/dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_ - [keyboard](./src/matchers/keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_ - [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_ - [regex](./src/matchers/regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_ - [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_ - [repeat](./src/matchers/repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sequence](./src/matchers/sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/matchers/types.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)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [crack-times](./src/scoring/crack-times.md) — Maps a `guesses` count to seconds-to-crack under the four canonical attack scenarios from Wheeler 2016 section 5: onlin…
- [estimate-guesses](./src/scoring/estimate-guesses.md) — Implements the Wheeler 2016 section 4 closed-form guess estimates per pattern type: dictionary (rank × uppercase × l33t…
- [most-guessable-match-sequence](./src/scoring/most-guessable-match-sequence.md) — Picks the lowest-`Π guesses × k!` cover of the password from the union of pattern matches plus implicit bruteforce-char…
- [score](./src/scoring/score.md) — Maps a `guesses` count to a coarse integer score per Wheeler 2016 section 5. This score (`0`–`4`) is what most consumer…
## Install
```bash
bun add @suaveplan/password-strength
```
## Quick Start
```ts
import { assess } from "@suaveplan/password-strength";
const result = assess("Password1!");
console.log(result.score); // 0..4 bucket
console.log(result.guesses); // closed-form guess count
console.log(result.feedback.warning); // English-default UI string
console.log(result.crackTimes.offline_fast_hashing_1e10_per_second);
```
## Modules
| Module | Doc |
|---|---|
| `errors/error-codes` | [`src/errors/error-codes.md`](src/errors/error-codes.md) |
| `matchers/dictionary` | [`src/matchers/dictionary/dictionary.md`](src/matchers/dictionary/dictionary.md) |
| `matchers/dictionary/l33t-table` | [`src/matchers/dictionary/l33t-table.md`](src/matchers/dictionary/l33t-table.md) |
| `matchers/sequence` | [`src/matchers/sequence/sequence.md`](src/matchers/sequence/sequence.md) |
| `matchers/repeat` | [`src/matchers/repeat/repeat.md`](src/matchers/repeat/repeat.md) |
| `matchers/date` | [`src/matchers/date/date.md`](src/matchers/date/date.md) |
| `matchers/keyboard` | [`src/matchers/keyboard/keyboard.md`](src/matchers/keyboard/keyboard.md) |
| `matchers/regex` | [`src/matchers/regex/regex.md`](src/matchers/regex/regex.md) |
| `graphs/qwerty` | [`src/graphs/qwerty/qwerty.md`](src/graphs/qwerty/qwerty.md) |
| `graphs/dvorak` | [`src/graphs/dvorak/dvorak.md`](src/graphs/dvorak/dvorak.md) |
| `graphs/azerty` | [`src/graphs/azerty/azerty.md`](src/graphs/azerty/azerty.md) |
| `graphs/validate-graph` | [`src/graphs/validate-graph.md`](src/graphs/validate-graph.md) |
| `dicts/en/passwords` | [`src/dicts/en/passwords.md`](src/dicts/en/passwords.md) |
| `dicts/en/english-wikipedia` | [`src/dicts/en/english-wikipedia.md`](src/dicts/en/english-wikipedia.md) |
| `dicts/en/names` | [`src/dicts/en/names.md`](src/dicts/en/names.md) |
| `dicts/de/wikipedia` | [`src/dicts/de/wikipedia.md`](src/dicts/de/wikipedia.md) |
| `dicts/fr/wikipedia` | [`src/dicts/fr/wikipedia.md`](src/dicts/fr/wikipedia.md) |
| `dicts/es/wikipedia` | [`src/dicts/es/wikipedia.md`](src/dicts/es/wikipedia.md) |
| `dicts/zh-Hans/wikipedia` | [`src/dicts/zh-Hans/wikipedia.md`](src/dicts/zh-Hans/wikipedia.md) |
| `dicts/ru/wikipedia` | [`src/dicts/ru/wikipedia.md`](src/dicts/ru/wikipedia.md) |
| `dicts/load-dictionaries` | [`src/dicts/load-dictionaries.md`](src/dicts/load-dictionaries.md) |
| `scoring/score` | [`src/scoring/score.md`](src/scoring/score.md) |
| `scoring/crack-times` | [`src/scoring/crack-times.md`](src/scoring/crack-times.md) |
| `scoring/estimate-guesses` | [`src/scoring/estimate-guesses.md`](src/scoring/estimate-guesses.md) |
| `scoring/most-guessable-match-sequence` | [`src/scoring/most-guessable-match-sequence.md`](src/scoring/most-guessable-match-sequence.md) |
| `assess/assess` | [`src/assess/assess.md`](src/assess/assess.md) |
| `assess/assess-reactive` | [`src/assess/assess-reactive.md`](src/assess/assess-reactive.md) |
| `feedback/feedback` | [`src/feedback/feedback.md`](src/feedback/feedback.md) |
| `schemas` | [`src/schemas/schemas.md`](src/schemas/schemas.md) |
| `observability/metrics` | [`src/observability/metrics.md`](src/observability/metrics.md) |
## Features
- Six pattern matchers: dictionary (with l33t + reverse), sequence,
repeat, date, keyboard adjacency, regex
- DP cover with factorial penalty per Wheeler section 4
- Four crack-time scenarios, site-tunable via `adjustments`
- 5-bucket score (`0`–`4`) per Wheeler section 5
- 200-character DoS guard
- Streaming variant (`assessReactive`) with debounce + cancel-on-newer
- Runtime dictionary swap via `loadDictionaries`
- Multilingual dictionary subpaths (`en`, `de`, `fr`, `es`, `zh-Hans`, `ru`)
- Keyboard graph subpaths (`qwerty`, `dvorak`, `azerty`)
- Zod-validated boundaries at every public entry point
- OTel histogram + counters for assess duration, score distribution,
error code, and matcher hit count
## Baseline benchmarks
Run `bun run src/__benchmarks__/assess.bench.ts` to print baseline numbers
locally. Indicative timings on commodity hardware (single fork, no warmup):
| Bench | Mean |
|---|---|
| 8-char password | ~0.5 ms |
| 64-char password | ~5 ms |
| Dictionary-heavy | ~1 ms |
| DP n=100 | ~3 ms |
| DP n=199 | ~12 ms |
The 200-character DoS guard keeps p99 well under the 5 ms `it()` default.

View File

@@ -0,0 +1,74 @@
# `assess-reactive` — streaming assessment with debounce + cancel
## Purpose
Debounce + cancel-on-newer-input wrapper around `assess` for UI strength
meters that update as the user types. Each new input cancels the pending
assessment for the previous input; only the latest stable value reaches
the consumer. The wrapper honours an optional `AbortSignal` so callers
can tear down a meter without orphaning pending work.
## Features
- Configurable `debounceMs` (default 150 ms, range `[0, 5000]`)
- Cancel-on-newer-input — superseded inputs do not yield results
- Clean `AbortSignal` completion (no thrown error on abort)
- 200-character DoS guard surfaced as `INPUT_TOO_LONG`
- Backed by an `AsyncIterable<StrengthResult>` so consumers integrate via
`for await`
## Usage: Basic Example
```ts
import { assessReactive } from "@suaveplan/password-strength";
async function* keystrokes() {
yield "p";
yield "pa";
yield "pas";
yield "pass";
}
for await (const r of assessReactive(keystrokes())) {
console.log(r.password, r.score);
}
```
## Usage: Advanced Example
```ts
import { assessReactive } from "@suaveplan/password-strength";
const controller = new AbortController();
const stream = assessReactive(keystrokes(), {
debounceMs: 100,
signal: controller.signal,
assessOptions: { userInputs: ["alice@example.com"] },
});
setTimeout(() => controller.abort(), 50);
for await (const r of stream) {
console.log(r.score);
}
```
## API Reference
- `assessReactive(input$: AsyncIterable<string>, opts?: AssessReactiveOptions): AsyncIterable<StrengthResult>`
- Throws `PasswordStrengthError(PWSTRENGTH.INVALID_OPTIONS)` on schema
failure; throws `PasswordStrengthError(PWSTRENGTH.INPUT_TOO_LONG)` when
an input exceeds 200 characters.
- `AssessReactiveOptions`: `{ debounceMs?, signal?, assessOptions? }`.
## Implementation Notes
The wrapper schedules each input via `setTimeout(debounceMs)`. A newer
input cancels the pending timer and reschedules. Consumers awaiting the
next iteration are tracked in an internal resolver queue; an emitted
result settles the next resolver or buffers if none is waiting. Abort
clears the pending timer and resolves every waiting consumer with
`{ done: true }`; the iterable then completes cleanly without throwing.
The `ABORTED` error code is registered with the OTel counter on abort so
operators can monitor cancellation rates without inspecting the iterator
itself.

View File

@@ -0,0 +1,70 @@
# `assess` — full-pipeline public API
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The primary public entry point: takes a candidate password, runs every
pattern matcher, picks the lowest-guesses cover via dynamic programming,
computes crack times, scores against Wheeler's bucket boundaries, and
generates feedback strings. Returns a `StrengthResult` value that drops
straight into a UI strength meter.
## Features
- Zod-validated options at the boundary
- 200-character DoS guard (`> 200` throws `INPUT_TOO_LONG`)
- Optional `userInputs` injected as a synthetic ranked dictionary
- Optional `dictionaries`, `graphs`, `l33tTable`, `referenceYear` overrides
- OTel histogram and counter emissions on every call
- Returns wall-clock `calcTimeMs` for observability
## Usage: Basic Example
```ts
import { assess } from "@suaveplan/password-strength";
const result = assess("Password1!");
console.log(result.score, result.guesses);
```
## Usage: Advanced Example
```ts
import { assess, loadDictionaries } from "@suaveplan/password-strength";
const dicts = loadDictionaries({ banned: new Map([["acmecorp", 1]]) });
const result = assess("acmecorp2026!", {
dictionaries: dicts,
userInputs: ["alice@example.com"],
referenceYear: 2026,
});
console.log(result.feedback.warning, result.feedback.suggestions);
```
## API Reference
- `assess(password: string, opts?: AssessOptions): StrengthResult`
- Throws `PasswordStrengthError(PWSTRENGTH.INPUT_TOO_LONG)` when
`password.length > 200`.
- Throws `PasswordStrengthError(PWSTRENGTH.INVALID_OPTIONS)` on
schema-validation failure.
- `AssessOptions`: `{ dictionaries?, userInputs?, graphs?, l33tTable?,
referenceYear?, feedback? }`.
- `StrengthResult`: `{ password, guesses, guessesLog10, score, crackTimes,
sequence, feedback, calcTimeMs }`.
- `MAX_INPUT_LENGTH: 199` — the maximum accepted password length. Inputs
with `length > 199` (i.e. 200 or more) throw `INPUT_TOO_LONG`.
## Implementation Notes
The DoS guard is checked _before_ the Zod parse so an oversized input
never enters the DP. The user-inputs synthesis prepends a `user_inputs`
dictionary keyed by 1-based input order; subsequent calls with the same
`userInputs` produce identical synthetic dictionaries. The pipeline
records every matcher's hit count and the wall-clock duration so
dashboards can correlate slow inputs to specific matcher cohorts. The
`feedback` step receives a partial `StrengthResult` (score and sequence
populated; `feedback` itself empty) so its keyed-string generator can
key off the longest-cover pattern.

View File

@@ -0,0 +1,63 @@
# `de/wikipedia` — German Wikipedia frequency dictionary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frequency-ranked dictionary of the most common German tokens observed in
Wikipedia (CC-BY-SA public-domain frequency lists). Plugs into `assess`
via `loadDictionaries` so German-locale passwords are evaluated against a
locale-appropriate corpus.
## Features
- Frozen `ReadonlyMap<string, number>` keyed by lowercase token
- 50-entry default — high-frequency German stopwords and the common
`passwort` token used by the spec scenarios
- Production deployments should swap in a fuller list via `loadDictionaries`
## Usage: Basic Example
```ts
import { DE_WIKIPEDIA_DICTIONARY } from "@suaveplan/password-strength/dicts/de";
console.log(DE_WIKIPEDIA_DICTIONARY.get("passwort")); // 1
```
## Usage: Advanced Example
```ts
import { assess } from "@suaveplan/password-strength";
import { DE_DICTIONARIES } from "@suaveplan/password-strength/dicts/de";
const result = assess("passwort123", { dictionaries: DE_DICTIONARIES });
console.log(result.score);
```
## API Reference
- `DE_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number>` — frozen
frequency-ranked German Wikipedia dictionary.
## Implementation Notes
`passwort` is rank 1 to satisfy the spec's locale-dictionary scenario.
Production lists should track Wikipedia's published top-30 000 German
tokens, but the bundled subset keeps the package bundle small while
demonstrating the locale-plug-in pattern.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,62 @@
# `english-wikipedia` — top English Wikipedia tokens
## Purpose
Frequency-ranked dictionary of the most common English tokens observed in
Wikipedia. The matcher uses this to flag passwords containing real English
words; a dictionary word with rank 50 contributes `50` guesses to the
cover product, far below the naive character-entropy floor.
## Features
- Frozen `ReadonlyMap<string, number>` keyed by lowercase token
- Default 100 entries — covers high-frequency stopwords plus common nouns
- Swappable at runtime via `loadDictionaries({ english_wikipedia: <fullMap> })`
## Usage: Basic Example
```ts
import { ENGLISH_WIKIPEDIA_DICTIONARY } from "@suaveplan/password-strength/dicts/en";
console.log(ENGLISH_WIKIPEDIA_DICTIONARY.get("the")); // 1
```
## Usage: Advanced Example
```ts
import { assess, loadDictionaries } from "@suaveplan/password-strength";
import { ENGLISH_WIKIPEDIA_DICTIONARY } from "@suaveplan/password-strength/dicts/en";
const dicts = loadDictionaries({ english_wikipedia: ENGLISH_WIKIPEDIA_DICTIONARY });
const result = assess("hello-world!", { dictionaries: dicts });
console.log(result.score);
```
## API Reference
- `ENGLISH_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number>` — frozen
frequency-ranked English Wikipedia dictionary.
## Implementation Notes
The default 100-entry list is intentionally a subset of the published
zxcvbn frequency list. Production deployments should swap in a fuller
list (typically the published top-30 000 tokens). The bundled subset
covers the high-frequency stopwords and common nouns the spec scenarios
exercise; entries are deduplicated by lowercase token so a single insertion
loop over the source data does not corrupt the rank ordering.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,71 @@
# `names` — US Census name dictionaries
## Purpose
Frequency-ranked dictionaries of common US given names (male and female) and
surnames. Names are over-represented in password breach corpora; the
matcher needs three separate dictionaries so feedback can attribute a match
to a name vs a surname vs a generic word.
## Features
- Three frozen `ReadonlyMap<string, number>` dictionaries
- `MALE_NAMES_DICTIONARY`: 20 most common male given names
- `FEMALE_NAMES_DICTIONARY`: 20 most common female given names
- `SURNAMES_DICTIONARY`: 20 most common surnames
- Public-domain US Census source data
## Usage: Basic Example
```ts
import { SURNAMES_DICTIONARY } from "@suaveplan/password-strength/dicts/en";
console.log(SURNAMES_DICTIONARY.get("smith")); // 1
```
## Usage: Advanced Example
```ts
import { loadDictionaries } from "@suaveplan/password-strength";
import {
FEMALE_NAMES_DICTIONARY,
MALE_NAMES_DICTIONARY,
SURNAMES_DICTIONARY,
} from "@suaveplan/password-strength/dicts/en";
const dicts = loadDictionaries({
male_names: MALE_NAMES_DICTIONARY,
female_names: FEMALE_NAMES_DICTIONARY,
surnames: SURNAMES_DICTIONARY,
});
```
## API Reference
- `MALE_NAMES_DICTIONARY: ReadonlyMap<string, number>`
- `FEMALE_NAMES_DICTIONARY: ReadonlyMap<string, number>`
- `SURNAMES_DICTIONARY: ReadonlyMap<string, number>`
## Implementation Notes
Each list is intentionally short to keep the bundle compact; production
deployments may extend any of the three by passing a fuller dictionary to
`loadDictionaries`. The lists are frozen at module load so consumers cannot
accidentally mutate them. The three-way split (male / female / surnames)
preserves zxcvbn's original feedback signal — knowing whether a match is a
given name vs a surname helps the resolver pick the right warning string.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,66 @@
# `passwords` — top-N breach-corpus passwords (English default)
## Purpose
Frequency-ranked dictionary of the most common passwords observed across
public breach corpora (RockYou, HaveIBeenPwned). The matcher uses this to
detect when a candidate password matches a real-world high-frequency token,
giving the cracker a guess count proportional to the token's rank rather
than to its character entropy.
## Features
- Frozen `ReadonlyMap<string, number>` keyed by lowercase token
- Rank ascends from 1 (most common) to N
- 100 entries default — small enough to keep the package bundle modest
- Swappable at runtime via `loadDictionaries({ passwords: <fullMap> })`
## Usage: Basic Example
```ts
import { PASSWORDS_DICTIONARY } from "@suaveplan/password-strength/dicts/en";
console.log(PASSWORDS_DICTIONARY.get("password")); // 1
```
## Usage: Advanced Example
```ts
import { loadDictionaries } from "@suaveplan/password-strength";
import { PASSWORDS_DICTIONARY } from "@suaveplan/password-strength/dicts/en";
// Extend the bundled list with a customer-supplied breach corpus
const extended = new Map(PASSWORDS_DICTIONARY);
for (const [token, rank] of customCorpus) extended.set(token, rank);
const dicts = loadDictionaries({ passwords: extended });
```
## API Reference
- `PASSWORDS_DICTIONARY: ReadonlyMap<string, number>` — frozen frequency-ranked
password dictionary. Keys are lowercase strings; values are 1-based ranks.
## Implementation Notes
The default 100-entry list reflects the top of the published RockYou top-1000
and the HaveIBeenPwned top-100. The spec scenarios reference `password`,
`trustno1`, `qwerty`, `azerty`, and `zxcvbn` so each of those appears in the
default. Production deployments tracking a larger corpus should swap the
dictionary via `loadDictionaries`; the slim default keeps the package small
and the Wikipedia-sized lists optional via `./dicts/<locale>` subpaths.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,60 @@
# `es/wikipedia` — Spanish Wikipedia frequency dictionary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frequency-ranked dictionary of the most common Spanish tokens observed in
Wikipedia. Plugs into `assess` for Spanish-locale strength evaluation.
## Features
- Frozen `ReadonlyMap<string, number>` keyed by lowercase token
- Covers high-frequency Spanish stopwords and common nouns
- Swappable via `loadDictionaries` for a fuller corpus
## Usage: Basic Example
```ts
import { ES_WIKIPEDIA_DICTIONARY } from "@suaveplan/password-strength/dicts/es";
console.log(ES_WIKIPEDIA_DICTIONARY.get("contraseña")); // 1
```
## Usage: Advanced Example
```ts
import { assess } from "@suaveplan/password-strength";
import { ES_DICTIONARIES } from "@suaveplan/password-strength/dicts/es";
const result = assess("contraseña2026", { dictionaries: ES_DICTIONARIES });
console.log(result.score);
```
## API Reference
- `ES_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number>` — frozen
frequency-ranked Spanish Wikipedia dictionary.
## Implementation Notes
`contraseña` is rank 1 in the bundled subset to mirror the German
locale's `passwort` placement. The full Wikipedia top-30 000 list belongs
in production deployments; the bundled subset stays small so the package
bundle does not balloon when consumers import a single locale.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,63 @@
# `fr/wikipedia` — French Wikipedia frequency dictionary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frequency-ranked dictionary of the most common French tokens observed in
Wikipedia. Plugs into `assess` for French-locale strength evaluation. The
default entry list is small to keep the package bundle compact;
production deployments should swap in a fuller list via `loadDictionaries`.
## Features
- Frozen `ReadonlyMap<string, number>` keyed by lowercase token
- Covers high-frequency French stopwords and common nouns
- Swappable at runtime via `loadDictionaries`
## Usage: Basic Example
```ts
import { FR_WIKIPEDIA_DICTIONARY } from "@suaveplan/password-strength/dicts/fr";
console.log(FR_WIKIPEDIA_DICTIONARY.get("de")); // 2
```
## Usage: Advanced Example
```ts
import { assess } from "@suaveplan/password-strength";
import { FR_DICTIONARIES } from "@suaveplan/password-strength/dicts/fr";
const result = assess("motdepasse123", { dictionaries: FR_DICTIONARIES });
console.log(result.score);
```
## API Reference
- `FR_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number>` — frozen
frequency-ranked French Wikipedia dictionary.
## Implementation Notes
The list is intentionally a subset of the full Wikipedia frequency list.
The default entries are diacritic-free where possible to keep matching
against typed passwords robust against keyboard variants; consumers who
need diacritic-preserving matches should preprocess input via a NFC
normaliser before calling `assess`.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,78 @@
# `load-dictionaries` — fresh `DictionarySet` builder
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [wikipedia](./de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- wikipedia — <!-- begin:auto-docs --> _(no submodules…
- [wikipedia](./de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- wikipedia — <!-- begin:auto-…
- [wikipedia](./de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [wikipedia](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_- wikipedia — <!-- begin:auto-docs --> _(no submodule…
- [wikipedia](./de/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [english-wikipedia](./en/english-wikipedia.md) — Frequency-ranked dictionary of the most common English tokens observed in Wikipedia. The matcher uses this to flag pass…
- [names](./en/names.md) — Frequency-ranked dictionaries of common US given names (male and female) and surnames. Names are over-represented in pa…
- [passwords](./en/passwords.md) — Frequency-ranked dictionary of the most common passwords observed across public breach corpora (RockYou, HaveIBeenPwned…
- [es](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./es/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [fr](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./fr/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [ru](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./ru/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [zh-Hans](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_ - [wikipedia](./zh-Hans/wikipedia.md) — <!-- begin:auto-docs --> _(no submodules)_- [load-dictionaries](./load-dictionaries.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Builds a fresh `DictionarySet` value from a caller-supplied partial map,
merged over the bundled English defaults. The bundled defaults are never
mutated. Callers can pass `assess(pwd, { dictionaries: <set> })` to swap the
dictionary set for one call without leaking state to subsequent calls.
## Features
- Fresh return value per call — two calls return distinct object identities
- Frozen output — accidental consumer mutation is rejected
- Validates every entry has a positive numeric rank and a string token
- Throws `PWSTRENGTH.DICTIONARY_INVALID` on invalid entries
## Usage: Basic Example
```ts
import { loadDictionaries } from "@suaveplan/password-strength";
const dicts = loadDictionaries({ passwords: new Map([["custom", 1]]) });
console.log(dicts.passwords?.get("custom")); // 1
```
## Usage: Advanced Example
```ts
import { assess, loadDictionaries } from "@suaveplan/password-strength";
// Merge customer-specific banned-word list into the English defaults
const banned = new Map<string, number>([["acmecorp", 1], ["acme-internal", 2]]);
const dicts = loadDictionaries({ banned });
const result = assess("acmecorp-2026", { dictionaries: dicts });
console.log(result.score);
```
## API Reference
- `loadDictionaries(dicts: PartialDictionaryMap): DictionarySet`
- Returns a fresh `DictionarySet` (frozen). The bundled English defaults
appear under the canonical names (`passwords`, `english_wikipedia`,
`male_names`, `female_names`, `surnames`); the supplied dictionaries
override any default with the same name.
- Throws `PasswordStrengthError(PWSTRENGTH.DICTIONARY_INVALID)` when any
entry has a non-string token or a non-positive rank.
## Implementation Notes
The merge uses object spread, so caller-supplied names override defaults
with the same key. The function does not deep-clone the entry `Map`s — they
are already frozen by the dictionary modules — but it does freeze the
outer record so consumers cannot add or remove named dictionaries after
construction. Rank validation rejects `0`, negative values, and
non-finite numbers; the spec scenario asserts the `0` case throws.

View File

@@ -0,0 +1,60 @@
# `ru/wikipedia` — Russian Wikipedia frequency dictionary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frequency-ranked dictionary of the most common Russian tokens observed in
Wikipedia. Plugs into `assess` for Russian-locale strength evaluation.
## Features
- Frozen `ReadonlyMap<string, number>` keyed by Cyrillic token
- Covers high-frequency Russian stopwords and common nouns
- Swappable via `loadDictionaries`
## Usage: Basic Example
```ts
import { RU_WIKIPEDIA_DICTIONARY } from "@suaveplan/password-strength/dicts/ru";
console.log(RU_WIKIPEDIA_DICTIONARY.get("пароль")); // 1
```
## Usage: Advanced Example
```ts
import { assess } from "@suaveplan/password-strength";
import { RU_DICTIONARIES } from "@suaveplan/password-strength/dicts/ru";
const result = assess("пароль123", { dictionaries: RU_DICTIONARIES });
console.log(result.score);
```
## API Reference
- `RU_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number>` — frozen
frequency-ranked Russian Wikipedia dictionary.
## Implementation Notes
The bundled subset is intentionally compact; production deployments should
swap in a fuller list. Russian tokens are matched against the lowercased
form of the input, so `assess` will pick up `ПАРОЛЬ` as well via the
uppercase variation factor in `estimateGuesses`.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,64 @@
# `zh-Hans/wikipedia` — Simplified Chinese frequency dictionary
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Frequency-ranked dictionary of the most common Simplified Chinese tokens
observed in Wikipedia. Plugs into `assess` for Simplified Chinese-locale
strength evaluation. Tokens are short surface forms (single CJK ideogram
or short word) so the matcher can recognise common prefixes in mixed
input.
## Features
- Frozen `ReadonlyMap<string, number>` keyed by surface-form token
- Covers high-frequency CJK ideograms including `密码` (password)
- Swappable via `loadDictionaries`
## Usage: Basic Example
```ts
import { ZH_HANS_WIKIPEDIA_DICTIONARY } from "@suaveplan/password-strength/dicts/zh-Hans";
console.log(ZH_HANS_WIKIPEDIA_DICTIONARY.get("密码")); // 1
```
## Usage: Advanced Example
```ts
import { assess } from "@suaveplan/password-strength";
import { ZH_HANS_DICTIONARIES } from "@suaveplan/password-strength/dicts/zh-Hans";
const result = assess("密码123", { dictionaries: ZH_HANS_DICTIONARIES });
console.log(result.score);
```
## API Reference
- `ZH_HANS_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number>` — frozen
frequency-ranked Simplified Chinese Wikipedia dictionary.
## Implementation Notes
The default subset is intentionally small; CJK tokens are surface forms
rather than word segments, so a richer production list typically uses
segmenter-aware frequencies. The matcher's `String.prototype.slice`
operates on UTF-16 code units, which means multi-code-unit CJK characters
are matched character-by-character as their canonical surface form.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,76 @@
# `error-codes` — stable error code registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares the registry of stable error codes raised by the package and
the `PasswordStrengthError` base class consumers branch on. Codes are
registered via `defineCodes` from `@suaveplan/error` so the literal-union
`PasswordStrengthErrorCode` propagates through every throw site without
hand-curated string-literal duplication.
## Features
- Six frozen codes covering the public throw paths
- `PasswordStrengthError extends SuaveplanError<PasswordStrengthErrorCode>`
- `passwordStrengthError` factory: prefixes the code on the message and
ignores non-Error `cause` values
- Codes-first design — consumers branch on `err.code`, never message text
## Usage: Basic Example
```ts
import {
PasswordStrengthError,
PWSTRENGTH_CODES,
passwordStrengthError,
} from "@suaveplan/password-strength";
try {
// ...
} catch (e) {
if (e instanceof PasswordStrengthError && e.code === PWSTRENGTH_CODES.INPUT_TOO_LONG) {
// handle DoS-guard rejection
}
}
```
## Usage: Advanced Example
```ts
import { passwordStrengthError, PWSTRENGTH_CODES } from "@suaveplan/password-strength";
function rejectIfWeak(score: number) {
if (score < 3) {
throw passwordStrengthError(
PWSTRENGTH_CODES.INPUT_TOO_LONG,
`score ${score} below required 3`,
{ cause: new Error("policy gate") },
);
}
}
```
## API Reference
- `PWSTRENGTH_CODES` — frozen object with keys: `INPUT_TOO_LONG`,
`INVALID_OPTIONS`, `DICTIONARY_INVALID`, `GRAPH_INVALID`,
`RESOLVER_FAILED`, `ABORTED`.
- `PasswordStrengthErrorCode` — literal-typed union of every registered
code.
- `PasswordStrengthError` — Error subclass carrying the typed `code`.
- `passwordStrengthError(code, message, options?)` — factory; preserves
`Error`-typed `cause` and prefixes the message with the code string.
## Implementation Notes
The factory deliberately silently drops non-`Error` `cause` values rather
than wrapping them — this prevents accidental serialisation of arbitrary
JavaScript values through `JSON.stringify(err)` (which would otherwise
leak details on error pages). The codes are also surfaced via the OTel
`pwstrength.errors_total{code}` counter so operators can monitor
distribution shifts without scraping logs. `ABORTED` is recorded against
the counter from the `assessReactive` abort handler even though it never
throws to consumers; this lets dashboards count cancellation rates.

View File

@@ -0,0 +1,69 @@
# `feedback` — keyed-string feedback generator
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Renders a structured `{ warning, suggestions[] }` value from a
`StrengthResult`'s longest-cover pattern. The warning string is selected
by message key from a built-in English map; consumers integrate with
their existing i18n stack by passing a `resolver` callback that receives
the key and an optional params bag.
## Features
- Built-in English message map (no runtime i18n dependency)
- Resolver bridge accepts any function `(key, params?) => string`
- Resolver throws bubble up as `PWSTRENGTH.RESOLVER_FAILED` with `cause`
- Warning key is selected from the longest-cover pattern in the sequence
- Suggestion keys are emitted for `score < 3` (omitted at 3 / 4)
- `personalInfo` key surfaces when the longest match is a `user_inputs` hit
## Usage: Basic Example
```ts
import { feedback } from "@suaveplan/password-strength";
const result = {
password: "password",
guesses: 1,
guessesLog10: 0,
score: 0,
sequence: [{ pattern: "dictionary", i: 0, j: 7, token: "password", matchedWord: "password", rank: 1, dictionaryName: "passwords", reversed: false, l33t: false }],
} as const;
const fb = feedback(result);
console.log(fb.warning);
```
## Usage: Advanced Example
```ts
import { feedback } from "@suaveplan/password-strength";
const fb = feedback(result, {
resolver: (key) => myI18nLib.translate(key, { locale: "fr" }),
});
console.log(fb.warning);
```
## API Reference
- `feedback(result: FeedbackResult, opts?: FeedbackOptions): Feedback`
- Throws `PasswordStrengthError(PWSTRENGTH.INVALID_OPTIONS)` on Zod failure.
- Throws `PasswordStrengthError(PWSTRENGTH.RESOLVER_FAILED)` when the
caller-supplied resolver throws.
- `Feedback`: `{ warning: string; suggestions: string[] }`.
- `FeedbackOptions`: `{ resolver?: MessageResolver; locale?: string }`.
- `FeedbackResult` — subset of `StrengthResult` consumed by `feedback`.
## Implementation Notes
The warning key is selected by walking the sequence to find the match with
the longest span; if the sequence is empty the empty warning key is used.
For dictionary matches, the key suffix reflects the bucket the rank falls
into (`top10`, `top100`, `common`, `word`). The English map covers every
emitted key so unresolved keys never produce empty strings; resolver
implementations are free to return undefined for unknown keys and the
package will substitute the English string.

View File

@@ -0,0 +1,66 @@
# `azerty` — AZERTY keyboard adjacency graph
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Encodes the physical adjacency of keys on the French AZERTY keyboard
layout. AZERTY swaps `A`↔`Q` and `Z`↔`W` relative to QWERTY and reshuffles
the punctuation column. Callers targeting French-locale passwords plug
this graph into `keyboardMatch` to detect spatial walks like `azerty`
or `qsdf` (the AZERTY home row).
## Features
- Frozen `KeyboardGraph` covering the AZERTY alphanumeric block
- Slot order matches QWERTY for consumer compatibility
- Per-key adjacency includes the unshifted and shifted character
## Usage: Basic Example
```ts
import { AZERTY_GRAPH } from "@suaveplan/password-strength/graphs/azerty";
console.log(AZERTY_GRAPH.a); // [null, "1!", "2é", "zZ", "qQ", null]
```
## Usage: Advanced Example
```ts
import { keyboardMatch } from "@suaveplan/password-strength/matchers/keyboard";
import { AZERTY_GRAPH } from "@suaveplan/password-strength/graphs/azerty";
const matches = keyboardMatch("azerty", { graphs: { azerty: AZERTY_GRAPH } });
console.log(matches);
```
## API Reference
- `AZERTY_GRAPH: KeyboardGraph` — frozen `Record<string, ReadonlyArray<string | null>>`
mapping each AZERTY key to its six adjacency slots.
## Implementation Notes
The shifted character for digit keys uses the standard French punctuation
mapping (`2é`, `3"`, `4'`, etc.), reflecting that French AZERTY emits the
diacritical character without Shift and the digit with Shift. Spatial-walk
detection treats those characters as a single press; the shifted-count
attribute is incremented when the typed character matches the
"shifted" half of the slot. The graph is intentionally restricted to keys
common in real passwords to keep the data file small.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,65 @@
# `dvorak` — DVORAK keyboard adjacency graph
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Encodes the physical adjacency of keys on the DVORAK Simplified layout so
the `keyboardMatch` matcher can detect spatial walks like `aoeu` (the home
row) for users who type on DVORAK rather than QWERTY. Driven by Wheeler
2016 section 4.5 keyboard-walk model.
## Features
- Frozen `KeyboardGraph` covering the alphanumeric block
- Slot order matches QWERTY (left, top-left, top-right, right, bottom-right,
bottom-left) so consumers can substitute graphs without code changes
- Each adjacency string encodes the unshifted and shifted character at that key
## Usage: Basic Example
```ts
import { DVORAK_GRAPH } from "@suaveplan/password-strength/graphs/dvorak";
console.log(DVORAK_GRAPH.a); // [null, "'\"", ",<", "oO", ";:", null]
```
## Usage: Advanced Example
```ts
import { keyboardMatch } from "@suaveplan/password-strength/matchers/keyboard";
import { DVORAK_GRAPH } from "@suaveplan/password-strength/graphs/dvorak";
// Detect DVORAK home-row walks
const matches = keyboardMatch("aoeu", { graphs: { dvorak: DVORAK_GRAPH } });
console.log(matches);
```
## API Reference
- `DVORAK_GRAPH: KeyboardGraph` — frozen `Record<string, ReadonlyArray<string | null>>`
mapping each DVORAK key to its six adjacency slots.
## Implementation Notes
The matcher treats DVORAK exactly like QWERTY for the purposes of run length,
turns, and shifted-count counting. Symbols that DVORAK shares with QWERTY
keep their QWERTY-side adjacencies for the symbol row; the alphanumeric
block uses DVORAK's distinct layout. Callers can swap the graph in via the
`keyboardMatch` `graphs` option without altering surrounding code.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,69 @@
# `qwerty` — QWERTY keyboard adjacency graph
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Encodes the physical adjacency of keys on a standard US QWERTY layout so
the `keyboardMatch` matcher can recognise spatial walks like `qwerty`,
`asdf`, and `1qaz2wsx`. The graph drives the keyboard-walk guess estimate
in Wheeler 2016 section 4.5. The encoding mirrors the upstream zxcvbn
reference implementation's `adjacency_graphs.coffee`.
## Features
- Complete coverage of the alphanumeric, symbol, and shifted-symbol rows
- Adjacency slots in zxcvbn's canonical order (left, top-left, top-right,
right, bottom-right, bottom-left)
- Each slot encodes both the unshifted and shifted character at that key
- Frozen at module load — safe against accidental mutation
## Usage: Basic Example
```ts
import { QWERTY_GRAPH } from "@suaveplan/password-strength/graphs/qwerty";
console.log(QWERTY_GRAPH.q); // [null, "1!", "2@", "wW", "aA", null]
```
## Usage: Advanced Example
```ts
import { keyboardMatch } from "@suaveplan/password-strength/matchers/keyboard";
import { QWERTY_GRAPH } from "@suaveplan/password-strength/graphs/qwerty";
import { DVORAK_GRAPH } from "@suaveplan/password-strength/graphs/dvorak";
const matches = keyboardMatch("qwerty", {
graphs: { qwerty: QWERTY_GRAPH, dvorak: DVORAK_GRAPH },
});
console.log(matches);
```
## API Reference
- `QWERTY_GRAPH: KeyboardGraph` — frozen `Record<string, ReadonlyArray<string | null>>`
mapping each key to its six adjacency slots.
## Implementation Notes
The matcher checks both characters of each slot when classifying a key
press: matching against the second character flags the press as
shift-required (`shiftedCount += 1`). Slots that point off the edge of the
keyboard are `null`. The graph deliberately preserves the upstream slot
order so callers porting from `zxcvbn` consume the same data layout.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,76 @@
# `validate-graph` — keyboard graph integrity guard
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [azerty](./azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- azerty — <!-- begin:auto-docs --> _(no submodules)_…
- [azerty](./azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- [azerty](./azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- azerty — <!-- begin:auto-doc…
- [azerty](./azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_- azerty — <!-- begin:auto-docs --> _(no submodules)_…
- [azerty](./azerty/azerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dvorak](./dvorak/dvorak.md) — <!-- begin:auto-docs --> _(no submodules)_- [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_ - [qwerty](./qwerty/qwerty.md) — <!-- begin:auto-docs --> _(no submodules)_- [validate-graph](./validate-graph.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Validates that a user-supplied keyboard adjacency graph is well-formed
before the `keyboardMatch` matcher walks it. A malformed graph would
either produce nonsensical matches (self-loops) or cause inconsistent
guess estimates across users typing the same word.
## Features
- Detects self-adjacency (a key listed as adjacent to itself)
- Detects asymmetric edges (`a` lists `b` but `b` does not list `a`)
- Throws `PasswordStrengthError` with code `PWSTRENGTH.GRAPH_INVALID`
- Pure data validation; no side effects
## Usage: Basic Example
```ts
import { validateGraph } from "@suaveplan/password-strength";
import { QWERTY_GRAPH } from "@suaveplan/password-strength/graphs/qwerty";
validateGraph("qwerty", QWERTY_GRAPH); // returns; throws if malformed
```
## Usage: Advanced Example
```ts
import { validateGraph, PWSTRENGTH_CODES, PasswordStrengthError } from "@suaveplan/password-strength";
const broken = { q: ["q"] };
try {
validateGraph("broken", broken);
} catch (e) {
if (e instanceof PasswordStrengthError && e.code === PWSTRENGTH_CODES.GRAPH_INVALID) {
console.log("graph rejected");
}
}
```
## API Reference
- `validateGraph(name: string, graph: KeyboardGraph): void`
- Throws `PasswordStrengthError` with code `PWSTRENGTH.GRAPH_INVALID` when
the graph contains a self-loop or an asymmetric edge. The error message
names the offending key for fast debugging.
## Implementation Notes
The symmetry check is one-directional per pair — if `a` claims adjacency to
`b`, then `b`'s adjacency slot list must contain `a` in at least one of its
slots' character pairs (covering both the shifted and unshifted halves).
Slots set to `null` are skipped. The walk is `O(n)` over the number of
edges in the graph; running it on a 60-key graph is negligible.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,62 @@
# `date` — date pattern matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Identifies date-shaped substrings inside a password per Wheeler 2016
section 3.4. Dates are extremely common in real passwords (birthdays,
anniversaries, account-creation years) and need a dedicated matcher so
the scorer can apply a far smaller guesses count than the naive entropy
upper bound for the same span.
## Features
- Separator forms: `mm/dd/yyyy`, `dd-mm-yy`, `yyyy.mm.dd`, `yyyy mm dd`
- Separator characters: `/`, `-`, `.`, space
- No-separator forms: `mmyy`, `mmddyy`, `ddmmyy`, `yymmdd`, `mddyyyy`,
`yyyymdd`, `mmddyyyy`, `yyyymmdd`, `ddmmyyyy`
- Year range `1900 ≤ year ≤ referenceYear + 50`
- Ambiguous parses tie-break toward the year closest to `referenceYear`
## Usage: Basic Example
```ts
import { dateMatch } from "@suaveplan/password-strength/matchers/date";
const matches = dateMatch("11/22/1985");
console.log(matches[0]); // { year: 1985, month: 11, day: 22, separator: "/" }
```
## Usage: Advanced Example
```ts
import { dateMatch } from "@suaveplan/password-strength/matchers/date";
// Fix the reference year for deterministic tests
const matches = dateMatch("login010199!", { referenceYear: 2026 });
for (const m of matches) {
console.log(m.token, m.year, m.month, m.day);
}
```
## API Reference
- `dateMatch(password: string, opts?: DateMatchOptions): DateMatch[]`
- `opts.referenceYear`: anchor year used for both the upper bound (`ref + 50`)
and the tie-break preference. Defaults to the current UTC year.
- `DateMatchOptions`: `{ referenceYear?: number }`
- `SEPARATORS: readonly string[]`: exported for callers that want to inspect
which separators the matcher accepts.
## Implementation Notes
The separator path uses a non-overlapping regex with a back-reference to
ensure the same separator separates each pair. The no-separator path
exhaustively tries every 4-to-8-digit substring against the canonical
zxcvbn forms and validates each parse against the `[1900, ref + 50]`
year window. When a substring admits multiple valid parses, the matcher
picks the one whose `year` is closest in absolute distance to
`referenceYear`. Two-digit years are expanded to the closer of `1900 + yy`
and `2000 + yy`.

View File

@@ -0,0 +1,87 @@
# `dictionary` — dictionary pattern matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - l33t-table — Provides the default character → lette…
- [l33t-table](./l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- l33t-table — Provides the default character → lette…
- [l33t-table](./l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- l33t-table — Provides the default character → lette…
- [l33t-table](./l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- l33t-table — Provides the default character → lette…
- [l33t-table](./l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [l33t-table](./l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
## Purpose
Implements the dictionary stage of the zxcvbn pattern matcher per Wheeler
2016 §3.1. For each supplied named dictionary, the matcher finds every
substring of the password that exists in the dictionary as a plain token,
reversed token, or l33t-substituted token. Each hit becomes a discrete
`DictionaryMatch` record consumed by `mostGuessableMatchSequence` to pick the
lowest-`Π guesses` cover of the password.
## Features
- Frequency-ranked lookup against any number of named dictionaries
- Reverse-string lookup (`drowssap` → `password`)
- L33t enumeration over a pluggable substitution table
- Emits one match per (l33t, reversed, dictionary) tuple
- Never mutates the supplied dictionaries (frozen-safe)
- Pure-data; runs unmodified in any tier
## Usage: Basic Example
```ts
import { dictionaryMatch } from "@suaveplan/password-strength/matchers/dictionary";
const dicts = { passwords: new Map([["password", 1]]) };
const matches = dictionaryMatch("password", dicts);
console.log(matches[0]); // { pattern: "dictionary", i: 0, j: 7, … }
```
## Usage: Advanced Example
```ts
import {
DEFAULT_L33T_TABLE,
dictionaryMatch,
} from "@suaveplan/password-strength/matchers/dictionary";
const dicts = {
passwords: new Map([["password", 1]]),
english_wikipedia: new Map([["dog", 42]]),
};
// Custom l33t table extending the default — adds "+" -> "t" alongside "+" -> "t"
const customTable = { ...DEFAULT_L33T_TABLE, t: ["+", "7", "†"] };
for (const m of dictionaryMatch("p4ssw0rd", dicts, customTable)) {
console.log(m.matchedWord, m.rank, m.l33t ? m.sub : "plain");
}
```
## API Reference
- `dictionaryMatch(password: string, dictionaries: DictionarySet, l33tTable?: L33tTable): DictionaryMatch[]`
- Returns every substring match per dictionary. The `l33tTable` argument
defaults to `DEFAULT_L33T_TABLE`. The function never mutates either
argument; both can be safely frozen by the caller.
## Implementation Notes
Plain matches walk every `[i, j]` substring under each dictionary's `get`,
which is `O(n^2 * d)` for password length `n` and number of dictionaries
`d`. The l33t path enumerates the cartesian product of relevant substitute
characters present in the password, which Wheeler §3.1 bounds by a few
thousand combinations on real passwords — far below the DP cost downstream.
Reversed matches reuse the plain matcher's logic on a reversed copy of the
password and the indices are remapped back to the original positions.
Matches whose transformed substring equals the original (no real
substitution applied) are filtered out to avoid emitting duplicate plain
matches under the l33t flag.

View File

@@ -0,0 +1,71 @@
# `l33t-table` — default substitution table
## Purpose
Provides the default character → letter substitution table used by the
`dictionaryMatch` matcher to recognise l33t-encoded variants of dictionary
tokens (e.g. `p4ssw0rd` for `password`). The table follows Wheeler §3.1 and
mirrors the `dropbox/zxcvbn` reference impl's `data/l33t_table.coffee`. The
companion `inverseL33tTable` utility flips the table for the matcher's enumeration
pass — given a substitute character, it returns the set of letters that character
could represent.
## Features
- Frozen default table covering twelve high-frequency substitutions
- Inverse lookup helper for matcher enumeration
- Pure data; safe to import in any tier (no runtime deps)
- Caller-pluggable: `assess` accepts an alternative table via options
## Usage: Basic Example
```ts
import { DEFAULT_L33T_TABLE, inverseL33tTable } from "@suaveplan/password-strength/matchers/dictionary";
const inverse = inverseL33tTable(DEFAULT_L33T_TABLE);
console.log(inverse["4"]); // ["a"]
console.log(inverse["1"]); // ["i", "l"]
```
## Usage: Advanced Example
```ts
import { DEFAULT_L33T_TABLE, inverseL33tTable, dictionaryMatch } from "@suaveplan/password-strength/matchers/dictionary";
const customTable = {
...DEFAULT_L33T_TABLE,
a: ["4", "@", "/-\\"],
};
const matches = dictionaryMatch("p4ssword", { passwords: new Map([["password", 1]]) }, customTable);
console.log(matches);
```
## API Reference
- `DEFAULT_L33T_TABLE: L33tTable` — frozen default substitution map.
- `inverseL33tTable(table: L33tTable): Record<string, string[]>` — return the
inverse map of substitute character → array of letters it can stand in for.
## Implementation Notes
The table is intentionally conservative: every substitute character maps to
real letters that appear in the breach-corpus literature. Adding more
substitutions inflates the enumeration cost quadratically per password
length, so callers who extend the table should benchmark against
`assess_dictionary_heavy`. The frozen `Object.freeze` wrapper prevents
inadvertent mutation by consumers.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,62 @@
# `keyboard` — keyboard adjacency matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Detects spatial walks across a physical keyboard layout (QWERTY, DVORAK,
AZERTY, or any caller-supplied adjacency graph) per Wheeler 2016 section
3.5. A password like `qwerty` traces a straight horizontal walk along the
top row; the matcher reports its length, the number of times the walker
changed direction, and how many shift-required presses it contained — all
inputs to the keyboard guess estimate in section 4.5.
## Features
- Pluggable graph map via the `graphs` option
- Counts both `turns` (direction changes) and `shiftedCount` (capital /
symbol presses)
- Default graph is QWERTY; alternative layouts ship under
`graphs/{dvorak,azerty}` subpaths
- Rejects malformed graphs (self-adjacency or asymmetric edges) via
`validateGraph` → `PWSTRENGTH.GRAPH_INVALID`
## Usage: Basic Example
```ts
import { keyboardMatch } from "@suaveplan/password-strength/matchers/keyboard";
const matches = keyboardMatch("qwerty");
console.log(matches[0]); // { graph: "qwerty", turns: 0, shiftedCount: 0, … }
```
## Usage: Advanced Example
```ts
import { keyboardMatch } from "@suaveplan/password-strength/matchers/keyboard";
import { DVORAK_GRAPH } from "@suaveplan/password-strength/graphs/dvorak";
const matches = keyboardMatch("aoeuhtns", { graphs: { dvorak: DVORAK_GRAPH } });
for (const m of matches) {
console.log(m.token, m.graph, m.turns, m.shiftedCount);
}
```
## API Reference
- `keyboardMatch(password: string, opts?: KeyboardMatchOptions): KeyboardMatch[]`
- `opts.graphs`: `Record<string, KeyboardGraph>` — override the default
QWERTY-only graph set. Each graph is validated before use.
- `KeyboardMatchOptions`: `{ graphs?: KeyboardGraphMap }`
## Implementation Notes
Each walk advances character by character; a transition is accepted iff the
target character appears in the current key's adjacency slots (six slots
per key, encoding the six surrounding keys on a staggered layout). A
direction change increments `turns`; a press whose target matches the
shifted half of the slot increments `shiftedCount`. The walk continues
greedily until the chain breaks; the matcher then restarts at the
following index. Graph validation rejects malformed graphs upfront so the
matcher never produces self-loops or asymmetric walks.

View File

@@ -0,0 +1,73 @@
# `regex` — regex pattern matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Recognises substrings matching a set of regex patterns per Wheeler 2016
section 3.6. The default registry covers a `recent_year` pattern matching
`19xx` and `20xx`. Callers extend the registry to flag additional patterns
(hex strings, phone-number shapes, project-specific identifiers) without
forking the scorer.
## Features
- Default `recent_year` regex matches `19xx` and `20xx`
- Registry-pluggable: callers can register their own named regexes
- Global-flag enforced internally so every occurrence is reported
- Every match carries the full `RegExpMatchArray` for downstream inspection
## Usage: Basic Example
```ts
import { regexMatch } from "@suaveplan/password-strength/matchers/regex";
const matches = regexMatch("login1985");
console.log(matches[0].regexName); // "recent_year"
console.log(matches[0].token); // "1985"
```
## Usage: Advanced Example
```ts
import { regexMatch } from "@suaveplan/password-strength/matchers/regex";
const matches = regexMatch("token=ABCDEF", { regexes: { hex: /[0-9A-F]+/g } });
for (const m of matches) {
console.log(m.regexName, m.token);
}
```
## API Reference
- `regexMatch(password: string, opts?: RegexMatchOptions): RegexMatch[]`
- `opts.regexes`: `Record<string, RegExp>` — the registry. Defaults to
`DEFAULT_REGEXES`.
- `DEFAULT_REGEXES: Readonly<Record<string, RegExp>>` — frozen default
registry (currently `recent_year`).
- `RegexMatchOptions`: `{ regexes?: Record<string, RegExp> }`
## Implementation Notes
The matcher copies each registered regex with the `g` flag forced on so a
global walk over the password emits every occurrence. The walk uses
`String.prototype.matchAll` rather than mutating `lastIndex` so zero-width
matches do not loop. The full `RegExpMatchArray` is preserved on each
output record so callers can read capture groups when their regex uses
them.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,61 @@
# `repeat` — repeated-chunk pattern matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Finds every maximal substring composed of a repeating chunk (e.g. `aaaa`,
`abcabc`, `babababa`) per Wheeler 2016 section 3.3. The chunk's strength is
estimated by recursively running `mostGuessableMatchSequence` on the chunk
itself with the same dictionary set — a password like `passwordpassword`
gets the dictionary's rank-1 strength times the repeat-count factor.
## Features
- Detects single-character and multi-character repeats
- Picks the longer of greedy and lazy chunk candidates so the matcher does
not prefer `aa` four times over `aaaa` once
- Recursive `baseGuesses` via `mostGuessableMatchSequence`
- Carries the recursive `baseMatches` so consumers can inspect the chunk's
own cover
## Usage: Basic Example
```ts
import { repeatMatch } from "@suaveplan/password-strength/matchers/repeat";
const matches = repeatMatch("aaaa", {});
console.log(matches[0].baseToken); // "a"
console.log(matches[0].repeatCount); // 4
```
## Usage: Advanced Example
```ts
import { repeatMatch } from "@suaveplan/password-strength/matchers/repeat";
const dicts = { passwords: new Map([["password", 1]]) };
const matches = repeatMatch("passwordpassword", dicts);
for (const m of matches) {
console.log(m.baseToken, m.repeatCount, m.baseGuesses);
}
```
## API Reference
- `repeatMatch(password: string, dictionaries: DictionarySet): RepeatMatch[]`
- Returns every maximal repeat-of-chunk substring. The dictionary set is
threaded into the chunk's recursive scoring so dictionary words appearing
inside the chunk inherit their rank.
## Implementation Notes
The matcher runs both a greedy and a lazy chunk regex from each candidate
start position and picks whichever produces the longer overall repeat. Greedy
finds the longest possible chunk that still repeats at least once; lazy finds
the smallest. Picking the longer span avoids over-fragmenting a real repeat
into two adjacent shorter ones with the same characters. The recursive scoring
call uses the dictionary and sequence matchers — recursive keyboard or date
matching inside a chunk is rare in practice and intentionally excluded for
performance under the 200-character DoS guard.

View File

@@ -0,0 +1,71 @@
# `sequence` — alphanumeric sequence matcher
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Detects ascending and descending runs of consecutive characters within a single
alphabet class (lowercase letters, uppercase letters, decimal digits) per
Wheeler §3.2. A run of `abcd` carries far less entropy than four random
letters and the scorer needs an explicit pattern record to compute the right
guesses count.
## Features
- Lower, upper, and digit alphabets
- Both directions (ascending `1234`, descending `9876`)
- Minimum length 2
- Each match carries `sequenceSpace` (size of the alphabet) and `ascending`
## Usage: Basic Example
```ts
import { sequenceMatch } from "@suaveplan/password-strength/matchers/sequence";
const matches = sequenceMatch("abcd");
console.log(matches[0].ascending); // true
console.log(matches[0].sequenceName); // "lower"
```
## Usage: Advanced Example
```ts
import { sequenceMatch } from "@suaveplan/password-strength/matchers/sequence";
// Mixed runs are reported separately
for (const m of sequenceMatch("abc987XYZ")) {
console.log(m.token, m.sequenceName, m.ascending);
}
// abc lower true
// 987 digits false
// XYZ upper true
```
## API Reference
- `sequenceMatch(password: string): SequenceMatch[]` — return every maximal
ascending or descending run of length ≥ 2 within a single alphabet class.
## Implementation Notes
The walk is single-pass: at each index, the matcher checks the delta between
adjacent characters; if it is `+1` or `-1` within the same alphabet, the
matcher extends the run greedily until the delta breaks or the alphabet
changes. Runs of length 1 are filtered out. The algorithm is `O(n)` over the
password length and emits each maximal run exactly once.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,88 @@
# `matchers/types` — cross-matcher shared type definitions
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [date](./date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [l33t-table](./dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_ - [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_ - [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_ - [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./types.md) — <!-- begin:auto-docs --> _(no submodules)_- date — <!-- begin:auto-docs --> _(no submodules)_ <!…
- [date](./date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [l33t-table](./dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./types.md) — <!-- begin:auto-docs --> _(no submodules)_- [date](./date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [l33t-table](./dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./types.md) — <!-- begin:auto-docs --> _(no submodules)_- date — <!-- begin:auto-docs…
- [date](./date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_- l33t-table — Provides the default character → lette…
- [l33t-table](./dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./types.md) — <!-- begin:auto-docs --> _(no submodules)_- date — <!-- begin:auto-docs --> _(no submodules)_ <…
- [date](./date/date.md) — <!-- begin:auto-docs --> _(no submodules)_- [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_ - [dictionary](./dictionary/dictionary.md) — <!-- begin:auto-docs --> _(no submodules)_ - [l33t-table](./dictionary/l33t-table.md) — Provides the default character → letter substitution table used by the `dictionaryMatch` matcher to recognise l33t-enco…
- [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_ - [keyboard](./keyboard/keyboard.md) — <!-- begin:auto-docs --> _(no submodules)_- [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_ - [regex](./regex/regex.md) — <!-- begin:auto-docs --> _(no submodules)_- [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_ - [repeat](./repeat/repeat.md) — <!-- begin:auto-docs --> _(no submodules)_- [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_ - [sequence](./sequence/sequence.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Declares the cross-matcher discriminated-union types and shared shapes
consumed by every pattern matcher, the dynamic-programming scorer, the
feedback generator, and the public `assess` API. Centralising these types
in one module avoids circular barrels and gives the schema package a
single canonical source for `MatchSchema`'s discriminator.
## Features
- `Match` discriminated union over six pattern shapes
- Per-pattern record types (`DictionaryMatch`, `SequenceMatch`,
`RepeatMatch`, `DateMatch`, `KeyboardMatch`, `RegexMatch`)
- `DictionarySet`, `RankedDictionary`, `PartialDictionaryMap`
- `KeyboardGraph`, `KeyboardGraphMap` for the spatial matcher
- `L33tTable` for the dictionary matcher
- No runtime code; types-only
## Usage: Basic Example
```ts
import type { DictionaryMatch, Match } from "@suaveplan/password-strength";
function isDictionary(m: Match): m is DictionaryMatch {
return m.pattern === "dictionary";
}
```
## Usage: Advanced Example
```ts
import type { DictionarySet, RankedDictionary } from "@suaveplan/password-strength";
const passwords: RankedDictionary = new Map([
["password", 1],
["letmein", 2],
]);
const set: DictionarySet = { passwords };
```
## API Reference
- `DictionaryMatch` — dictionary hit; carries `matchedWord`, `rank`,
`dictionaryName`, `reversed`, `l33t`, and optional `sub` map.
- `SequenceMatch` — alphanumeric run; carries `sequenceName`,
`sequenceSpace`, and `ascending`.
- `RepeatMatch` — repeated chunk; carries `baseToken`, `baseGuesses`,
`repeatCount`, and recursive `baseMatches`.
- `DateMatch` — parseable date; carries `year`, `month`, `day`, and
`separator` (empty string for no-separator forms).
- `KeyboardMatch` — spatial walk; carries `graph` (name), `turns`,
`shiftedCount`.
- `RegexMatch` — registered regex hit; carries `regexName` and the full
`RegExpMatchArray`.
- `Match` — discriminated union over all six.
- `DictionarySet`, `RankedDictionary`, `PartialDictionaryMap`,
`KeyboardGraph`, `KeyboardGraphMap`, `L33tTable` — shared data types.
## Implementation Notes
Every match record carries the `[i, j]` start and end positions plus the
matched `token` so downstream consumers can render highlights without
recomputing offsets. The `RepeatMatch` retains the recursive `baseMatches`
to support nested feedback strings (e.g. "this is a repeated dictionary
word") without re-running the matcher. The discriminator field is named
`pattern` to match the upstream `zxcvbn` reference impl exactly.

View File

@@ -0,0 +1,72 @@
# `metrics` — OTel observability surface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Exports the OTel histogram and counter instruments emitted by the package's
public-facing operations. Each instrument is keyed to a low-cardinality
attribute so dashboards stay cheap. The instruments are lazily resolved
through `metrics.getMeter` so a host swapping the global `MeterProvider`
after import still sees emissions against the intended provider.
## Features
- `pwstrength.assess_duration_ms{score}` — wall time of `assess`
- `pwstrength.score_distribution_total{score}` — bucketed score counter
- `pwstrength.errors_total{code}` — error counter keyed by stable code
- `pwstrength.matchers_run_total{matcher}` — match-count counter by matcher
- `resetPasswordStrengthMetricsForTesting()` for fixture-driven tests
## Usage: Basic Example
```ts
import { recordAssess, recordPasswordStrengthError } from "@suaveplan/password-strength";
import { PWSTRENGTH_CODES } from "@suaveplan/password-strength/errors";
recordAssess(3, 1.5);
recordPasswordStrengthError(PWSTRENGTH_CODES.INPUT_TOO_LONG);
```
## Usage: Advanced Example
```ts
import { resetPasswordStrengthMetricsForTesting } from "@suaveplan/password-strength";
// In a test fixture: swap the global MeterProvider, then reset caches so the
// next emission resolves against the new provider.
resetPasswordStrengthMetricsForTesting();
```
## API Reference
- `recordAssess(score: Score, durationMs: number): void`
- `recordPasswordStrengthError(code: PasswordStrengthErrorCode): void`
- `recordMatcherRun(matcher: MatcherName, count: number): void`
- `resetPasswordStrengthMetricsForTesting(): void`
## Implementation Notes
Each accessor lazily constructs its instrument so a host that installs an
in-memory `MeterProvider` after import time can still observe emissions.
The reset function clears the module-level caches; the next emission
re-resolves and creates fresh instruments. Attributes are intentionally
low-cardinality: `score` has five values; `code` and `matcher` are
fixed-set enums; no per-password attributes are emitted so the password
itself never leaks to logs.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,83 @@
# `schemas` — Zod boundary schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Declares every Zod schema validating the package's public boundaries:
`AssessOptions`, `FeedbackOptions`, `AssessReactiveOptions`,
`CrackTimeOptions`, `Match` (discriminated union over all six pattern
types), `StrengthResult`, and `DictionarySet`. Every public function
Zod-parses its options before any other work, so a malformed input throws
`PWSTRENGTH.INVALID_OPTIONS` with the original `ZodError` preserved on
`cause`.
## Features
- Discriminated `MatchSchema` over six pattern types
- Stable `StrengthResultSchema` exported for downstream consumers
- Strict refinements on numeric ranges (e.g. `debounceMs ∈ [0, 5000]`)
- AbortSignal and function-typed fields validated via `z.custom`
## Usage: Basic Example
```ts
import { AssessOptionsSchema } from "@suaveplan/password-strength/schemas";
const parsed = AssessOptionsSchema.parse({ userInputs: ["alice@example.com"] });
console.log(parsed);
```
## Usage: Advanced Example
```ts
import { MatchSchema, StrengthResultSchema } from "@suaveplan/password-strength/schemas";
const match = MatchSchema.parse({
pattern: "dictionary",
i: 0,
j: 7,
token: "password",
matchedWord: "password",
rank: 1,
dictionaryName: "passwords",
reversed: false,
l33t: false,
});
console.log(match.pattern); // narrowed to "dictionary"
```
## API Reference
- `AssessOptionsSchema` — options accepted by `assess`.
- `FeedbackOptionsSchema` — options accepted by `feedback`.
- `AssessReactiveOptionsSchema` — options accepted by `assessReactive`.
- `CrackTimeOptionsSchema` — re-exported from `scoring/crack-times`.
- `MatchSchema` — discriminated union over all six pattern types.
- `StrengthResultSchema` — the canonical `StrengthResult` shape.
- `DictionarySetSchema` — `Record<string, Map<string, number>>`.
## Implementation Notes
`MatchSchema` uses `z.discriminatedUnion` on the `pattern` field so callers
parsing untrusted JSON get type-narrowed output. Function-typed and
`AbortSignal`-typed fields use `z.custom` since Zod has no built-in for
either. The `baseMatch` partial is spread into each variant to keep
position-tracking fields (`i`, `j`, `token`) consistent across the union
without duplication.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.

View File

@@ -0,0 +1,60 @@
# `crack-times` — four-scenario crack-time calculator
## Purpose
Maps a `guesses` count to seconds-to-crack under the four canonical attack
scenarios from Wheeler 2016 section 5: online throttled, online without
throttling, offline slow hashing (Argon2-class), and offline fast hashing
(commodity-GPU MD5). Site operators tune the effective rate per scenario
via the `adjustments` map so the meter reflects their actual rate-limit
policy.
## Features
- Four canonical scenarios, all always present in the output
- Per-scenario positive `adjustments` multiplier
- Zod-validated options; non-positive multipliers raise
`PWSTRENGTH.INVALID_OPTIONS`
- `humanizeCrackTime` helper for plain-English UI strings
- Pure: zero allocations beyond the result object
## Usage: Basic Example
```ts
import { crackTimes } from "@suaveplan/password-strength/scoring";
const times = crackTimes(1e8);
console.log(times.online_throttling_100_per_hour);
console.log(times.offline_fast_hashing_1e10_per_second);
```
## Usage: Advanced Example
```ts
import { crackTimes, humanizeCrackTime } from "@suaveplan/password-strength/scoring";
// Site enforces a stricter rate limit (half the default online rate)
const times = crackTimes(1e8, { adjustments: { online_throttled: 0.5 } });
console.log(humanizeCrackTime(times.online_throttling_100_per_hour));
```
## API Reference
- `crackTimes(guesses: number, opts?: CrackTimeOptions): CrackTimes`
- `opts.adjustments`: positive-number map keyed by `online_throttled`,
`online_no_throttling`, `offline_slow`, `offline_fast`. Throws
`PasswordStrengthError(PWSTRENGTH.INVALID_OPTIONS)` on validation failure.
- `humanizeCrackTime(seconds: number): string` — concise English string.
- `CrackTimes`, `CrackTimeOptions`, `CrackTimeAdjustments` — value types.
- `CrackTimeOptionsSchema`, `CrackTimeAdjustmentsSchema` — Zod boundary schemas.
## Implementation Notes
The four scenarios use Wheeler section 5 rates: `100/h` (3600 seconds per
hour ÷ 100 guesses = 36 s/guess), `10/s`, `1e4/s`, `1e10/s`. Adjustments
multiply the rate, so `online_throttled: 0.5` halves the effective rate
and doubles the time. The Zod schema enforces every adjustment is strictly
positive — zero and negative values throw `INVALID_OPTIONS` with the
original `ZodError` preserved on `cause`. `humanizeCrackTime` is
intentionally locale-neutral; consumers wanting localised output should
plug into `@suaveplan/datetime` `formatRelative`.

View File

@@ -0,0 +1,57 @@
# `estimate-guesses` — per-pattern closed-form guesses estimator
## Purpose
Implements the Wheeler 2016 section 4 closed-form guess estimates per pattern
type: dictionary (rank × uppercase × l33t × reverse), sequence (alphabet base
× length × direction), repeat (baseGuesses × count), date (`365 × |year - ref|
× (separator ? 4 : 1)`), keyboard (Wheeler section 4.5 walk model), and regex
(year-distance for `recent_year`). The output drives
`mostGuessableMatchSequence`'s DP over candidate covers.
## Features
- Closed-form formula per pattern; no DP recursion at this layer
- Configurable `referenceYear` for date and regex year formulae
- Binomial-coefficient and combinatorial-walk helpers inlined
- Uppercase variation factor handles `Word`, `wordE`, `WoRd`, and mixed forms
- L33t variation factor counts the cartesian product of sub-positions
- Keyboard walk factor combines start-position, turn-product, and shift count
## Usage: Basic Example
```ts
import { estimateGuesses } from "@suaveplan/password-strength/scoring";
const match = { pattern: "dictionary", rank: 100, token: "password", matchedWord: "password", dictionaryName: "passwords", reversed: false, l33t: false, i: 0, j: 7 } as const;
console.log(estimateGuesses(match, "password")); // 100
```
## Usage: Advanced Example
```ts
import { estimateGuesses } from "@suaveplan/password-strength/scoring";
// Anchor a date estimate to a fixed reference year for reproducible tests
const date = { pattern: "date", year: 1985, month: 11, day: 22, separator: "/", token: "11/22/1985", i: 0, j: 9 } as const;
const guesses = estimateGuesses(date, "11/22/1985", { referenceYear: 2026 });
console.log(guesses); // 365 * |1985 - 2026| * 4 → see Wheeler 4.4
```
## API Reference
- `estimateGuesses(match: Match, password: string, context?: EstimateGuessesContext): number`
- `context.referenceYear`: anchor year for `date` and `recent_year` regex
formulae. Defaults to the current UTC year.
- `EstimateGuessesContext`: `{ referenceYear?: number }`
## Implementation Notes
The formulae are sourced line-for-line from Wheeler section 4.1–4.6 and
cross-checked against the `dropbox/zxcvbn` reference impl
`scoring.coffee::estimate_guesses` family. The conformance suite asserts
the corpus agrees within ≤1 bit of `log10(guesses)`. The keyboard walk
factor follows Wheeler section 4.5: it sums `(i_choose_j) × starts × degree^j`
over walk length `i` and turn count `j`, then multiplies by the
combinatorial variation factor for shift placement. Year distances clamp
to a minimum of 20 to avoid divide-by-zero on `year === ref`.

View File

@@ -0,0 +1,69 @@
# `most-guessable-match-sequence` — DP cover selector
## Purpose
Picks the lowest-`Π guesses × k!` cover of the password from the union of
pattern matches plus implicit bruteforce-character fallback per Wheeler 2016
section 4. The factorial penalty discourages over-fragmentation — a single
plausible cover beats a flurry of tiny matches even when each piece is
individually cheap.
## Features
- `O(n^2)` DP over the password length
- Bruteforce-character fallback ensures every cover spans the whole password
- Factorial penalty applied once at the end (count of segments in the cover)
- Ties tie-broken by fewer matches
- Empty input returns `{ guesses: 1, guessesLog10: 0, sequence: [] }`
- Pure: no allocations beyond the result and DP cells
## Usage: Basic Example
```ts
import { dictionaryMatch } from "@suaveplan/password-strength/matchers/dictionary";
import { mostGuessableMatchSequence } from "@suaveplan/password-strength/scoring";
const matches = dictionaryMatch("password", { passwords: new Map([["password", 1]]) });
const result = mostGuessableMatchSequence("password", matches);
console.log(result.guesses);
```
## Usage: Advanced Example
```ts
import { mostGuessableMatchSequence } from "@suaveplan/password-strength/scoring";
import { dictionaryMatch } from "@suaveplan/password-strength/matchers/dictionary";
import { sequenceMatch } from "@suaveplan/password-strength/matchers/sequence";
const pwd = "abc!Word123";
const matches = [
...dictionaryMatch(pwd, { en: new Map([["word", 1]]) }),
...sequenceMatch(pwd),
];
// Anchor the date / regex year formulae for deterministic tests
const result = mostGuessableMatchSequence(pwd, matches, { referenceYear: 2026 });
console.log(result.sequence.length, result.guessesLog10);
```
## API Reference
- `mostGuessableMatchSequence(password: string, matches: Match[], context?: EstimateGuessesContext): MatchSequence`
- Returns `{ password, guesses, guessesLog10, sequence }`. `sequence`
contains only real pattern matches; the bruteforce fragments are
consumed internally but not reported back to the caller.
- `MatchSequence` — result type defined above.
## Implementation Notes
The DP fills a 1-D table indexed by end position `j`. Each cell tracks the
best (lowest `product × factorial(length)`) cover of `[0..j]`. Three
candidate transitions populate each cell: a per-character bruteforce
extension from `[0..j-1]`, a "use match `m` ending at `j`" transition
prepending `best[m.i-1]`, and a single bruteforce span `[i..j]` prepended
to `best[i-1]`. Ties prefer the cover with fewer segments, matching
Wheeler section 4's stated tie-break. The factorial penalty is applied
exactly once on the final cell so cells with the same length are
comparable without re-multiplying. Bruteforce uses cardinality 10 — a
deliberate Wheeler-recommended floor that keeps short ungrouped digits from
collapsing to 1 guess.

View File

@@ -0,0 +1,74 @@
# `score` — Wheeler 5-bucket score function
## Purpose
Maps a `guesses` count to a coarse integer score per Wheeler 2016 section 5.
This score (`0`–`4`) is what most consumer UIs display as a strength meter:
zero is trivially crackable; four is "no realistic attacker will guess this".
The bucket boundaries are `1e3`, `1e6`, `1e8`, `1e10`.
## Features
- Exact bucket boundaries match Wheeler section 5 and `dropbox/zxcvbn`
- Literal-typed return (`0 | 1 | 2 | 3 | 4`) for compile-time exhaustiveness
- Pure: zero allocations, zero side effects
## Usage: Basic Example
```ts
import { score } from "@suaveplan/password-strength/scoring";
console.log(score(0)); // 0
console.log(score(1e6)); // 2
console.log(score(1e10)); // 4
```
## Usage: Advanced Example
```ts
import { score, type Score } from "@suaveplan/password-strength/scoring";
function strengthLabel(s: Score): string {
switch (s) {
case 0:
return "very weak";
case 1:
return "weak";
case 2:
return "fair";
case 3:
return "strong";
case 4:
return "very strong";
}
}
console.log(strengthLabel(score(1e7))); // "fair"
```
## API Reference
- `score(guesses: number): Score` — return the Wheeler 5-bucket score.
- `Score` — literal union type `0 | 1 | 2 | 3 | 4`.
## Implementation Notes
The bucket boundaries are exclusive at the left and inclusive at the right
of each previous bucket: `999` → `0`, `1000` → `1`. The output type is a
literal union so callers can write exhaustive switches against the bucket
set, and `expectTypeOf` assertions can verify the type at compile time.
## Additional Implementation Notes
The implementation follows the canonical zxcvbn algorithm from Wheeler 2016 with
the suaveplan-style additions described in the package proposal: structured error
codes via the `defineCodes` registry, Zod boundaries at every public entry
point, OTel histograms and counters keyed to low-cardinality attributes, and
a strict input-length guard at 200 characters. Consumers integrate with their
existing observability stack by installing a meter provider before importing
the package, and with their i18n stack via the `MessageResolver` callback type
declared in `@suaveplan/types`. The bundled data tables are intentionally
small to keep the package bundle compact; production deployments should swap
in fuller frequency-ranked tables via `loadDictionaries`. The default English
dictionary set is a frozen `DictionarySet` that the matcher never mutates;
caller-supplied dictionaries override defaults by name.