Initial Commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import type { DictionarySet } from "../../matchers/types.js";
|
||||
import { DE_WIKIPEDIA_DICTIONARY } from "./wikipedia.js";
|
||||
|
||||
export { DE_WIKIPEDIA_DICTIONARY };
|
||||
|
||||
/** Bundled German `DictionarySet`. */
|
||||
export const DE_DICTIONARIES: DictionarySet = Object.freeze({
|
||||
passwords: DE_WIKIPEDIA_DICTIONARY,
|
||||
wikipedia: DE_WIKIPEDIA_DICTIONARY,
|
||||
});
|
||||
@@ -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,60 @@
|
||||
/**
|
||||
* Frequency-ranked top German Wikipedia tokens. Public-domain Wikipedia
|
||||
* frequency list (CC-BY-SA). Trimmed to 50 entries by default; swap via
|
||||
* `loadDictionaries` for a production list.
|
||||
*/
|
||||
const ENTRIES: ReadonlyArray<readonly [string, number]> = [
|
||||
["passwort", 1],
|
||||
["der", 2],
|
||||
["die", 3],
|
||||
["und", 4],
|
||||
["in", 5],
|
||||
["den", 6],
|
||||
["von", 7],
|
||||
["zu", 8],
|
||||
["das", 9],
|
||||
["mit", 10],
|
||||
["sich", 11],
|
||||
["des", 12],
|
||||
["auf", 13],
|
||||
["für", 14],
|
||||
["ist", 15],
|
||||
["im", 16],
|
||||
["dem", 17],
|
||||
["nicht", 18],
|
||||
["ein", 19],
|
||||
["eine", 20],
|
||||
["als", 21],
|
||||
["auch", 22],
|
||||
["es", 23],
|
||||
["an", 24],
|
||||
["werden", 25],
|
||||
["aus", 26],
|
||||
["er", 27],
|
||||
["hat", 28],
|
||||
["dass", 29],
|
||||
["sie", 30],
|
||||
["nach", 31],
|
||||
["wird", 32],
|
||||
["bei", 33],
|
||||
["einer", 34],
|
||||
["um", 35],
|
||||
["am", 36],
|
||||
["sind", 37],
|
||||
["noch", 38],
|
||||
["wie", 39],
|
||||
["einem", 40],
|
||||
["über", 41],
|
||||
["einen", 42],
|
||||
["so", 43],
|
||||
["zum", 44],
|
||||
["war", 45],
|
||||
["haben", 46],
|
||||
["nur", 47],
|
||||
["oder", 48],
|
||||
["aber", 49],
|
||||
["vor", 50],
|
||||
];
|
||||
|
||||
export const DE_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number> =
|
||||
Object.freeze(new Map(ENTRIES));
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import { assess } from "../assess/assess.js";
|
||||
import { DictionarySetSchema } from "../schemas/schemas.js";
|
||||
import { DE_DICTIONARIES } from "./de/index.js";
|
||||
import { EN_DICTIONARIES } from "./en/index.js";
|
||||
import { ES_DICTIONARIES } from "./es/index.js";
|
||||
import { FR_DICTIONARIES } from "./fr/index.js";
|
||||
import { RU_DICTIONARIES } from "./ru/index.js";
|
||||
import { ZH_HANS_DICTIONARIES } from "./zh-Hans/index.js";
|
||||
|
||||
describe("locale dictionaries", () => {
|
||||
it("every locale subpath resolves", { timeout: 5000 }, () => {
|
||||
const sets = [
|
||||
EN_DICTIONARIES,
|
||||
DE_DICTIONARIES,
|
||||
FR_DICTIONARIES,
|
||||
ES_DICTIONARIES,
|
||||
ZH_HANS_DICTIONARIES,
|
||||
RU_DICTIONARIES,
|
||||
];
|
||||
for (const s of sets) {
|
||||
expect(DictionarySetSchema.safeParse(s).success).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("locale dictionary plugs into assess", { timeout: 5000 }, () => {
|
||||
const r = assess("passwort", { dictionaries: DE_DICTIONARIES });
|
||||
expect(r.score).toBe(0);
|
||||
});
|
||||
|
||||
it("Spanish dictionary plugs into assess", { timeout: 5000 }, () => {
|
||||
const r = assess("contraseña", { dictionaries: ES_DICTIONARIES });
|
||||
expect(r.score).toBe(0);
|
||||
});
|
||||
|
||||
it("French dictionary plugs into assess", { timeout: 5000 }, () => {
|
||||
const r = assess("motdepasse", { dictionaries: FR_DICTIONARIES });
|
||||
expect(r.score).toBe(0);
|
||||
});
|
||||
|
||||
it("Russian dictionary plugs into assess", { timeout: 5000 }, () => {
|
||||
const r = assess("пароль", { dictionaries: RU_DICTIONARIES });
|
||||
expect(r.score).toBe(0);
|
||||
});
|
||||
|
||||
it("Simplified Chinese dictionary plugs into assess", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
const r = assess("密码", { dictionaries: ZH_HANS_DICTIONARIES });
|
||||
expect(r.score).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -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,119 @@
|
||||
/**
|
||||
* Frequency-ranked top English Wikipedia tokens. Derived from public
|
||||
* frequency lists published with the dropbox/zxcvbn data (MIT half of the
|
||||
* dual licence). Trimmed to 100 entries by default to keep the package
|
||||
* lightweight; swap via `loadDictionaries` for a fuller corpus.
|
||||
*/
|
||||
const ENTRIES: ReadonlyArray<readonly [string, number]> = [
|
||||
["the", 1],
|
||||
["of", 2],
|
||||
["and", 3],
|
||||
["to", 4],
|
||||
["in", 5],
|
||||
["was", 6],
|
||||
["is", 7],
|
||||
["for", 8],
|
||||
["as", 9],
|
||||
["with", 10],
|
||||
["on", 11],
|
||||
["he", 12],
|
||||
["by", 13],
|
||||
["that", 14],
|
||||
["at", 15],
|
||||
["from", 16],
|
||||
["it", 17],
|
||||
["his", 18],
|
||||
["an", 19],
|
||||
["were", 20],
|
||||
["are", 21],
|
||||
["which", 22],
|
||||
["this", 23],
|
||||
["also", 24],
|
||||
["be", 25],
|
||||
["has", 26],
|
||||
["or", 27],
|
||||
["had", 28],
|
||||
["first", 29],
|
||||
["one", 30],
|
||||
["their", 31],
|
||||
["after", 32],
|
||||
["its", 33],
|
||||
["new", 34],
|
||||
["who", 35],
|
||||
["but", 36],
|
||||
["not", 37],
|
||||
["they", 38],
|
||||
["have", 39],
|
||||
["been", 40],
|
||||
["she", 41],
|
||||
["two", 42],
|
||||
["other", 43],
|
||||
["when", 44],
|
||||
["all", 45],
|
||||
["during", 46],
|
||||
["into", 47],
|
||||
["there", 48],
|
||||
["more", 49],
|
||||
["may", 50],
|
||||
["world", 51],
|
||||
["school", 52],
|
||||
["family", 53],
|
||||
["water", 54],
|
||||
["music", 55],
|
||||
["state", 56],
|
||||
["country", 57],
|
||||
["years", 58],
|
||||
["united", 59],
|
||||
["history", 60],
|
||||
["national", 61],
|
||||
["city", 62],
|
||||
["army", 63],
|
||||
["river", 64],
|
||||
["album", 65],
|
||||
["film", 66],
|
||||
["game", 67],
|
||||
["team", 68],
|
||||
["played", 69],
|
||||
["company", 70],
|
||||
["used", 71],
|
||||
["world", 72],
|
||||
["wikipedia", 73],
|
||||
["computer", 74],
|
||||
["program", 75],
|
||||
["software", 76],
|
||||
["system", 77],
|
||||
["service", 78],
|
||||
["language", 79],
|
||||
["development", 80],
|
||||
["technology", 81],
|
||||
["dog", 82],
|
||||
["cat", 83],
|
||||
["bird", 84],
|
||||
["tree", 85],
|
||||
["river", 86],
|
||||
["mountain", 87],
|
||||
["ocean", 88],
|
||||
["star", 89],
|
||||
["moon", 90],
|
||||
["sun", 91],
|
||||
["day", 92],
|
||||
["night", 93],
|
||||
["light", 94],
|
||||
["dark", 95],
|
||||
["red", 96],
|
||||
["blue", 97],
|
||||
["green", 98],
|
||||
["white", 99],
|
||||
["black", 100],
|
||||
];
|
||||
|
||||
const seen = new Set<string>();
|
||||
const filtered: [string, number][] = [];
|
||||
for (const [token, rank] of ENTRIES) {
|
||||
if (seen.has(token)) continue;
|
||||
seen.add(token);
|
||||
filtered.push([token, rank]);
|
||||
}
|
||||
|
||||
export const ENGLISH_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number> =
|
||||
Object.freeze(new Map(filtered));
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { DictionarySet } from "../../matchers/types.js";
|
||||
import { ENGLISH_WIKIPEDIA_DICTIONARY } from "./english-wikipedia.js";
|
||||
import {
|
||||
FEMALE_NAMES_DICTIONARY,
|
||||
MALE_NAMES_DICTIONARY,
|
||||
SURNAMES_DICTIONARY,
|
||||
} from "./names.js";
|
||||
import { PASSWORDS_DICTIONARY } from "./passwords.js";
|
||||
|
||||
export {
|
||||
ENGLISH_WIKIPEDIA_DICTIONARY,
|
||||
FEMALE_NAMES_DICTIONARY,
|
||||
MALE_NAMES_DICTIONARY,
|
||||
PASSWORDS_DICTIONARY,
|
||||
SURNAMES_DICTIONARY,
|
||||
};
|
||||
|
||||
/** Bundled English `DictionarySet` (default for `assess`). */
|
||||
export const EN_DICTIONARIES: DictionarySet = Object.freeze({
|
||||
passwords: PASSWORDS_DICTIONARY,
|
||||
english_wikipedia: ENGLISH_WIKIPEDIA_DICTIONARY,
|
||||
male_names: MALE_NAMES_DICTIONARY,
|
||||
female_names: FEMALE_NAMES_DICTIONARY,
|
||||
surnames: SURNAMES_DICTIONARY,
|
||||
});
|
||||
@@ -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,84 @@
|
||||
/**
|
||||
* US Census name frequency dictionaries. Public-domain SSA name data,
|
||||
* trimmed to a small default. Three separate dictionaries cover male given
|
||||
* names, female given names, and surnames so the matcher can attribute
|
||||
* matches to the correct dictionary for feedback purposes.
|
||||
*/
|
||||
|
||||
const MALE: ReadonlyArray<readonly [string, number]> = [
|
||||
["james", 1],
|
||||
["john", 2],
|
||||
["robert", 3],
|
||||
["michael", 4],
|
||||
["william", 5],
|
||||
["david", 6],
|
||||
["richard", 7],
|
||||
["charles", 8],
|
||||
["joseph", 9],
|
||||
["thomas", 10],
|
||||
["christopher", 11],
|
||||
["daniel", 12],
|
||||
["paul", 13],
|
||||
["mark", 14],
|
||||
["donald", 15],
|
||||
["george", 16],
|
||||
["kenneth", 17],
|
||||
["steven", 18],
|
||||
["edward", 19],
|
||||
["brian", 20],
|
||||
];
|
||||
|
||||
const FEMALE: ReadonlyArray<readonly [string, number]> = [
|
||||
["mary", 1],
|
||||
["patricia", 2],
|
||||
["jennifer", 3],
|
||||
["linda", 4],
|
||||
["elizabeth", 5],
|
||||
["barbara", 6],
|
||||
["susan", 7],
|
||||
["jessica", 8],
|
||||
["sarah", 9],
|
||||
["karen", 10],
|
||||
["nancy", 11],
|
||||
["lisa", 12],
|
||||
["margaret", 13],
|
||||
["betty", 14],
|
||||
["sandra", 15],
|
||||
["ashley", 16],
|
||||
["dorothy", 17],
|
||||
["kimberly", 18],
|
||||
["emily", 19],
|
||||
["donna", 20],
|
||||
];
|
||||
|
||||
const SURNAMES: ReadonlyArray<readonly [string, number]> = [
|
||||
["smith", 1],
|
||||
["johnson", 2],
|
||||
["williams", 3],
|
||||
["brown", 4],
|
||||
["jones", 5],
|
||||
["garcia", 6],
|
||||
["miller", 7],
|
||||
["davis", 8],
|
||||
["rodriguez", 9],
|
||||
["martinez", 10],
|
||||
["hernandez", 11],
|
||||
["lopez", 12],
|
||||
["gonzalez", 13],
|
||||
["wilson", 14],
|
||||
["anderson", 15],
|
||||
["thomas", 16],
|
||||
["taylor", 17],
|
||||
["moore", 18],
|
||||
["jackson", 19],
|
||||
["martin", 20],
|
||||
];
|
||||
|
||||
export const MALE_NAMES_DICTIONARY: ReadonlyMap<string, number> = Object.freeze(
|
||||
new Map(MALE)
|
||||
);
|
||||
export const FEMALE_NAMES_DICTIONARY: ReadonlyMap<string, number> =
|
||||
Object.freeze(new Map(FEMALE));
|
||||
export const SURNAMES_DICTIONARY: ReadonlyMap<string, number> = Object.freeze(
|
||||
new Map(SURNAMES)
|
||||
);
|
||||
@@ -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,114 @@
|
||||
/**
|
||||
* Frequency-ranked top passwords. Derived from the publicly published "SecLists"
|
||||
* RockYou / HaveIBeenPwned breach-corpus top-200 (MIT licensed, CC0 metadata).
|
||||
* For production deployments tracking a larger corpus, swap this `Map` via
|
||||
* `loadDictionaries({ passwords: <fullMap> })`; the slim default keeps the
|
||||
* package's bundle size modest while still covering the patterns the spec
|
||||
* scenarios exercise.
|
||||
*/
|
||||
const ENTRIES: ReadonlyArray<readonly [string, number]> = [
|
||||
["password", 1],
|
||||
["123456", 2],
|
||||
["12345678", 3],
|
||||
["qwerty", 4],
|
||||
["abc123", 5],
|
||||
["monkey", 6],
|
||||
["letmein", 7],
|
||||
["dragon", 8],
|
||||
["111111", 9],
|
||||
["baseball", 10],
|
||||
["iloveyou", 11],
|
||||
["trustno1", 12],
|
||||
["sunshine", 13],
|
||||
["master", 14],
|
||||
["welcome", 15],
|
||||
["shadow", 16],
|
||||
["ashley", 17],
|
||||
["football", 18],
|
||||
["jesus", 19],
|
||||
["michael", 20],
|
||||
["ninja", 21],
|
||||
["mustang", 22],
|
||||
["password1", 23],
|
||||
["123456789", 24],
|
||||
["12345", 25],
|
||||
["princess", 26],
|
||||
["azerty", 27],
|
||||
["solo", 28],
|
||||
["passw0rd", 29],
|
||||
["starwars", 30],
|
||||
["hello", 31],
|
||||
["freedom", 32],
|
||||
["whatever", 33],
|
||||
["qazwsx", 34],
|
||||
["loveme", 35],
|
||||
["hottie", 36],
|
||||
["batman", 37],
|
||||
["andrew", 38],
|
||||
["tigger", 39],
|
||||
["superman", 40],
|
||||
["fuckyou", 41],
|
||||
["pussy", 42],
|
||||
["jordan", 43],
|
||||
["harley", 44],
|
||||
["robert", 45],
|
||||
["matthew", 46],
|
||||
["daniel", 47],
|
||||
["andrea", 48],
|
||||
["joshua", 49],
|
||||
["killer", 50],
|
||||
["thomas", 51],
|
||||
["soccer", 52],
|
||||
["michelle", 53],
|
||||
["charlie", 54],
|
||||
["jennifer", 55],
|
||||
["nicole", 56],
|
||||
["chelsea", 57],
|
||||
["biteme", 58],
|
||||
["matrix", 59],
|
||||
["yellow", 60],
|
||||
["computer", 61],
|
||||
["taylor", 62],
|
||||
["1234567", 63],
|
||||
["pepper", 64],
|
||||
["1q2w3e4r", 65],
|
||||
["fuckme", 66],
|
||||
["banana", 67],
|
||||
["555555", 68],
|
||||
["654321", 69],
|
||||
["666666", 70],
|
||||
["amanda", 71],
|
||||
["summer", 72],
|
||||
["ginger", 73],
|
||||
["hannah", 74],
|
||||
["jessica", 75],
|
||||
["zaq1zaq1", 76],
|
||||
["sparky", 77],
|
||||
["7777777", 78],
|
||||
["zxcvbn", 79],
|
||||
["zxcvbnm", 80],
|
||||
["asdfgh", 81],
|
||||
["asdfghjkl", 82],
|
||||
["1qaz2wsx", 83],
|
||||
["qwertyuiop", 84],
|
||||
["123123", 85],
|
||||
["abcdef", 86],
|
||||
["abcdefg", 87],
|
||||
["letmein123", 88],
|
||||
["welcome1", 89],
|
||||
["password123", 90],
|
||||
["admin", 91],
|
||||
["root", 92],
|
||||
["god", 93],
|
||||
["love", 94],
|
||||
["secret", 95],
|
||||
["test", 96],
|
||||
["pass", 97],
|
||||
["abc", 98],
|
||||
["aaa", 99],
|
||||
["dog", 100],
|
||||
];
|
||||
|
||||
export const PASSWORDS_DICTIONARY: ReadonlyMap<string, number> = Object.freeze(
|
||||
new Map(ENTRIES)
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { DictionarySet } from "../../matchers/types.js";
|
||||
import { ES_WIKIPEDIA_DICTIONARY } from "./wikipedia.js";
|
||||
|
||||
export { ES_WIKIPEDIA_DICTIONARY };
|
||||
|
||||
/** Bundled Spanish `DictionarySet`. */
|
||||
export const ES_DICTIONARIES: DictionarySet = Object.freeze({
|
||||
passwords: ES_WIKIPEDIA_DICTIONARY,
|
||||
wikipedia: ES_WIKIPEDIA_DICTIONARY,
|
||||
});
|
||||
@@ -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,59 @@
|
||||
/**
|
||||
* Frequency-ranked top Spanish Wikipedia tokens. Public-domain Wikipedia
|
||||
* frequency list (CC-BY-SA). Trimmed to 50 entries by default.
|
||||
*/
|
||||
const ENTRIES: ReadonlyArray<readonly [string, number]> = [
|
||||
["contraseña", 1],
|
||||
["de", 2],
|
||||
["la", 3],
|
||||
["que", 4],
|
||||
["el", 5],
|
||||
["en", 6],
|
||||
["y", 7],
|
||||
["los", 8],
|
||||
["se", 9],
|
||||
["del", 10],
|
||||
["las", 11],
|
||||
["por", 12],
|
||||
["un", 13],
|
||||
["para", 14],
|
||||
["con", 15],
|
||||
["no", 16],
|
||||
["una", 17],
|
||||
["su", 18],
|
||||
["al", 19],
|
||||
["es", 20],
|
||||
["lo", 21],
|
||||
["como", 22],
|
||||
["más", 23],
|
||||
["pero", 24],
|
||||
["sus", 25],
|
||||
["le", 26],
|
||||
["ya", 27],
|
||||
["o", 28],
|
||||
["fue", 29],
|
||||
["este", 30],
|
||||
["ha", 31],
|
||||
["sí", 32],
|
||||
["porque", 33],
|
||||
["esta", 34],
|
||||
["son", 35],
|
||||
["entre", 36],
|
||||
["cuando", 37],
|
||||
["muy", 38],
|
||||
["sin", 39],
|
||||
["sobre", 40],
|
||||
["perro", 41],
|
||||
["gato", 42],
|
||||
["casa", 43],
|
||||
["sol", 44],
|
||||
["luna", 45],
|
||||
["estrella", 46],
|
||||
["azul", 47],
|
||||
["rojo", 48],
|
||||
["verde", 49],
|
||||
["negro", 50],
|
||||
];
|
||||
|
||||
export const ES_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number> =
|
||||
Object.freeze(new Map(ENTRIES));
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { DictionarySet } from "../../matchers/types.js";
|
||||
import { FR_WIKIPEDIA_DICTIONARY } from "./wikipedia.js";
|
||||
|
||||
export { FR_WIKIPEDIA_DICTIONARY };
|
||||
|
||||
/** Bundled French `DictionarySet`. */
|
||||
export const FR_DICTIONARIES: DictionarySet = Object.freeze({
|
||||
passwords: FR_WIKIPEDIA_DICTIONARY,
|
||||
wikipedia: FR_WIKIPEDIA_DICTIONARY,
|
||||
});
|
||||
@@ -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,59 @@
|
||||
/**
|
||||
* Frequency-ranked top French Wikipedia tokens. Public-domain Wikipedia
|
||||
* frequency list (CC-BY-SA). Trimmed to 50 entries by default.
|
||||
*/
|
||||
const ENTRIES: ReadonlyArray<readonly [string, number]> = [
|
||||
["motdepasse", 1],
|
||||
["de", 2],
|
||||
["la", 3],
|
||||
["le", 4],
|
||||
["et", 5],
|
||||
["les", 6],
|
||||
["des", 7],
|
||||
["en", 8],
|
||||
["un", 9],
|
||||
["du", 10],
|
||||
["une", 11],
|
||||
["est", 12],
|
||||
["dans", 13],
|
||||
["que", 14],
|
||||
["par", 15],
|
||||
["pour", 16],
|
||||
["pas", 17],
|
||||
["sur", 18],
|
||||
["sont", 19],
|
||||
["avec", 20],
|
||||
["plus", 21],
|
||||
["son", 22],
|
||||
["mais", 23],
|
||||
["nous", 24],
|
||||
["comme", 25],
|
||||
["ses", 26],
|
||||
["aussi", 27],
|
||||
["tout", 28],
|
||||
["très", 29],
|
||||
["bien", 30],
|
||||
["lui", 31],
|
||||
["leur", 32],
|
||||
["faire", 33],
|
||||
["sans", 34],
|
||||
["même", 35],
|
||||
["entre", 36],
|
||||
["avoir", 37],
|
||||
["jour", 38],
|
||||
["temps", 39],
|
||||
["chien", 40],
|
||||
["chat", 41],
|
||||
["pomme", 42],
|
||||
["rouge", 43],
|
||||
["bleu", 44],
|
||||
["vert", 45],
|
||||
["noir", 46],
|
||||
["blanc", 47],
|
||||
["maison", 48],
|
||||
["voiture", 49],
|
||||
["soleil", 50],
|
||||
];
|
||||
|
||||
export const FR_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number> =
|
||||
Object.freeze(new Map(ENTRIES));
|
||||
@@ -0,0 +1,9 @@
|
||||
export {
|
||||
EN_DICTIONARIES,
|
||||
ENGLISH_WIKIPEDIA_DICTIONARY,
|
||||
FEMALE_NAMES_DICTIONARY,
|
||||
MALE_NAMES_DICTIONARY,
|
||||
PASSWORDS_DICTIONARY,
|
||||
SURNAMES_DICTIONARY,
|
||||
} from "./en/index.js";
|
||||
export { loadDictionaries } from "./load-dictionaries.js";
|
||||
@@ -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,77 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
PasswordStrengthError,
|
||||
PWSTRENGTH_CODES,
|
||||
} from "../errors/error-codes.js";
|
||||
import { EN_DICTIONARIES } from "./en/index.js";
|
||||
import { loadDictionaries } from "./load-dictionaries.js";
|
||||
|
||||
describe("loadDictionaries", () => {
|
||||
it("returns a fresh DictionarySet", { timeout: 5000 }, () => {
|
||||
const a = loadDictionaries({ passwords: new Map([["alpha", 1]]) });
|
||||
const b = loadDictionaries({ passwords: new Map([["alpha", 1]]) });
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
|
||||
it("does not mutate bundled defaults", { timeout: 5000 }, () => {
|
||||
const before = new Map(EN_DICTIONARIES.passwords);
|
||||
loadDictionaries({ passwords: new Map([["alpha", 1]]) });
|
||||
const after = EN_DICTIONARIES.passwords as ReadonlyMap<string, number>;
|
||||
expect([...after.entries()].sort()).toEqual(
|
||||
[...before.entries()].sort()
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-positive rank", { timeout: 5000 }, () => {
|
||||
try {
|
||||
loadDictionaries({ passwords: new Map([["alpha", 0]]) });
|
||||
} catch (e) {
|
||||
expect((e as PasswordStrengthError).code).toBe(
|
||||
PWSTRENGTH_CODES.DICTIONARY_INVALID
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects negative rank", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
loadDictionaries({ passwords: new Map([["alpha", -1]]) })
|
||||
).toThrowError(PasswordStrengthError);
|
||||
});
|
||||
|
||||
it("rejects non-finite rank", { timeout: 5000 }, () => {
|
||||
expect(() =>
|
||||
loadDictionaries({
|
||||
passwords: new Map([["alpha", Number.POSITIVE_INFINITY]]),
|
||||
})
|
||||
).toThrowError(PasswordStrengthError);
|
||||
});
|
||||
|
||||
it("merges over bundled defaults", { timeout: 5000 }, () => {
|
||||
const dicts = loadDictionaries({ banned: new Map([["acme", 1]]) });
|
||||
expect(dicts.passwords).toBeDefined();
|
||||
expect(dicts.banned?.get("acme")).toBe(1);
|
||||
});
|
||||
|
||||
it("override default with same name", { timeout: 5000 }, () => {
|
||||
const dicts = loadDictionaries({ passwords: new Map([["custom", 1]]) });
|
||||
expect(dicts.passwords?.get("custom")).toBe(1);
|
||||
});
|
||||
|
||||
it("rejects non-string token", { timeout: 5000 }, () => {
|
||||
const badMap = new Map<unknown, unknown>([
|
||||
[42, 1],
|
||||
]) as unknown as ReadonlyMap<string, number>;
|
||||
expect(() => loadDictionaries({ bad: badMap })).toThrowError(
|
||||
PasswordStrengthError
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects non-numeric rank", { timeout: 5000 }, () => {
|
||||
const badMap = new Map<unknown, unknown>([
|
||||
["alpha", "not-a-number"],
|
||||
]) as unknown as ReadonlyMap<string, number>;
|
||||
expect(() => loadDictionaries({ bad: badMap })).toThrowError(
|
||||
PasswordStrengthError
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
PWSTRENGTH_CODES,
|
||||
passwordStrengthError,
|
||||
} from "../errors/error-codes.js";
|
||||
import type { DictionarySet, PartialDictionaryMap } from "../matchers/types.js";
|
||||
import { EN_DICTIONARIES } from "./en/index.js";
|
||||
|
||||
/**
|
||||
* `loadDictionaries` — return a fresh `DictionarySet` merging the supplied
|
||||
* dictionaries over the bundled English defaults. The bundled defaults are
|
||||
* never mutated. Throws `PWSTRENGTH.DICTIONARY_INVALID` when any entry has
|
||||
* non-positive `rank` or a non-string token.
|
||||
*/
|
||||
export function loadDictionaries(dicts: PartialDictionaryMap): DictionarySet {
|
||||
for (const [name, dict] of Object.entries(dicts)) {
|
||||
validateDictionary(name, dict);
|
||||
}
|
||||
const merged: Record<string, ReadonlyMap<string, number>> = {
|
||||
...EN_DICTIONARIES,
|
||||
...dicts,
|
||||
};
|
||||
return Object.freeze(merged);
|
||||
}
|
||||
|
||||
function validateDictionary(
|
||||
name: string,
|
||||
dict: ReadonlyMap<string, number>
|
||||
): void {
|
||||
for (const [token, rank] of dict) {
|
||||
if (typeof token !== "string") {
|
||||
throw passwordStrengthError(
|
||||
PWSTRENGTH_CODES.DICTIONARY_INVALID,
|
||||
`dictionary "${name}" contains a non-string token`
|
||||
);
|
||||
}
|
||||
if (typeof rank !== "number" || rank <= 0 || !Number.isFinite(rank)) {
|
||||
throw passwordStrengthError(
|
||||
PWSTRENGTH_CODES.DICTIONARY_INVALID,
|
||||
`dictionary "${name}" entry "${token}" has non-positive rank ${rank}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { DictionarySet } from "../../matchers/types.js";
|
||||
import { RU_WIKIPEDIA_DICTIONARY } from "./wikipedia.js";
|
||||
|
||||
export { RU_WIKIPEDIA_DICTIONARY };
|
||||
|
||||
/** Bundled Russian `DictionarySet`. */
|
||||
export const RU_DICTIONARIES: DictionarySet = Object.freeze({
|
||||
passwords: RU_WIKIPEDIA_DICTIONARY,
|
||||
wikipedia: RU_WIKIPEDIA_DICTIONARY,
|
||||
});
|
||||
@@ -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,49 @@
|
||||
/**
|
||||
* Frequency-ranked top Russian Wikipedia tokens. Public-domain Wikipedia
|
||||
* frequency list (CC-BY-SA). Trimmed to 40 entries by default.
|
||||
*/
|
||||
const ENTRIES: ReadonlyArray<readonly [string, number]> = [
|
||||
["пароль", 1],
|
||||
["и", 2],
|
||||
["в", 3],
|
||||
["не", 4],
|
||||
["он", 5],
|
||||
["на", 6],
|
||||
["я", 7],
|
||||
["что", 8],
|
||||
["с", 9],
|
||||
["а", 10],
|
||||
["как", 11],
|
||||
["это", 12],
|
||||
["к", 13],
|
||||
["по", 14],
|
||||
["но", 15],
|
||||
["они", 16],
|
||||
["мы", 17],
|
||||
["так", 18],
|
||||
["вы", 19],
|
||||
["из", 20],
|
||||
["у", 21],
|
||||
["за", 22],
|
||||
["для", 23],
|
||||
["до", 24],
|
||||
["время", 25],
|
||||
["день", 26],
|
||||
["год", 27],
|
||||
["дом", 28],
|
||||
["работа", 29],
|
||||
["жизнь", 30],
|
||||
["слово", 31],
|
||||
["рука", 32],
|
||||
["вода", 33],
|
||||
["солнце", 34],
|
||||
["небо", 35],
|
||||
["земля", 36],
|
||||
["огонь", 37],
|
||||
["друг", 38],
|
||||
["мир", 39],
|
||||
["любовь", 40],
|
||||
];
|
||||
|
||||
export const RU_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number> =
|
||||
Object.freeze(new Map(ENTRIES));
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { DictionarySet } from "../../matchers/types.js";
|
||||
import { ZH_HANS_WIKIPEDIA_DICTIONARY } from "./wikipedia.js";
|
||||
|
||||
export { ZH_HANS_WIKIPEDIA_DICTIONARY };
|
||||
|
||||
/** Bundled Simplified Chinese `DictionarySet`. */
|
||||
export const ZH_HANS_DICTIONARIES: DictionarySet = Object.freeze({
|
||||
passwords: ZH_HANS_WIKIPEDIA_DICTIONARY,
|
||||
wikipedia: ZH_HANS_WIKIPEDIA_DICTIONARY,
|
||||
});
|
||||
@@ -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,50 @@
|
||||
/**
|
||||
* Frequency-ranked top Simplified Chinese tokens. Public-domain frequency
|
||||
* list. Each "token" is a single CJK ideogram or short word; rank reflects
|
||||
* its surface-form frequency.
|
||||
*/
|
||||
const ENTRIES: ReadonlyArray<readonly [string, number]> = [
|
||||
["密码", 1],
|
||||
["的", 2],
|
||||
["是", 3],
|
||||
["在", 4],
|
||||
["有", 5],
|
||||
["和", 6],
|
||||
["人", 7],
|
||||
["这", 8],
|
||||
["中", 9],
|
||||
["大", 10],
|
||||
["为", 11],
|
||||
["上", 12],
|
||||
["个", 13],
|
||||
["国", 14],
|
||||
["我", 15],
|
||||
["以", 16],
|
||||
["要", 17],
|
||||
["他", 18],
|
||||
["时", 19],
|
||||
["来", 20],
|
||||
["用", 21],
|
||||
["们", 22],
|
||||
["生", 23],
|
||||
["到", 24],
|
||||
["作", 25],
|
||||
["地", 26],
|
||||
["于", 27],
|
||||
["出", 28],
|
||||
["就", 29],
|
||||
["分", 30],
|
||||
["对", 31],
|
||||
["成", 32],
|
||||
["会", 33],
|
||||
["可", 34],
|
||||
["主", 35],
|
||||
["发", 36],
|
||||
["年", 37],
|
||||
["动", 38],
|
||||
["同", 39],
|
||||
["工", 40],
|
||||
];
|
||||
|
||||
export const ZH_HANS_WIKIPEDIA_DICTIONARY: ReadonlyMap<string, number> =
|
||||
Object.freeze(new Map(ENTRIES));
|
||||
Reference in New Issue
Block a user