Password Policy
PasswordPolicy and PasswordScore — check()/enforce() a password against configurable PasswordPolicyOptions with a PasswordCheckContext, returning a PasswordCheckResult of PasswordWeakness entries keyed by PasswordWeaknessCode.
PasswordPolicy grades a password and, on request, gates on it. It is the same measurement the library applies to the secrets that protect a workspace or account, exposed so you can screen a password before you ever hand it to Workspace.open or accounts.create. A check returns a structured PasswordCheckResult — a numeric score, an entropy estimate, and a list of typed PasswordWeakness entries — rather than a bare boolean, so your UI can explain why a password was refused.
import { PasswordPolicy, PasswordScore } from "wative-core";
const policy = new PasswordPolicy({ minLength: 12, minScore: PasswordScore.Fair });
const result = policy.check("correct horse battery staple");
result.passable; // true
result.summary; // "Password OK (score 3, ~132 bits)."
// Or gate on it — throws WativeError("WEAK_PASSWORD") when not passable.
policy.enforce("hunter2");Constructing a policy
new PasswordPolicy(opts?: PasswordPolicyOptions). Every option is optional; an omitted option takes its default, and passing undefined for an option is treated as "not supplied" (so new PasswordPolicy({ minLength: undefined }) keeps the default floor rather than removing it). Construct one policy and reuse it — instances are stateless between calls.
check vs enforce
Both take the password plus an optional PasswordCheckContext. They differ only in what they do with a failing result.
| Method | Signature | Behavior |
|---|---|---|
check | check(password: string, context?: PasswordCheckContext): PasswordCheckResult | Always returns the full result. Never throws for a weak password. |
enforce | enforce(password: string, context?: PasswordCheckContext): void | Runs check; returns nothing when passable, otherwise throws. |
enforce throws WativeError("WEAK_PASSWORD", summary, { details: { result } }) — the details.result field carries the same PasswordCheckResult check would have returned, so a handler can render the specific weaknesses. check itself throws only when its input is not a string (WativeError("PROVIDER_IO")).
A password is passable when its score is at least the policy's minScore and it has no critical weakness. A single critical weakness fails the password no matter how high its entropy.
Options
interface PasswordPolicyOptions {
minScore?: PasswordScore;
minLength?: number;
requireUppercase?: boolean;
requireLowercase?: boolean;
requireDigit?: boolean;
requireSymbol?: boolean;
forbiddenWords?: string[];
}| Option | Default | Effect |
|---|---|---|
minScore | PasswordScore.Fair (2) | Lowest PasswordScore that still counts as passable. |
minLength | 12 | Fewer code points than this adds TOO_SHORT (a critical weakness). |
requireUppercase | false | When true, a password with no A–Z adds NO_UPPERCASE. |
requireLowercase | false | When true, a password with no a–z adds NO_LOWERCASE. |
requireDigit | false | When true, a password with no 0–9 adds NO_DIGIT. |
requireSymbol | false | When true, a password with no non-alphanumeric character adds NO_SYMBOL. |
forbiddenWords | [] | Case-insensitive substrings; a match adds FORBIDDEN_WORD (a critical weakness). |
PasswordCheckContext
The optional second argument supplies per-user context the policy cannot know on its own.
interface PasswordCheckContext {
username?: string;
history?: string[];
}| Field | Type | Effect |
|---|---|---|
username | string | If the password contains it (case-insensitive), adds USERNAME_SUBSTRING. |
history | string[] | Prior passwords. A match adds REUSED_FROM_HISTORY. |
history is compared in the same NFC form the KDF derives from, not raw — two Unicode spellings of one password derive a byte-identical key, so reusing either counts as reuse. history is also guarded against non-array and non-string values so a malformed argument cannot escape the library's error contract.
Result and weakness codes
interface PasswordCheckResult {
readonly score: PasswordScore;
readonly entropyBits: number;
readonly weaknesses: ReadonlyArray<PasswordWeakness>;
readonly passable: boolean;
readonly summary: string;
}| Field | Type | Meaning |
|---|---|---|
score | PasswordScore | The graded strength, 0–4. |
entropyBits | number | Rough Shannon-style entropy estimate from length and character pool. |
weaknesses | ReadonlyArray<PasswordWeakness> | Every issue found, in check order. |
passable | boolean | Whether the password meets this policy. |
summary | string | A one-line human-readable summary. |
PasswordScore is a named enum that replaces bare 0..4 magic numbers:
| Member | Value |
|---|---|
PasswordScore.VeryWeak | 0 |
PasswordScore.Weak | 1 |
PasswordScore.Fair | 2 |
PasswordScore.Strong | 3 |
PasswordScore.VeryStrong | 4 |
Each PasswordWeakness is { code, severity, message, fix? }, where severity is one of "critical" | "warning" | "info" and fix is an optional remediation hint:
interface PasswordWeakness {
readonly code: PasswordWeaknessCode;
readonly severity: "critical" | "warning" | "info";
readonly message: string;
readonly fix?: string;
}PasswordWeaknessCode is the closed union below. Only critical codes can flip passable to false; warning and info codes lower the score but never reject on their own.
PasswordWeaknessCode | Severity | Raised when |
|---|---|---|
TOO_SHORT | critical | Shorter than minLength (measured after NFC normalization). |
USERNAME_SUBSTRING | critical | Contains the context.username. |
FORBIDDEN_WORD | critical | Contains one of forbiddenWords. |
REUSED_FROM_HISTORY | critical | Matches an entry in context.history. |
NO_UPPERCASE | warning | requireUppercase set and no uppercase letter. |
NO_LOWERCASE | warning | requireLowercase set and no lowercase letter. |
NO_DIGIT | warning | requireDigit set and no digit. |
NO_SYMBOL | warning | requireSymbol set and no symbol. |
LEADING_TRAILING_WHITESPACE | warning | Surrounding whitespace clipboard managers often add silently. |
MIXED_UNICODE_NORMALIZATION | warning | Not in NFC form; some IMEs re-normalize on paste. |
INVISIBLE_CHARACTERS | warning | Zero-width or BIDI control characters that aren't visible. |
REPEATED_CHARS | info | Four or more of the same character in a row. |
SEQUENTIAL_CHARS | info | A run of four or more sequential characters. |
NFC-normalized measurement
The KDF normalizes a password to NFC before deriving a key, so check measures the NFC form too — length, character-class rules, entropy, username and history comparisons all run against password.normalize("NFC"). A decomposed password is longer than its composed form; measuring the raw string would report a stronger secret than the one actually protecting the wallet, and could pass a length rule the derived form fails.
The three clipboard-hazard codes — LEADING_TRAILING_WHITESPACE, MIXED_UNICODE_NORMALIZATION, and INVISIBLE_CHARACTERS — deliberately test the original input, not the normalized form. A password that changes shape between machines is a real hazard whatever its strength, but these are warning-only: the policy detects and reports them, and never silently rewrites the password, because retroactive normalization would orphan already-sealed records.