use-password-policy 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.1.0
4
+
5
+ ### Added
6
+ - **`patternCheck`** rejects predictable passwords: repeated characters (`aaaaaaaaaaaaaaa`), repeated chunks (`qwertyqwertyqwerty`, `dragon dragon dragon`), sequences and keyboard runs (`123456789012345`, `abcdefghijk`), and passwords made of only a few distinct characters, including all spaces. It's on in `presets.nist` and `presets.nistMfa`.
7
+ - **`breachCheck`** puts Have I Been Pwned into the normal result. The hook checks once the other rules pass and adds a `notBreached` requirement that is `pending` until answered. `isValid` waits for it, and the component shows it automatically. `failOpen` decides what happens when the service is down (default: let through).
8
+ - **`validatePasswordAsync()`** for servers, with the same result plus the breach check. Also `zodPasswordRuleAsync()`, `passwordValidatorAsync()` and `applyBreachResult()`.
9
+ - Results include `breach: { status, count }`, and requirements can be `pending`.
10
+ - `isPredictablePattern()` and `passwordLength()` helpers.
11
+
12
+ ### Fixed
13
+ - Length rules count Unicode code points, as NIST specifies. 8 emoji no longer pass a 15-character minimum.
14
+ - The common-password check now catches list words joined together (`passwordpassword`, `Summer2024!Summer`).
15
+ - `<PasswordPolicyInput />` calls `onPasswordChange` again when an async check settles.
16
+ - Repeated breach checks for the same hash prefix reuse the previous response.
17
+
3
18
  ## 3.0.0
4
19
 
5
20
  ### Added
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - A **NIST SP 800-63B** preset, a common-password blocklist and **Have I Been Pwned** breach checks
13
13
  - Optional **zxcvbn** scoring, so "strength" means how hard a password is to guess, not how many boxes it ticks
14
14
  - **Zod** and **react-hook-form** helpers
15
- - No runtime dependencies. About 3 KB gzipped for the core, about 5.5 KB with the React parts.
15
+ - No runtime dependencies. About 4 KB gzipped for the core, about 6.5 KB with the React parts.
16
16
 
17
17
  ### [➡️ Live demo & playground](https://rahulpatwa1303.github.io/use-password-policy/)
18
18
 
@@ -78,18 +78,20 @@ const { isValid, requirements, strengthLabel, strengthPercent } = usePasswordPol
78
78
  ```ts
79
79
  // password-policy.ts — shared by client and server
80
80
  import { presets, type PasswordPolicyOptions } from 'use-password-policy/core';
81
- export const policy: PasswordPolicyOptions = { ...presets.nist };
81
+ export const policy: PasswordPolicyOptions = { ...presets.nist, breachCheck: true };
82
82
  ```
83
83
 
84
84
  ```ts
85
85
  // api/sign-up.ts (Node, Next.js route handler, Express, Cloudflare Worker…)
86
- import { validatePassword } from 'use-password-policy/core';
86
+ import { validatePasswordAsync } from 'use-password-policy/core';
87
87
  import { policy } from './password-policy';
88
88
 
89
- const { isValid, errors } = validatePassword(body.password, policy);
89
+ const { isValid, errors } = await validatePasswordAsync(body.password, policy);
90
90
  if (!isValid) return Response.json({ errors }, { status: 400 });
91
91
  ```
92
92
 
93
+ Use `validatePasswordAsync` when the policy has `breachCheck`. Without it, the synchronous `validatePassword` returns the same result.
94
+
93
95
  `use-password-policy/core` doesn't import React, so it's safe in server bundles.
94
96
 
95
97
  ## Presets
@@ -102,13 +104,13 @@ usePasswordPolicy({ ...presets.nistMfa, password }); // NIST, password is
102
104
  usePasswordPolicy({ ...presets.classic, password }); // 8+ chars, upper, lower, number, symbol (the default)
103
105
  ```
104
106
 
105
- | Preset | Min | Max | Composition rules | Blocks common passwords |
106
- | --- | --- | --- | --- | --- |
107
- | `classic` (default) | 8 | – | upper, lower, number, symbol | no |
108
- | `nist` | 15 | 64 | none | yes |
109
- | `nistMfa` | 8 | 64 | none | yes |
107
+ | Preset | Min | Max | Composition rules | Blocks common passwords | Blocks patterns |
108
+ | --- | --- | --- | --- | --- | --- |
109
+ | `classic` (default) | 8 | – | upper, lower, number, symbol | no | no |
110
+ | `nist` | 15 | 64 | none | yes | yes |
111
+ | `nistMfa` | 8 | 64 | none | yes | yes |
110
112
 
111
- The NIST presets follow [SP 800-63B-4](https://pages.nist.gov/800-63-4/sp800-63b.html). It asks for length and a blocklist check, and says not to require "mixtures of different character types".
113
+ The NIST presets follow [SP 800-63B-4](https://pages.nist.gov/800-63-4/sp800-63b.html). It asks for length and a blocklist check, and says not to require "mixtures of different character types". Length is counted in Unicode code points, as NIST specifies, so an emoji counts as one character. To also check known breaches, add `breachCheck: true`.
112
114
 
113
115
  ## Security add-ons
114
116
 
@@ -118,19 +120,34 @@ The NIST presets follow [SP 800-63B-4](https://pages.nist.gov/800-63-4/sp800-63b
118
120
  usePasswordPolicy({ password, commonPasswordCheck: true });
119
121
  ```
