Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -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,
});

View File

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

View File

@@ -0,0 +1,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));

View File

@@ -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);
});
});

View File

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

View File

@@ -0,0 +1,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));

View File

@@ -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,
});

View File

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

View File

@@ -0,0 +1,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)
);

View File

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

View File

@@ -0,0 +1,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)
);

View File

@@ -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,
});

View File

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

View File

@@ -0,0 +1,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));

View File

@@ -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,
});

View File

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

View File

@@ -0,0 +1,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));

View File

@@ -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";

View File

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

View File

@@ -0,0 +1,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
);
});
});

View File

@@ -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}`
);
}
}
}

View File

@@ -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,
});

View File

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

View File

@@ -0,0 +1,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));

View File

@@ -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,
});

View File

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

View File

@@ -0,0 +1,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));