use-password-policy 2.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/dist/core.mjs ADDED
@@ -0,0 +1,586 @@
1
+ // src/common-passwords.ts
2
+ var COMMON_PASSWORDS = [
3
+ // number sequences & repeats
4
+ "123456",
5
+ "1234567",
6
+ "12345678",
7
+ "123456789",
8
+ "1234567890",
9
+ "0123456789",
10
+ "111111",
11
+ "1111111",
12
+ "11111111",
13
+ "000000",
14
+ "00000000",
15
+ "123123",
16
+ "123321",
17
+ "654321",
18
+ "666666",
19
+ "777777",
20
+ "7777777",
21
+ "888888",
22
+ "88888888",
23
+ "999999",
24
+ "121212",
25
+ "112233",
26
+ "123654",
27
+ "987654321",
28
+ "147258369",
29
+ "159753",
30
+ "12341234",
31
+ "1q2w3e",
32
+ "1q2w3e4r",
33
+ "1q2w3e4r5t",
34
+ "1qaz2wsx",
35
+ "1qazxsw2",
36
+ "zaq12wsx",
37
+ // keyboard walks
38
+ "qwerty",
39
+ "qwertyu",
40
+ "qwertyuiop",
41
+ "qwe",
42
+ "qweasd",
43
+ "qweasdzxc",
44
+ "qwer",
45
+ "qazwsx",
46
+ "asdf",
47
+ "asdfgh",
48
+ "asdfghjkl",
49
+ "zxcvbn",
50
+ "zxcvbnm",
51
+ "azerty",
52
+ "qwertz",
53
+ "abc",
54
+ "abcd",
55
+ "abcde",
56
+ "abcdef",
57
+ "abcdefg",
58
+ "abcdefgh",
59
+ "aaaaaa",
60
+ // the classics
61
+ "password",
62
+ "pass",
63
+ "passwort",
64
+ "passpass",
65
+ "secret",
66
+ "letmein",
67
+ "welcome",
68
+ "admin",
69
+ "administrator",
70
+ "root",
71
+ "login",
72
+ "user",
73
+ "guest",
74
+ "test",
75
+ "default",
76
+ "changeme",
77
+ "access",
78
+ "master",
79
+ "trustno",
80
+ "whatever",
81
+ "nothing",
82
+ "unknown",
83
+ "iloveyou",
84
+ "loveme",
85
+ "love",
86
+ "lovely",
87
+ "hello",
88
+ "hellokitty",
89
+ "freedom",
90
+ "monkey",
91
+ "dragon",
92
+ "shadow",
93
+ "sunshine",
94
+ "princess",
95
+ "superman",
96
+ "batman",
97
+ "spiderman",
98
+ "starwars",
99
+ "pokemon",
100
+ "naruto",
101
+ "matrix",
102
+ "ninja",
103
+ "killer",
104
+ "football",
105
+ "baseball",
106
+ "basketball",
107
+ "soccer",
108
+ "hockey",
109
+ "golf",
110
+ "tennis",
111
+ "liverpool",
112
+ "arsenal",
113
+ "chelsea",
114
+ "barcelona",
115
+ "realmadrid",
116
+ "juventus",
117
+ "mustang",
118
+ "ferrari",
119
+ "porsche",
120
+ "harley",
121
+ "corvette",
122
+ "mercedes",
123
+ "computer",
124
+ "internet",
125
+ "google",
126
+ "apple",
127
+ "samsung",
128
+ "facebook",
129
+ "linkedin",
130
+ "myspace",
131
+ "youtube",
132
+ "twitter",
133
+ "instagram",
134
+ "microsoft",
135
+ "windows",
136
+ "summer",
137
+ "winter",
138
+ "spring",
139
+ "autumn",
140
+ "monday",
141
+ "friday",
142
+ "sunday",
143
+ "flower",
144
+ "cheese",
145
+ "cookie",
146
+ "chocolate",
147
+ "pepper",
148
+ "ginger",
149
+ "banana",
150
+ "orange",
151
+ "purple",
152
+ "silver",
153
+ "golden",
154
+ "diamond",
155
+ "money",
156
+ "angel",
157
+ "michael",
158
+ "jennifer",
159
+ "jessica",
160
+ "ashley",
161
+ "jordan",
162
+ "hunter",
163
+ "ranger",
164
+ "buster",
165
+ "tigger",
166
+ "charlie",
167
+ "robert",
168
+ "thomas",
169
+ "daniel",
170
+ "andrew",
171
+ "joshua",
172
+ "maggie",
173
+ "bailey",
174
+ "matthew",
175
+ "anthony"
176
+ ];
177
+
178
+ // src/pwned.ts
179
+ var HIBP_RANGE = "https://api.pwnedpasswords.com/range/";
180
+ var CACHE_LIMIT = 100;
181
+ var responseCache = /* @__PURE__ */ new WeakMap();
182
+ var inflight = /* @__PURE__ */ new WeakMap();
183
+ function inflightFor(fetchFn) {
184
+ let map = inflight.get(fetchFn);
185
+ if (!map) {
186
+ map = /* @__PURE__ */ new Map();
187
+ inflight.set(fetchFn, map);
188
+ }
189
+ return map;
190
+ }
191
+ function cacheFor(fetchFn) {
192
+ let cache = responseCache.get(fetchFn);
193
+ if (!cache) {
194
+ cache = /* @__PURE__ */ new Map();
195
+ responseCache.set(fetchFn, cache);
196
+ }
197
+ return cache;
198
+ }
199
+ async function sha1Hex(input) {
200
+ var _a;
201
+ const subtle = (_a = globalThis.crypto) == null ? void 0 : _a.subtle;
202
+ if (!subtle) {
203
+ throw new Error(
204
+ "use-password-policy: crypto.subtle is not available. It needs a secure context (https or localhost) in browsers, or Node 20+."
205
+ );
206
+ }
207
+ const digest = await subtle.digest("SHA-1", new TextEncoder().encode(input));
208
+ return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
209
+ }
210
+ async function checkPwnedPassword(password, options = {}) {
211
+ var _a, _b, _c;
212
+ if (!password) return 0;
213
+ const doFetch = (_a = options.fetch) != null ? _a : globalThis.fetch;
214
+ if (!doFetch) throw new Error("use-password-policy: no fetch implementation available.");
215
+ const hash = await sha1Hex(password);
216
+ const prefix = hash.slice(0, 5);
217
+ const suffix = hash.slice(5);
218
+ const url = `${(_b = options.endpoint) != null ? _b : HIBP_RANGE}${prefix}`;
219
+ const cache = cacheFor(doFetch);
220
+ let body = cache.get(url);
221
+ if (body === void 0) {
222
+ const pending = inflightFor(doFetch);
223
+ let request = pending.get(url);
224
+ if (!request) {
225
+ request = (async () => {
226
+ const res = await doFetch(url, { headers: options.padding ? { "Add-Padding": "true" } : void 0 });
227
+ if (!res.ok) throw new Error(`use-password-policy: breach check failed (HTTP ${res.status}).`);
228
+ const text = await res.text();
229
+ cache.set(url, text);
230
+ if (cache.size > CACHE_LIMIT) cache.delete(cache.keys().next().value);
231
+ return text;
232
+ })().finally(() => pending.delete(url));
233
+ pending.set(url, request);
234
+ }
235
+ body = await request;
236
+ if ((_c = options.signal) == null ? void 0 : _c.aborted) throw new DOMException("The check was cancelled.", "AbortError");
237
+ }
238
+ for (const line of body.split("\n")) {
239
+ const [lineSuffix, count] = line.trim().split(":");
240
+ if (lineSuffix === suffix) return parseInt(count, 10) || 0;
241
+ }
242
+ return 0;
243
+ }
244
+
245
+ // src/core.ts
246
+ var DEFAULT_OPTIONS = {
247
+ minLength: 8,
248
+ maxLength: 0,
249
+ lowercaseCheck: true,
250
+ uppercaseCheck: true,
251
+ numberCheck: true,
252
+ specialCharCheck: true,
253
+ commonPasswordCheck: false,
254
+ commonPasswords: COMMON_PASSWORDS,
255
+ patternCheck: false,
256
+ customRules: [],
257
+ messages: {},
258
+ lowercaseRegex: /[a-z]/,
259
+ uppercaseRegex: /[A-Z]/,
260
+ numberRegex: /\d/,
261
+ specialCharRegex: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/
262
+ };
263
+ var presets = {
264
+ /** The classic checklist (these are also the defaults): 8+ chars, upper, lower, number, symbol. */
265
+ classic: {
266
+ minLength: 8,
267
+ lowercaseCheck: true,
268
+ uppercaseCheck: true,
269
+ numberCheck: true,
270
+ specialCharCheck: true
271
+ },
272
+ /**
273
+ * NIST SP 800-63B-4 (Aug 2025) for passwords used on their own:
274
+ * at least 15 characters, allow up to 64, no composition rules, block common passwords
275
+ * and predictable patterns. Add `breachCheck: true` to also check known breaches.
276
+ */
277
+ nist: {
278
+ minLength: 15,
279
+ maxLength: 64,
280
+ lowercaseCheck: false,
281
+ uppercaseCheck: false,
282
+ numberCheck: false,
283
+ specialCharCheck: false,
284
+ commonPasswordCheck: true,
285
+ patternCheck: true
286
+ },
287
+ /** NIST SP 800-63B-4 when the password is one factor of MFA: at least 8 characters. */
288
+ nistMfa: {
289
+ minLength: 8,
290
+ maxLength: 64,
291
+ lowercaseCheck: false,
292
+ uppercaseCheck: false,
293
+ numberCheck: false,
294
+ specialCharCheck: false,
295
+ commonPasswordCheck: true,
296
+ patternCheck: true
297
+ }
298
+ };
299
+ var DEFAULT_MESSAGES = {
300
+ minLength: (o) => `At least ${o.minLength} characters`,
301
+ maxLength: (o) => `At most ${o.maxLength} characters`,
302
+ uppercase: "An uppercase letter",
303
+ lowercase: "A lowercase letter",
304
+ number: "A number",
305
+ specialChar: "A special character",
306
+ notCommon: "Not a commonly used password",
307
+ noPattern: "No repeats, sequences or keyboard patterns",
308
+ notBreached: "Not found in known data breaches",
309
+ breachUnavailable: "Couldn't check known data breaches. Try again",
310
+ match: "Passwords match",
311
+ strength: "Hard to guess"
312
+ };
313
+ var STRENGTH_LABELS = ["Very Weak", "Weak", "Medium", "Strong", "Very Strong"];
314
+ function resolveOptions(options = {}) {
315
+ const merged = { ...DEFAULT_OPTIONS };
316
+ for (const [key, value] of Object.entries(options)) {
317
+ if (key !== "password" && value !== void 0) merged[key] = value;
318
+ }
319
+ return merged;
320
+ }
321
+ var LEET = { "@": "a", "4": "a", "8": "b", "3": "e", "1": "i", "!": "i", "0": "o", "$": "s", "5": "s", "7": "t" };
322
+ var unleet = (s) => s.replace(/[@483!1$057]/g, (c) => {
323
+ var _a;
324
+ return (_a = LEET[c]) != null ? _a : c;
325
+ });
326
+ var commonSetCache = /* @__PURE__ */ new WeakMap();
327
+ function getCommonSet(list) {
328
+ let set = commonSetCache.get(list);
329
+ if (!set) {
330
+ set = new Set(list.map((p) => p.toLowerCase()));
331
+ commonSetCache.set(list, set);
332
+ }
333
+ return set;
334
+ }
335
+ var passwordLength = (password) => Array.from(password).length;
336
+ function isCommonCombo(word, set) {
337
+ const n = word.length;
338
+ if (n < 6) return false;
339
+ const reachable = new Array(n + 1).fill(false);
340
+ reachable[0] = true;
341
+ for (let end = 3; end <= n; end++) {
342
+ for (let start = Math.max(0, end - 20); start <= end - 3; start++) {
343
+ if (reachable[start] && set.has(word.slice(start, end))) {
344
+ reachable[end] = true;
345
+ break;
346
+ }
347
+ }
348
+ }
349
+ return reachable[n];
350
+ }
351
+ function isCommonPassword(password, list = COMMON_PASSWORDS) {
352
+ if (!password) return false;
353
+ const set = getCommonSet(list);
354
+ const lower = password.toLowerCase();
355
+ const noTrailingSymbols = lower.replace(/[^a-z0-9]+$/, "");
356
+ const trimmed = lower.replace(/[^a-z]+$/, "");
357
+ const base = unleet(trimmed);
358
+ const baseNoLeading = unleet(trimmed.replace(/^[^a-z]+/, ""));
359
+ if ([lower, noTrailingSymbols, base, baseNoLeading].some((c) => c.length > 0 && set.has(c))) return true;
360
+ const lettersOnly = lower.replace(/[^a-z]/g, "");
361
+ const unleetedLetters = unleet(baseNoLeading).replace(/[^a-z]/g, "");
362
+ return isCommonCombo(lettersOnly, set) || isCommonCombo(unleetedLetters, set);
363
+ }
364
+ var KEYBOARD_ROWS = ["1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm", "abcdefghijklmnopqrstuvwxyz"];
365
+ function isStep(a, b) {
366
+ if (a === b) return false;
367
+ for (const row of KEYBOARD_ROWS) {
368
+ const i = row.indexOf(a);
369
+ const j = row.indexOf(b);
370
+ if (i >= 0 && j >= 0 && (Math.abs(i - j) === 1 || Math.abs(i - j) === row.length - 1)) return true;
371
+ }
372
+ return false;
373
+ }
374
+ function isPredictablePattern(password) {
375
+ if (!password) return false;
376
+ if (password.trim() === "") return true;
377
+ const chars = Array.from(password.toLowerCase());
378
+ if (new Set(chars).size <= 3) return true;
379
+ const compact = chars.filter((c) => /[a-z0-9]/.test(c) || c.toUpperCase() !== c).join("");
380
+ for (const s of [compact, compact.replace(/\d+$/, "")]) {
381
+ if (s.length >= 6 && /^(.+?)\1+$/.test(s)) return true;
382
+ }
383
+ if (chars.length >= 4) {
384
+ let steps = 0;
385
+ for (let i = 1; i < chars.length; i++) if (isStep(chars[i - 1], chars[i])) steps++;
386
+ if (steps / (chars.length - 1) >= 0.75) return true;
387
+ }
388
+ return false;
389
+ }
390
+ var prettify = (name) => name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
391
+ function resolveMessage(rule, options) {
392
+ var _a, _b;
393
+ const msg = (_b = (_a = options.messages[rule.name]) != null ? _a : rule.message) != null ? _b : DEFAULT_MESSAGES[rule.name];
394
+ if (typeof msg === "function") return msg(options);
395
+ return msg != null ? msg : prettify(rule.name);
396
+ }
397
+ function builtInRules(options, estimate) {
398
+ const rules = [];
399
+ if (options.minLength > 0) {
400
+ rules.push({ name: "minLength", optionsKey: "minLength", test: (p, o) => passwordLength(p) >= o.minLength });
401
+ }
402
+ if (options.maxLength > 0) {
403
+ rules.push({ name: "maxLength", optionsKey: "maxLength", test: (p, o) => passwordLength(p) <= o.maxLength });
404
+ }
405
+ if (options.uppercaseCheck) {
406
+ rules.push({ name: "uppercase", optionsKey: "uppercaseCheck", test: (p, o) => o.uppercaseRegex.test(p) });
407
+ }
408
+ if (options.lowercaseCheck) {
409
+ rules.push({ name: "lowercase", optionsKey: "lowercaseCheck", test: (p, o) => o.lowercaseRegex.test(p) });
410
+ }
411
+ if (options.numberCheck) {
412
+ rules.push({ name: "number", optionsKey: "numberCheck", test: (p, o) => o.numberRegex.test(p) });
413
+ }
414
+ if (options.specialCharCheck) {
415
+ rules.push({ name: "specialChar", optionsKey: "specialCharCheck", test: (p, o) => o.specialCharRegex.test(p) });
416
+ }
417
+ if (options.commonPasswordCheck) {
418
+ rules.push({
419
+ name: "notCommon",
420
+ optionsKey: "commonPasswordCheck",
421
+ test: (p, o) => p.length > 0 && !isCommonPassword(p, o.commonPasswords)
422
+ });
423
+ }
424
+ if (options.patternCheck) {
425
+ rules.push({
426
+ name: "noPattern",
427
+ optionsKey: "patternCheck",
428
+ test: (p) => p.length > 0 && !isPredictablePattern(p)
429
+ });
430
+ }
431
+ if (options.confirmPassword !== void 0) {
432
+ rules.push({
433
+ name: "match",
434
+ optionsKey: "confirmPassword",
435
+ test: (p, o) => p.length > 0 && p === o.confirmPassword
436
+ });
437
+ }
438
+ if (estimate && options.minStrength !== void 0) {
439
+ const min = options.minStrength;
440
+ rules.push({ name: "strength", optionsKey: "minStrength", test: () => estimate.score >= min });
441
+ }
442
+ return rules;
443
+ }
444
+ function validatePassword(password, options = {}) {
445
+ const pw = password != null ? password : "";
446
+ const resolved = resolveOptions(options);
447
+ const estimate = resolved.strengthEstimator ? resolved.strengthEstimator(pw) : void 0;
448
+ const rules = [...builtInRules(resolved, estimate), ...resolved.customRules];
449
+ const policyState = {};
450
+ const requirements = [];
451
+ const errors = [];
452
+ let passedCount = 0;
453
+ for (const rule of rules) {
454
+ const passed = Boolean(rule.test(pw, resolved));
455
+ const message = resolveMessage(rule, resolved);
456
+ policyState[rule.name] = passed;
457
+ requirements.push({ name: rule.name, passed, message });
458
+ if (passed) passedCount++;
459
+ else errors.push(message);
460
+ }
461
+ const total = rules.length;
462
+ let strengthPercent;
463
+ let strengthLabel;
464
+ if (estimate) {
465
+ const score = Math.max(0, Math.min(4, Math.round(estimate.score)));
466
+ strengthPercent = pw.length === 0 ? 0 : score / 4;
467
+ strengthLabel = STRENGTH_LABELS[score];
468
+ } else {
469
+ strengthPercent = total > 0 ? passedCount / total : 0;
470
+ if (strengthPercent >= 1) strengthLabel = "Very Strong";
471
+ else if (strengthPercent >= 0.75) strengthLabel = "Strong";
472
+ else if (strengthPercent >= 0.5) strengthLabel = "Medium";
473
+ else if (strengthPercent > 0) strengthLabel = "Weak";
474
+ else strengthLabel = "Very Weak";
475
+ }
476
+ return {
477
+ isValid: total > 0 && passedCount === total,
478
+ policyState,
479
+ requirements,
480
+ errors,
481
+ strengthScore: passedCount,
482
+ strengthPercent,
483
+ strengthLabel,
484
+ ...estimate ? { estimate } : {}
485
+ };
486
+ }
487
+ function breachOptions(option) {
488
+ if (!option) return null;
489
+ return option === true ? {} : option;
490
+ }
491
+ function applyBreachResult(result, breach, options = {}) {
492
+ var _a;
493
+ const failOpen = ((_a = breachOptions(options.breachCheck)) != null ? _a : {}).failOpen !== false;
494
+ const resolved = resolveOptions(options);
495
+ const message = resolveMessage({ name: "notBreached", test: () => true }, resolved);
496
+ const pending = breach.status === "idle" || breach.status === "checking";
497
+ const passed = breach.status === "safe" || breach.status === "error" && failOpen;
498
+ const requirement = pending ? { name: "notBreached", passed: false, message, pending: true } : { name: "notBreached", passed, message };
499
+ const errors = [...result.errors];
500
+ if (!pending && !passed) {
501
+ errors.push(
502
+ breach.status === "error" ? resolveMessage({ name: "breachUnavailable", test: () => true }, resolved) : message
503
+ );
504
+ }
505
+ const pwned = breach.status === "pwned";
506
+ return {
507
+ ...result,
508
+ isValid: result.isValid && passed,
509
+ policyState: { ...result.policyState, notBreached: passed },
510
+ requirements: [...result.requirements.filter((r) => r.name !== "notBreached"), requirement],
511
+ errors,
512
+ strengthScore: result.strengthScore + (passed ? 1 : 0),
513
+ strengthPercent: pwned ? 0 : result.strengthPercent,
514
+ strengthLabel: pwned ? "Very Weak" : result.strengthLabel,
515
+ breach
516
+ };
517
+ }
518
+ async function validatePasswordAsync(password, options = {}) {
519
+ const result = validatePassword(password, options);
520
+ const b = breachOptions(options.breachCheck);
521
+ if (!b) return result;
522
+ if (!result.isValid) return applyBreachResult(result, { status: "idle", count: 0 }, options);
523
+ try {
524
+ const count = await checkPwnedPassword(password != null ? password : "", { fetch: b.fetch, endpoint: b.endpoint, padding: b.padding });
525
+ return applyBreachResult(result, { status: count > 0 ? "pwned" : "safe", count }, options);
526
+ } catch {
527
+ return applyBreachResult(result, { status: "error", count: 0 }, options);
528
+ }
529
+ }
530
+ function fromZxcvbn(zxcvbn, userInputs) {
531
+ const run = typeof zxcvbn === "function" ? zxcvbn : (pw, inputs) => zxcvbn.check(pw, inputs);
532
+ return (password) => {
533
+ var _a, _b, _c;
534
+ const result = run(password, userInputs);
535
+ const score = Math.max(0, Math.min(4, Math.round(result.score)));
536
+ const feedback = ((_a = result.feedback) == null ? void 0 : _a.warning) || ((_c = (_b = result.feedback) == null ? void 0 : _b.suggestions) == null ? void 0 : _c[0]) || void 0;
537
+ return feedback ? { score, feedback } : { score };
538
+ };
539
+ }
540
+ function zodPasswordRule(options = {}, { allErrors = true } = {}) {
541
+ return (value, ctx) => {
542
+ const { errors } = validatePassword(value, options);
543
+ for (const message of allErrors ? errors : errors.slice(0, 1)) {
544
+ ctx.addIssue({ code: "custom", message });
545
+ }
546
+ };
547
+ }
548
+ function zodPasswordRuleAsync(options = {}, { allErrors = true } = {}) {
549
+ return async (value, ctx) => {
550
+ const { errors } = await validatePasswordAsync(value, options);
551
+ for (const message of allErrors ? errors : errors.slice(0, 1)) {
552
+ ctx.addIssue({ code: "custom", message });
553
+ }
554
+ };
555
+ }
556
+ function passwordValidatorAsync(options = {}) {
557
+ return async (value) => {
558
+ const { errors } = await validatePasswordAsync(value != null ? value : "", options);
559
+ return errors.length === 0 ? true : errors[0];
560
+ };
561
+ }
562
+ function passwordValidator(options = {}) {
563
+ return (value) => {
564
+ const { errors } = validatePassword(value != null ? value : "", options);
565
+ return errors.length === 0 ? true : errors[0];
566
+ };
567
+ }
568
+ export {
569
+ COMMON_PASSWORDS,
570
+ DEFAULT_MESSAGES,
571
+ DEFAULT_OPTIONS,
572
+ applyBreachResult,
573
+ checkPwnedPassword,
574
+ fromZxcvbn,
575
+ isCommonPassword,
576
+ isPredictablePattern,
577
+ passwordLength,
578
+ passwordValidator,
579
+ passwordValidatorAsync,
580
+ presets,
581
+ resolveOptions,
582
+ validatePassword,
583
+ validatePasswordAsync,
584
+ zodPasswordRule,
585
+ zodPasswordRuleAsync
586
+ };