120
122
 
121
- This uses a small built-in list of the most common passwords and base words. It also catches simple variations such as `Password123!`, `P@ssw0rd`, `123qwerty` and `Monkey!!`. Pass `commonPasswords: [...]` to use your own list, for example your product name.
123
+ This uses a small built-in list of the most common passwords and base words. It also catches simple variations such as `Password123!`, `P@ssw0rd`, `123qwerty` and `Monkey!!`, and list words joined together such as `passwordpassword` or `Summer2024!Summer`. Pass `commonPasswords: [...]` to use your own list, for example your product name.
124
+
125
+ ### Block predictable patterns
126
+
127
+ ```ts
128
+ usePasswordPolicy({ password, patternCheck: true }); // on in the NIST presets
129
+ ```
130
+
131
+ Rejects repeated characters (`aaaaaaaaaaaaaaa`), repeated chunks (`qwertyqwertyqwerty`, `dragon dragon dragon`), sequences and keyboard runs (`123456789012345`, `abcdefghijk`, `qwertyuiop`), and passwords made of only a few distinct characters, including all spaces.
122
132
 
123
133
  ### Check breached passwords (Have I Been Pwned)
124
134
 
125
135
  ```tsx
126
- import { usePwnedPassword } from 'use-password-policy';
127
-
128
- const pwned = usePwnedPassword(password, { enabled: isValid }); // debounced, cancels stale requests
129
- // pwned.status: 'idle' | 'checking' | 'safe' | 'pwned' | 'error'
130
- {pwned.isPwned && <p>Seen {pwned.count.toLocaleString()} times in data breaches. Pick another.</p>}
136
+ const { isValid, requirements, breach } = usePasswordPolicy({ ...presets.nist, breachCheck: true, password });
131
137
  ```
132
138
 
133
- On the server: `await checkPwnedPassword(password)` from `use-password-policy/core` returns the breach count.
139
+ With `breachCheck`, the check is part of the normal result:
140
+ - Once every other rule passes, the hook checks Have I Been Pwned (debounced, stale requests cancelled).
141
+ - A "Not found in known data breaches" requirement is `pending` until the check answers, and `isValid` stays `false` until then.
142
+ - A breached password fails with that message and is marked Very Weak. `breach` gives `{ status, count }`, e.g. `{ status: 'pwned', count: 10434004 }`.
143
+ - `<PasswordPolicyInput policyOptions={{ breachCheck: true }} />` shows it in the checklist automatically.
144
+
145
+ On the server, `await validatePasswordAsync(password, policy)` does the same.
146
+
147
+ If the service can't be reached, the password is let through by default and `breach.status` is `'error'`. Pass `breachCheck: { failOpen: false }` to block instead. Other settings: `debounceMs` (default 500), `fetch`, `endpoint`, `padding`.
148
+
149
+ For a custom flow, the lower-level `usePwnedPassword(password)` hook and `checkPwnedPassword(password)` (returns the breach count) are still available.
150
+
134
151
  Only the first 5 characters of the password's SHA-1 hash are sent ([k-anonymity](https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange)), never the password. It needs `crypto.subtle`, which means HTTPS or `localhost` in browsers and Node 20+ on the server.
135
152
 
136
153
  ### Real strength scoring with zxcvbn
