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:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user