@@ -168,7 +185,7 @@ const schema = z.object({ password: z.string().superRefine(zodPasswordRule(polic
168
185
  register('password', { validate: passwordValidator(policy) });
169
186
  ```
170
187
 
171
- Neither helper imports Zod or react-hook-form, so they add no dependencies.
188
+ With `breachCheck`, use the async versions, `zodPasswordRuleAsync(policy)` (with `parseAsync`) and `passwordValidatorAsync(policy)`, so the breach check runs too. None of these helpers import Zod or react-hook-form, so they add no dependencies.
172
189
 
173
190
  ## Confirm-password field
174
191
 
@@ -206,6 +223,8 @@ Rule names: `minLength`, `maxLength`, `uppercase`, `lowercase`, `number`, `speci
206
223
  | `numberCheck` | `boolean` | `true` | Require a digit. |
207
224
  | `specialCharCheck` | `boolean` | `true` | Require a special character. |
208
225
  | `commonPasswordCheck` | `boolean` | `false` | Reject common passwords. |
226
+ | `patternCheck` | `boolean` | `false` | Reject repeats, sequences and keyboard patterns. |
227
+ | `breachCheck` | `boolean \| { failOpen, debounceMs, fetch, endpoint, padding }` | `false` | Include the Have I Been Pwned check (hook and `validatePasswordAsync`). |
209
228
  | `commonPasswords` | `string[]` | built-in | Replace the blocklist. |
210
229
  | `confirmPassword` | `string` | – | Adds a `match` rule when set. |
211
230
  | `strengthEstimator` | `(pw) => { score, feedback? }` | – | For example `fromZxcvbn(zxcvbn)`. |
@@ -226,6 +245,9 @@ Rule names: `minLength`, `maxLength`, `uppercase`, `lowercase`, `number`, `speci
226
245
  | `strengthPercent` | `number` (0–1) | Fill for a meter. |
227
246
  | `strengthScore` | `number` | Number of rules passed. |
228
247
  | `estimate` | `{ score, feedback? }` | Only with `strengthEstimator`. |
248
+ | `breach` | `{ status, count }` | Only with `breachCheck`. `status` is `idle`, `checking`, `safe`, `pwned` or `error`. |
249
+
250
+ Each requirement is `{ name, passed, message, pending? }`. `pending` is `true` while the breach check hasn't answered.
229
251
 
230
252
  ### `<PasswordPolicyInput />` props
231
253
 
package/dist/core.d.mts CHANGED
@@ -9,6 +9,28 @@
9
9
  */
10
10
  declare const COMMON_PASSWORDS: readonly string[];
11
11
 
12
+ interface CheckPwnedOptions {
13
+ /** Abort the request (e.g. when the user keeps typing). */
14
+ signal?: AbortSignal;
15
+ /** Custom fetch implementation. Defaults to the global `fetch`. */
16
+ fetch?: typeof fetch;
17
+ /** Range API base URL. Defaults to Have I Been Pwned. */
18
+ endpoint?: string;
19
+ /**
20
+ * Ask HIBP to pad the response so its size doesn't hint at the result.
21
+ * Sends an `Add-Padding` header. Default `false`.
22
+ */
23
+ padding?: boolean;
24
+ }
25
+ /**
26
+ * How many times this password appears in known data breaches, using the
27
+ * Have I Been Pwned range API (k-anonymity). Only the first 5 characters of
28
+ * the password's SHA-1 hash leave the device — never the password itself.
29
+ *
30
+ * Resolves to `0` when the password was not found.
31
+ */
32
+ declare function checkPwnedPassword(password: string, options?: CheckPwnedOptions): Promise<number>;
33
+
12
34
  /** Built-in strength labels, weakest to strongest. */
13
35
  type StrengthLabel = 'Very Weak' | 'Weak' | 'Medium' | 'Strong' | 'Very Strong';
14
36
  /** Text for a rule: a plain string, or a function of the resolved options (handy for i18n). */
@@ -56,6 +78,19 @@ interface PasswordPolicyOptions {
56
78
  commonPasswordCheck?: boolean;
57
79
  /** Replace the built-in common-password list. */
58
80
  commonPasswords?: readonly string[];
81
+ /**
82
+ * Reject predictable patterns: repeated characters (`aaaaaaaa`), repeated chunks
83
+ * (`abcabcabc`), sequences and keyboard runs (`123456789`, `qwertyuiop`), and passwords
84
+ * made of only a few distinct characters (including all spaces). Default `false`;
85
+ * on in the NIST presets.
86
+ */
87
+ patternCheck?: boolean;
88
+ /**
89
+ * Check Have I Been Pwned as part of the result. The hook runs it automatically once
90
+ * every other rule passes; on the server use `validatePasswordAsync`. The synchronous
91
+ * `validatePassword` ignores this option. Default `false`.
92
+ */
93
+ breachCheck?: boolean | BreachCheckOptions;
59
94
  /**
60
95
  * When set (even to `''`), adds a `match` rule that passes only if
61
96
  * `password === confirmPassword`.
@@ -77,8 +112,34 @@ interface PasswordPolicyOptions {
77
112
  numberRegex?: RegExp;
78
113
  specialCharRegex?: RegExp;
79
114
  }
115
+ interface BreachCheckOptions {
116
+ /**
117
+ * What to do when the breach service can't be reached: `true` lets the password
118
+ * through (the requirement passes and `breach.status` is `'error'`), `false` blocks it.
119
+ * Default `true`.
120
+ */
121
+ failOpen?: boolean;
122
+ /** Hook only: wait this long after the last keystroke. Default `500` ms. */
123
+ debounceMs?: number;
124
+ /** Custom fetch implementation. */
125
+ fetch?: typeof fetch;
126
+ /** Range API base URL. Defaults to Have I Been Pwned. */
127
+ endpoint?: string;
128
+ /** Send the `Add-Padding` header. Default `false`. */
129
+ padding?: boolean;
130
+ }
131
+ type BreachStatus = 'idle' | 'checking' | 'safe' | 'pwned' | 'error';
132
+ interface BreachResult {
133
+ /**
134
+ * `idle`: not checked yet (other rules still failing, or no input).
135
+ * `checking`: request in flight. `safe` / `pwned`: answered. `error`: couldn't reach the service.
136
+ */
137
+ status: BreachStatus;
138
+ /** Times seen in breaches (0 unless `pwned`). */
139
+ count: number;
140
+ }
80
141
  /** Options after defaults are applied. Passed to every rule's `test`. */
81
- type ResolvedPolicyOptions = Required<Omit<PasswordPolicyOptions, 'password' | 'confirmPassword' | 'strengthEstimator' | 'minStrength'>> & Pick<PasswordPolicyOptions, 'confirmPassword' | 'strengthEstimator' | 'minStrength'>;
142
+ type ResolvedPolicyOptions = Required<Omit<PasswordPolicyOptions, 'password' | 'confirmPassword' | 'strengthEstimator' | 'minStrength' | 'breachCheck'>> & Pick<PasswordPolicyOptions, 'confirmPassword' | 'strengthEstimator' | 'minStrength' | 'breachCheck'>;
82
143
  /** @deprecated Use `ResolvedPolicyOptions`. */
83
144
  type PolicyDefaults = ResolvedPolicyOptions;
84
145
  /** Pass/fail for each active rule, keyed by rule name. */
@@ -90,6 +151,8 @@ interface Requirement {
90
151
  name: string;
91
152
  passed: boolean;
92
153
  message: string;
154
+ /** `true` while the result isn't known yet (the breach check before it has answered). */
155
+ pending?: boolean;
93
156
  }
94
157
  /** Result of `validatePassword` (and the hook). */
95
158
  interface ValidationResult {
@@ -108,34 +171,14 @@ interface ValidationResult {
108
171
  strengthLabel: StrengthLabel;
109
172
  /** The estimator's result, if `strengthEstimator` is set. */
110
173
  estimate?: StrengthEstimate;
174
+ /** Breach-check state, when `breachCheck` is on (hook and `validatePasswordAsync`). */
175
+ breach?: BreachResult;
111
176
  }
112
177
  /** What `usePasswordPolicy` returns. */
113
178
  interface HookReturnValue extends ValidationResult {
114
179
  password?: string;
115
180
  }
116
181
 
117
- interface CheckPwnedOptions {
118
- /** Abort the request (e.g. when the user keeps typing). */
119
- signal?: AbortSignal;
120
- /** Custom fetch implementation. Defaults to the global `fetch`. */
121
- fetch?: typeof fetch;
122
- /** Range API base URL. Defaults to Have I Been Pwned. */
123
- endpoint?: string;
124
- /**
125
- * Ask HIBP to pad the response so its size doesn't hint at the result.
126
- * Sends an `Add-Padding` header. Default `false`.
127
- */
128
- padding?: boolean;
129
- }
130
- /**
131
- * How many times this password appears in known data breaches, using the
132
- * Have I Been Pwned range API (k-anonymity). Only the first 5 characters of
133
- * the password's SHA-1 hash leave the device — never the password itself.
134
- *
135
- * Resolves to `0` when the password was not found.
136
- */
137
- declare function checkPwnedPassword(password: string, options?: CheckPwnedOptions): Promise<number>;
138
-
139
182
  /**
140
183
  * use-password-policy/core
141
184
  *
@@ -160,7 +203,8 @@ declare const presets: {
160
203
  };
161
204
  /**
162
205
  * NIST SP 800-63B-4 (Aug 2025) for passwords used on their own:
163
- * at least 15 characters, allow up to 64, no composition rules, block common passwords.
206
+ * at least 15 characters, allow up to 64, no composition rules, block common passwords
207
+ * and predictable patterns. Add `breachCheck: true` to also check known breaches.
164
208
  */
165
209
  nist: {
166
210
  minLength: number;
@@ -170,6 +214,7 @@ declare const presets: {
170
214
  numberCheck: false;
171
215
  specialCharCheck: false;
172
216
  commonPasswordCheck: true;
217
+ patternCheck: true;
173
218
  };
174
219
  /** NIST SP 800-63B-4 when the password is one factor of MFA: at least 8 characters. */
175
220
  nistMfa: {
@@ -180,17 +225,28 @@ declare const presets: {
180
225
  numberCheck: false;
181
226
  specialCharCheck: false;
182
227
  commonPasswordCheck: true;
228
+ patternCheck: true;
183
229
  };
184
230
  };
185
231
  declare const DEFAULT_MESSAGES: Record<string, RuleMessage>;
186
232
  /** Merge user options over the defaults, ignoring keys set to `undefined`. */
187
233
  declare function resolveOptions(options?: PasswordPolicyOptions): ResolvedPolicyOptions;
234
+ /** Length in Unicode code points, so an emoji counts as one character (as NIST specifies). */
235
+ declare const passwordLength: (password: string) => number;
188
236
  /**
189
- * `true` if the password is on the list, or is a list entry with simple
190
- * decoration: trailing digits/symbols ("password123!"), a leading number
191
- * ("123qwerty") or leet swaps ("p@ssw0rd").
237
+ * `true` if the password is on the list, is a list entry with simple decoration
238
+ * (trailing digits/symbols "password123!", a leading number "123qwerty", leet swaps
239
+ * "p@ssw0rd"), or is only list words joined together ("passwordpassword",
240
+ * "dragon dragon dragon", "Summer2024!Summer").
192
241
  */
193
242
  declare function isCommonPassword(password: string, list?: readonly string[]): boolean;
243
+ /**
244
+ * `true` for passwords that follow a predictable pattern: only whitespace, three or
245
+ * fewer distinct characters ("aaaaaaaa", "abababab"), a repeated chunk ("abcabcabc",
246
+ * "qwertyqwertyqwerty"), or mostly sequences and keyboard runs ("123456789012345",
247
+ * "abcdefghijk", "qwertyuiop", "987654321").
248
+ */
249
+ declare function isPredictablePattern(password: string): boolean;
194
250
  /**
195
251
  * Validate a password against a policy. Pure and synchronous.
196
252
  *
@@ -200,6 +256,22 @@ declare function isCommonPassword(password: string, list?: readonly string[]): b
200
256
  * ```
201
257
  */
202
258
  declare function validatePassword(password: string, options?: PasswordPolicyOptions): ValidationResult;
259
+ /**
260
+ * Fold a breach-check outcome into a validation result: adds a `notBreached`
261
+ * requirement (pending while unanswered), makes `isValid` depend on it, and marks a
262
+ * breached password Very Weak. The hook and `validatePasswordAsync` use this; you
263
+ * only need it for custom flows.
264
+ */
265
+ declare function applyBreachResult(result: ValidationResult, breach: BreachResult, options?: PasswordPolicyOptions): ValidationResult;
266
+ /**
267
+ * Like `validatePassword`, plus the Have I Been Pwned check when `breachCheck` is on.
268
+ * The breach lookup only runs once every other rule passes. Use this on the server.
269
+ *
270
+ * ```ts
271
+ * const { isValid, errors, breach } = await validatePasswordAsync(password, { ...presets.nist, breachCheck: true });
272
+ * ```
273
+ */
274
+ declare function validatePasswordAsync(password: string, options?: PasswordPolicyOptions): Promise<ValidationResult>;
203
275
  /** Anything shaped like the result of `zxcvbn(password)` or `@zxcvbn-ts/core`'s `zxcvbn(password)`. */
204
276
  interface ZxcvbnLikeResult {
205
277
  score: number;
@@ -241,6 +313,18 @@ interface ZodLikeRefinementCtx {
241
313
  declare function zodPasswordRule(options?: PasswordPolicyOptions, { allErrors }?: {
242
314
  allErrors?: boolean | undefined;
243
315
  }): (value: string, ctx: ZodLikeRefinementCtx) => void;
316
+ /**
317
+ * Async version of `zodPasswordRule` that also runs the breach check when
318
+ * `breachCheck` is on. Use it with `safeParseAsync` / `parseAsync`.
319
+ */
320
+ declare function zodPasswordRuleAsync(options?: PasswordPolicyOptions, { allErrors }?: {
321
+ allErrors?: boolean | undefined;
322
+ }): (value: string, ctx: ZodLikeRefinementCtx) => Promise<void>;
323
+ /**
324
+ * Async version of `passwordValidator` that also runs the breach check when
325
+ * `breachCheck` is on. react-hook-form accepts async `validate` functions.
326
+ */
327
+ declare function passwordValidatorAsync(options?: PasswordPolicyOptions): (value: string) => Promise<true | string>;
244
328
  /**
245
329
  * A validate function that returns `true` or the first error message.
246
330
  * Drops straight into react-hook-form's `validate`, and works with anything
@@ -252,4 +336,4 @@ declare function zodPasswordRule(options?: PasswordPolicyOptions, { allErrors }?
252
336
  */
253
337
  declare function passwordValidator(options?: PasswordPolicyOptions): (value: string) => true | string;
254
338
 
255
- export { COMMON_PASSWORDS, type CheckPwnedOptions, DEFAULT_MESSAGES, DEFAULT_OPTIONS, type HookReturnValue, type PasswordPolicyOptions, type PasswordPolicyState, type PolicyDefaults, type PolicyRule, type Requirement, type ResolvedPolicyOptions, type RuleMessage, type StrengthEstimate, type StrengthEstimator, type StrengthLabel, type ValidationResult, type ZodLikeRefinementCtx, type ZxcvbnLikeResult, checkPwnedPassword, fromZxcvbn, isCommonPassword, passwordValidator, presets, resolveOptions, validatePassword, zodPasswordRule };
339
+ export { type BreachCheckOptions, type BreachResult, type BreachStatus, COMMON_PASSWORDS, type CheckPwnedOptions, DEFAULT_MESSAGES, DEFAULT_OPTIONS, type HookReturnValue, type PasswordPolicyOptions, type PasswordPolicyState, type PolicyDefaults, type PolicyRule, type Requirement, type ResolvedPolicyOptions, type RuleMessage, type StrengthEstimate, type StrengthEstimator, type StrengthLabel, type ValidationResult, type ZodLikeRefinementCtx, type ZxcvbnLikeResult, applyBreachResult, checkPwnedPassword, fromZxcvbn, isCommonPassword, isPredictablePattern, passwordLength, passwordValidator, passwordValidatorAsync, presets, resolveOptions, validatePassword, validatePasswordAsync, zodPasswordRule, zodPasswordRuleAsync };
package/dist/core.d.ts CHANGED
@@ -9,6 +9,28 @@
9
9
  */
10
10
  declare const COMMON_PASSWORDS: readonly string[];
11
11
 
12
+ interface CheckPwnedOptions {
13
+ /** Abort the request (e.g. when the user keeps typing). */
14
+ signal?: AbortSignal;
15
+ /** Custom fetch implementation. Defaults to the global `fetch`. */
16
+ fetch?: typeof fetch;
17
+ /** Range API base URL. Defaults to Have I Been Pwned. */
18
+ endpoint?: string;
19
+ /**
20
+ * Ask HIBP to pad the response so its size doesn't hint at the result.
21
+ * Sends an `Add-Padding` header. Default `false`.
22
+ */
23
+ padding?: boolean;
24
+ }
25
+ /**
26
+ * How many times this password appears in known data breaches, using the
27
+ * Have I Been Pwned range API (k-anonymity). Only the first 5 characters of
28
+ * the password's SHA-1 hash leave the device — never the password itself.
29
+ *
30
+ * Resolves to `0` when the password was not found.
31
+ */
32
+ declare function checkPwnedPassword(password: string, options?: CheckPwnedOptions): Promise<number>;
33
+
12
34
  /** Built-in strength labels, weakest to strongest. */
13
35
  type StrengthLabel = 'Very Weak' | 'Weak' | 'Medium' | 'Strong' | 'Very Strong';
14
36
  /** Text for a rule: a plain string, or a function of the resolved options (handy for i18n). */
@@ -56,6 +78,19 @@ interface PasswordPolicyOptions {
56
78
  commonPasswordCheck?: boolean;
57
79
  /** Replace the built-in common-password list. */
58
80
  commonPasswords?: readonly string[];
81
+ /**
82
+ * Reject predictable patterns: repeated characters (`aaaaaaaa`), repeated chunks
83
+ * (`abcabcabc`), sequences and keyboard runs (`123456789`, `qwertyuiop`), and passwords
84
+ * made of only a few distinct characters (including all spaces). Default `false`;
85
+ * on in the NIST presets.
86
+ */
87
+ patternCheck?: boolean;
88
+ /**
89
+ * Check Have I Been Pwned as part of the result. The hook runs it automatically once
90
+ * every other rule passes; on the server use `validatePasswordAsync`. The synchronous
91
+ * `validatePassword` ignores this option. Default `false`.
92
+ */
93
+ breachCheck?: boolean | BreachCheckOptions;
59
94
  /**
60
95
  * When set (even to `''`), adds a `match` rule that passes only if
61
96
  * `password === confirmPassword`.
@@ -77,8 +112,34 @@ interface PasswordPolicyOptions {
77
112
  numberRegex?: RegExp;
78
113
  specialCharRegex?: RegExp;
79
114
  }
115
+ interface BreachCheckOptions {
116
+ /**
117
+ * What to do when the breach service can't be reached: `true` lets the password
118
+ * through (the requirement passes and `breach.status` is `'error'`), `false` blocks it.
119
+ * Default `true`.
120
+ */
121
+ failOpen?: boolean;
122
+ /** Hook only: wait this long after the last keystroke. Default `500` ms. */
123
+ debounceMs?: number;
124
+ /** Custom fetch implementation. */
125
+ fetch?: typeof fetch;
126
+ /** Range API base URL. Defaults to Have I Been Pwned. */
127
+ endpoint?: string;
128
+ /** Send the `Add-Padding` header. Default `false`. */
129
+ padding?: boolean;
130
+ }
131
+ type BreachStatus = 'idle' | 'checking' | 'safe' | 'pwned' | 'error';
132
+ interface BreachResult {
133
+ /**
134
+ * `idle`: not checked yet (other rules still failing, or no input).
135
+ * `checking`: request in flight. `safe` / `pwned`: answered. `error`: couldn't reach the service.
136
+ */
137
+ status: BreachStatus;
138
+ /** Times seen in breaches (0 unless `pwned`). */
139
+ count: number;
140
+ }
80
141
  /** Options after defaults are applied. Passed to every rule's `test`. */
81
- type ResolvedPolicyOptions = Required<Omit<PasswordPolicyOptions, 'password' | 'confirmPassword' | 'strengthEstimator' | 'minStrength'>> & Pick<PasswordPolicyOptions, 'confirmPassword' | 'strengthEstimator' | 'minStrength'>;
142
+ type ResolvedPolicyOptions = Required<Omit<PasswordPolicyOptions, 'password' | 'confirmPassword' | 'strengthEstimator' | 'minStrength' | 'breachCheck'>> & Pick<PasswordPolicyOptions, 'confirmPassword' | 'strengthEstimator' | 'minStrength' | 'breachCheck'>;
82
143
  /** @deprecated Use `ResolvedPolicyOptions`. */
83
144
  type PolicyDefaults = ResolvedPolicyOptions;
84
145
  /** Pass/fail for each active rule, keyed by rule name. */
@@ -90,6 +151,8 @@ interface Requirement {
90
151
  name: string;
91
152
  passed: boolean;
92
153
  message: string;
154
+ /** `true` while the result isn't known yet (the breach check before it has answered). */
155
+ pending?: boolean;
93
156
  }
94
157
  /** Result of `validatePassword` (and the hook). */
95
158
  interface ValidationResult {
@@ -108,34 +171,14 @@ interface ValidationResult {
108
171
  strengthLabel: StrengthLabel;
109
172
  /** The estimator's result, if `strengthEstimator` is set. */
110
173
  estimate?: StrengthEstimate;
174
+ /** Breach-check state, when `breachCheck` is on (hook and `validatePasswordAsync`). */
175
+ breach?: BreachResult;
111
176
  }
112
177
  /** What `usePasswordPolicy` returns. */
113
178
  interface HookReturnValue extends ValidationResult {
114
179
  password?: string;
115
180
  }
116
181
 
117
- interface CheckPwnedOptions {
118
- /** Abort the request (e.g. when the user keeps typing). */
119
- signal?: AbortSignal;
120
- /** Custom fetch implementation. Defaults to the global `fetch`. */
121
- fetch?: typeof fetch;
122
- /** Range API base URL. Defaults to Have I Been Pwned. */
123
- endpoint?: string;
124
- /**
125
- * Ask HIBP to pad the response so its size doesn't hint at the result.
126
- * Sends an `Add-Padding` header. Default `false`.
127
- */
128
- padding?: boolean;
129
- }
130
- /**
131
- * How many times this password appears in known data breaches, using the
132
- * Have I Been Pwned range API (k-anonymity). Only the first 5 characters of
133
- * the password's SHA-1 hash leave the device — never the password itself.
134
- *
135
- * Resolves to `0` when the password was not found.
136
- */
137
- declare function checkPwnedPassword(password: string, options?: CheckPwnedOptions): Promise<number>;
138
-
139
182
  /**
140
183
  * use-password-policy/core
141
184
  *
@@ -160,7 +203,8 @@ declare const presets: {
160
203
  };
161
204
  /**
162
205
  * NIST SP 800-63B-4 (Aug 2025) for passwords used on their own:
163
- * at least 15 characters, allow up to 64, no composition rules, block common passwords.
206
+ * at least 15 characters, allow up to 64, no composition rules, block common passwords
207
+ * and predictable patterns. Add `breachCheck: true` to also check known breaches.
164
208
  */
165
209
  nist: {
166
210
  minLength: number;
@@ -170,6 +214,7 @@ declare const presets: {
170
214
  numberCheck: false;
171
215
  specialCharCheck: false;
172
216
  commonPasswordCheck: true;
217
+ patternCheck: true;
173
218
  };
174
219
  /** NIST SP 800-63B-4 when the password is one factor of MFA: at least 8 characters. */
175
220
  nistMfa: {
@@ -180,17 +225,28 @@ declare const presets: {
180
225
  numberCheck: false;
181
226
  specialCharCheck: false;
182
227
  commonPasswordCheck: true;
228
+ patternCheck: true;
183
229
  };
184
230
  };
185
231
  declare const DEFAULT_MESSAGES: Record<string, RuleMessage>;
186
232
  /** Merge user options over the defaults, ignoring keys set to `undefined`. */
187
233
  declare function resolveOptions(options?: PasswordPolicyOptions): ResolvedPolicyOptions;
234
+ /** Length in Unicode code points, so an emoji counts as one character (as NIST specifies). */
235
+ declare const passwordLength: (password: string) => number;
188
236
  /**
189
- * `true` if the password is on the list, or is a list entry with simple
190
- * decoration: trailing digits/symbols ("password123!"), a leading number
191
- * ("123qwerty") or leet swaps ("p@ssw0rd").
237
+ * `true` if the password is on the list, is a list entry with simple decoration
238
+ * (trailing digits/symbols "password123!", a leading number "123qwerty", leet swaps
239
+ * "p@ssw0rd"), or is only list words joined together ("passwordpassword",
240
+ * "dragon dragon dragon", "Summer2024!Summer").
192
241
  */
193
242
  declare function isCommonPassword(password: string, list?: readonly string[]): boolean;
243
+ /**
244
+ * `true` for passwords that follow a predictable pattern: only whitespace, three or
245
+ * fewer distinct characters ("aaaaaaaa", "abababab"), a repeated chunk ("abcabcabc",
246
+ * "qwertyqwertyqwerty"), or mostly sequences and keyboard runs ("123456789012345",
247
+ * "abcdefghijk", "qwertyuiop", "987654321").
248
+ */
249
+ declare function isPredictablePattern(password: string): boolean;
194
250
  /**
195
251
  * Validate a password against a policy. Pure and synchronous.
196
252
  *
@@ -200,6 +256,22 @@ declare function isCommonPassword(password: string, list?: readonly string[]): b
200
256
  * ```
201
257
  */
202
258
  declare function validatePassword(password: string, options?: PasswordPolicyOptions): ValidationResult;
259
+ /**
260
+ * Fold a breach-check outcome into a validation result: adds a `notBreached`
261
+ * requirement (pending while unanswered), makes `isValid` depend on it, and marks a
262
+ * breached password Very Weak. The hook and `validatePasswordAsync` use this; you
263
+ * only need it for custom flows.
264
+ */
265
+ declare function applyBreachResult(result: ValidationResult, breach: BreachResult, options?: PasswordPolicyOptions): ValidationResult;
266
+ /**
267
+ * Like `validatePassword`, plus the Have I Been Pwned check when `breachCheck` is on.
268
+ * The breach lookup only runs once every other rule passes. Use this on the server.
269
+ *
270
+ * ```ts
271
+ * const { isValid, errors, breach } = await validatePasswordAsync(password, { ...presets.nist, breachCheck: true });
272
+ * ```
273
+ */
274
+ declare function validatePasswordAsync(password: string, options?: PasswordPolicyOptions): Promise<ValidationResult>;
203
275
  /** Anything shaped like the result of `zxcvbn(password)` or `@zxcvbn-ts/core`'s `zxcvbn(password)`. */
204
276
  interface ZxcvbnLikeResult {
205
277
  score: number;
@@ -241,6 +313,18 @@ interface ZodLikeRefinementCtx {
241
313
  declare function zodPasswordRule(options?: PasswordPolicyOptions, { allErrors }?: {
242
314
  allErrors?: boolean | undefined;
243
315
  }): (value: string, ctx: ZodLikeRefinementCtx) => void;
316
+ /**
317
+ * Async version of `zodPasswordRule` that also runs the breach check when
318
+ * `breachCheck` is on. Use it with `safeParseAsync` / `parseAsync`.
319
+ */
320
+ declare function zodPasswordRuleAsync(options?: PasswordPolicyOptions, { allErrors }?: {
321
+ allErrors?: boolean | undefined;
322
+ }): (value: string, ctx: ZodLikeRefinementCtx) => Promise<void>;
323
+ /**
324
+ * Async version of `passwordValidator` that also runs the breach check when
325
+ * `breachCheck` is on. react-hook-form accepts async `validate` functions.
326
+ */
327
+ declare function passwordValidatorAsync(options?: PasswordPolicyOptions): (value: string) => Promise<true | string>;
244
328
  /**
245
329
  * A validate function that returns `true` or the first error message.
246
330
  * Drops straight into react-hook-form's `validate`, and works with anything
@@ -252,4 +336,4 @@ declare function zodPasswordRule(options?: PasswordPolicyOptions, { allErrors }?
252
336
  */
253
337
  declare function passwordValidator(options?: PasswordPolicyOptions): (value: string) => true | string;
254
338
 
255
- export { COMMON_PASSWORDS, type CheckPwnedOptions, DEFAULT_MESSAGES, DEFAULT_OPTIONS, type HookReturnValue, type PasswordPolicyOptions, type PasswordPolicyState, type PolicyDefaults, type PolicyRule, type Requirement, type ResolvedPolicyOptions, type RuleMessage, type StrengthEstimate, type StrengthEstimator, type StrengthLabel, type ValidationResult, type ZodLikeRefinementCtx, type ZxcvbnLikeResult, checkPwnedPassword, fromZxcvbn, isCommonPassword, passwordValidator, presets, resolveOptions, validatePassword, zodPasswordRule };
339
+ export { type BreachCheckOptions, type BreachResult, type BreachStatus, COMMON_PASSWORDS, type CheckPwnedOptions, DEFAULT_MESSAGES, DEFAULT_OPTIONS, type HookReturnValue, type PasswordPolicyOptions, type PasswordPolicyState, type PolicyDefaults, type PolicyRule, type Requirement, type ResolvedPolicyOptions, type RuleMessage, type StrengthEstimate, type StrengthEstimator, type StrengthLabel, type ValidationResult, type ZodLikeRefinementCtx, type ZxcvbnLikeResult, applyBreachResult, checkPwnedPassword, fromZxcvbn, isCommonPassword, isPredictablePattern, passwordLength, passwordValidator, passwordValidatorAsync, presets, resolveOptions, validatePassword, validatePasswordAsync, zodPasswordRule, zodPasswordRuleAsync };