use-password-policy 2.0.0 → 3.0.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 +37 -0
- package/LICENSE +21 -0
- package/README.md +232 -135
- package/dist/core.d.mts +255 -0
- package/dist/core.d.ts +255 -0
- package/dist/core.js +468 -0
- package/dist/core.mjs +431 -0
- package/dist/index.d.mts +292 -9
- package/dist/index.d.ts +292 -9
- package/dist/index.js +635 -190
- package/dist/index.mjs +621 -188
- package/dist/styles.css +80 -0
- package/package.json +93 -27
package/dist/core.mjs
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
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
|
+
async function sha1Hex(input) {
|
|
181
|
+
var _a;
|
|
182
|
+
const subtle = (_a = globalThis.crypto) == null ? void 0 : _a.subtle;
|
|
183
|
+
if (!subtle) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"use-password-policy: crypto.subtle is not available. It needs a secure context (https or localhost) in browsers, or Node 20+."
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const digest = await subtle.digest("SHA-1", new TextEncoder().encode(input));
|
|
189
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
190
|
+
}
|
|
191
|
+
async function checkPwnedPassword(password, options = {}) {
|
|
192
|
+
var _a, _b;
|
|
193
|
+
if (!password) return 0;
|
|
194
|
+
const doFetch = (_a = options.fetch) != null ? _a : globalThis.fetch;
|
|
195
|
+
if (!doFetch) throw new Error("use-password-policy: no fetch implementation available.");
|
|
196
|
+
const hash = await sha1Hex(password);
|
|
197
|
+
const prefix = hash.slice(0, 5);
|
|
198
|
+
const suffix = hash.slice(5);
|
|
199
|
+
const res = await doFetch(`${(_b = options.endpoint) != null ? _b : HIBP_RANGE}${prefix}`, {
|
|
200
|
+
signal: options.signal,
|
|
201
|
+
headers: options.padding ? { "Add-Padding": "true" } : void 0
|
|
202
|
+
});
|
|
203
|
+
if (!res.ok) throw new Error(`use-password-policy: breach check failed (HTTP ${res.status}).`);
|
|
204
|
+
const body = await res.text();
|
|
205
|
+
for (const line of body.split("\n")) {
|
|
206
|
+
const [lineSuffix, count] = line.trim().split(":");
|
|
207
|
+
if (lineSuffix === suffix) return parseInt(count, 10) || 0;
|
|
208
|
+
}
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/core.ts
|
|
213
|
+
var DEFAULT_OPTIONS = {
|
|
214
|
+
minLength: 8,
|
|
215
|
+
maxLength: 0,
|
|
216
|
+
lowercaseCheck: true,
|
|
217
|
+
uppercaseCheck: true,
|
|
218
|
+
numberCheck: true,
|
|
219
|
+
specialCharCheck: true,
|
|
220
|
+
commonPasswordCheck: false,
|
|
221
|
+
commonPasswords: COMMON_PASSWORDS,
|
|
222
|
+
customRules: [],
|
|
223
|
+
messages: {},
|
|
224
|
+
lowercaseRegex: /[a-z]/,
|
|
225
|
+
uppercaseRegex: /[A-Z]/,
|
|
226
|
+
numberRegex: /\d/,
|
|
227
|
+
specialCharRegex: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/
|
|
228
|
+
};
|
|
229
|
+
var presets = {
|
|
230
|
+
/** The classic checklist (these are also the defaults): 8+ chars, upper, lower, number, symbol. */
|
|
231
|
+
classic: {
|
|
232
|
+
minLength: 8,
|
|
233
|
+
lowercaseCheck: true,
|
|
234
|
+
uppercaseCheck: true,
|
|
235
|
+
numberCheck: true,
|
|
236
|
+
specialCharCheck: true
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* NIST SP 800-63B-4 (Aug 2025) for passwords used on their own:
|
|
240
|
+
* at least 15 characters, allow up to 64, no composition rules, block common passwords.
|
|
241
|
+
*/
|
|
242
|
+
nist: {
|
|
243
|
+
minLength: 15,
|
|
244
|
+
maxLength: 64,
|
|
245
|
+
lowercaseCheck: false,
|
|
246
|
+
uppercaseCheck: false,
|
|
247
|
+
numberCheck: false,
|
|
248
|
+
specialCharCheck: false,
|
|
249
|
+
commonPasswordCheck: true
|
|
250
|
+
},
|
|
251
|
+
/** NIST SP 800-63B-4 when the password is one factor of MFA: at least 8 characters. */
|
|
252
|
+
nistMfa: {
|
|
253
|
+
minLength: 8,
|
|
254
|
+
maxLength: 64,
|
|
255
|
+
lowercaseCheck: false,
|
|
256
|
+
uppercaseCheck: false,
|
|
257
|
+
numberCheck: false,
|
|
258
|
+
specialCharCheck: false,
|
|
259
|
+
commonPasswordCheck: true
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var DEFAULT_MESSAGES = {
|
|
263
|
+
minLength: (o) => `At least ${o.minLength} characters`,
|
|
264
|
+
maxLength: (o) => `At most ${o.maxLength} characters`,
|
|
265
|
+
uppercase: "An uppercase letter",
|
|
266
|
+
lowercase: "A lowercase letter",
|
|
267
|
+
number: "A number",
|
|
268
|
+
specialChar: "A special character",
|
|
269
|
+
notCommon: "Not a commonly used password",
|
|
270
|
+
match: "Passwords match",
|
|
271
|
+
strength: "Hard to guess"
|
|
272
|
+
};
|
|
273
|
+
var STRENGTH_LABELS = ["Very Weak", "Weak", "Medium", "Strong", "Very Strong"];
|
|
274
|
+
function resolveOptions(options = {}) {
|
|
275
|
+
const merged = { ...DEFAULT_OPTIONS };
|
|
276
|
+
for (const [key, value] of Object.entries(options)) {
|
|
277
|
+
if (key !== "password" && value !== void 0) merged[key] = value;
|
|
278
|
+
}
|
|
279
|
+
return merged;
|
|
280
|
+
}
|
|
281
|
+
var LEET = { "@": "a", "4": "a", "8": "b", "3": "e", "1": "i", "!": "i", "0": "o", "$": "s", "5": "s", "7": "t" };
|
|
282
|
+
var unleet = (s) => s.replace(/[@483!1$057]/g, (c) => {
|
|
283
|
+
var _a;
|
|
284
|
+
return (_a = LEET[c]) != null ? _a : c;
|
|
285
|
+
});
|
|
286
|
+
var commonSetCache = /* @__PURE__ */ new WeakMap();
|
|
287
|
+
function getCommonSet(list) {
|
|
288
|
+
let set = commonSetCache.get(list);
|
|
289
|
+
if (!set) {
|
|
290
|
+
set = new Set(list.map((p) => p.toLowerCase()));
|
|
291
|
+
commonSetCache.set(list, set);
|
|
292
|
+
}
|
|
293
|
+
return set;
|
|
294
|
+
}
|
|
295
|
+
function isCommonPassword(password, list = COMMON_PASSWORDS) {
|
|
296
|
+
if (!password) return false;
|
|
297
|
+
const set = getCommonSet(list);
|
|
298
|
+
const lower = password.toLowerCase();
|
|
299
|
+
const noTrailingSymbols = lower.replace(/[^a-z0-9]+$/, "");
|
|
300
|
+
const trimmed = lower.replace(/[^a-z]+$/, "");
|
|
301
|
+
const base = unleet(trimmed);
|
|
302
|
+
const baseNoLeading = unleet(trimmed.replace(/^[^a-z]+/, ""));
|
|
303
|
+
return [lower, noTrailingSymbols, base, baseNoLeading].some((c) => c.length > 0 && set.has(c));
|
|
304
|
+
}
|
|
305
|
+
var prettify = (name) => name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
306
|
+
function resolveMessage(rule, options) {
|
|
307
|
+
var _a, _b;
|
|
308
|
+
const msg = (_b = (_a = options.messages[rule.name]) != null ? _a : rule.message) != null ? _b : DEFAULT_MESSAGES[rule.name];
|
|
309
|
+
if (typeof msg === "function") return msg(options);
|
|
310
|
+
return msg != null ? msg : prettify(rule.name);
|
|
311
|
+
}
|
|
312
|
+
function builtInRules(options, estimate) {
|
|
313
|
+
const rules = [];
|
|
314
|
+
if (options.minLength > 0) {
|
|
315
|
+
rules.push({ name: "minLength", optionsKey: "minLength", test: (p, o) => p.length >= o.minLength });
|
|
316
|
+
}
|
|
317
|
+
if (options.maxLength > 0) {
|
|
318
|
+
rules.push({ name: "maxLength", optionsKey: "maxLength", test: (p, o) => p.length <= o.maxLength });
|
|
319
|
+
}
|
|
320
|
+
if (options.uppercaseCheck) {
|
|
321
|
+
rules.push({ name: "uppercase", optionsKey: "uppercaseCheck", test: (p, o) => o.uppercaseRegex.test(p) });
|
|
322
|
+
}
|
|
323
|
+
if (options.lowercaseCheck) {
|
|
324
|
+
rules.push({ name: "lowercase", optionsKey: "lowercaseCheck", test: (p, o) => o.lowercaseRegex.test(p) });
|
|
325
|
+
}
|
|
326
|
+
if (options.numberCheck) {
|
|
327
|
+
rules.push({ name: "number", optionsKey: "numberCheck", test: (p, o) => o.numberRegex.test(p) });
|
|
328
|
+
}
|
|
329
|
+
if (options.specialCharCheck) {
|
|
330
|
+
rules.push({ name: "specialChar", optionsKey: "specialCharCheck", test: (p, o) => o.specialCharRegex.test(p) });
|
|
331
|
+
}
|
|
332
|
+
if (options.commonPasswordCheck) {
|
|
333
|
+
rules.push({
|
|
334
|
+
name: "notCommon",
|
|
335
|
+
optionsKey: "commonPasswordCheck",
|
|
336
|
+
test: (p, o) => p.length > 0 && !isCommonPassword(p, o.commonPasswords)
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
if (options.confirmPassword !== void 0) {
|
|
340
|
+
rules.push({
|
|
341
|
+
name: "match",
|
|
342
|
+
optionsKey: "confirmPassword",
|
|
343
|
+
test: (p, o) => p.length > 0 && p === o.confirmPassword
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (estimate && options.minStrength !== void 0) {
|
|
347
|
+
const min = options.minStrength;
|
|
348
|
+
rules.push({ name: "strength", optionsKey: "minStrength", test: () => estimate.score >= min });
|
|
349
|
+
}
|
|
350
|
+
return rules;
|
|
351
|
+
}
|
|
352
|
+
function validatePassword(password, options = {}) {
|
|
353
|
+
const pw = password != null ? password : "";
|
|
354
|
+
const resolved = resolveOptions(options);
|
|
355
|
+
const estimate = resolved.strengthEstimator ? resolved.strengthEstimator(pw) : void 0;
|
|
356
|
+
const rules = [...builtInRules(resolved, estimate), ...resolved.customRules];
|
|
357
|
+
const policyState = {};
|
|
358
|
+
const requirements = [];
|
|
359
|
+
const errors = [];
|
|
360
|
+
let passedCount = 0;
|
|
361
|
+
for (const rule of rules) {
|
|
362
|
+
const passed = Boolean(rule.test(pw, resolved));
|
|
363
|
+
const message = resolveMessage(rule, resolved);
|
|
364
|
+
policyState[rule.name] = passed;
|
|
365
|
+
requirements.push({ name: rule.name, passed, message });
|
|
366
|
+
if (passed) passedCount++;
|
|
367
|
+
else errors.push(message);
|
|
368
|
+
}
|
|
369
|
+
const total = rules.length;
|
|
370
|
+
let strengthPercent;
|
|
371
|
+
let strengthLabel;
|
|
372
|
+
if (estimate) {
|
|
373
|
+
const score = Math.max(0, Math.min(4, Math.round(estimate.score)));
|
|
374
|
+
strengthPercent = pw.length === 0 ? 0 : score / 4;
|
|
375
|
+
strengthLabel = STRENGTH_LABELS[score];
|
|
376
|
+
} else {
|
|
377
|
+
strengthPercent = total > 0 ? passedCount / total : 0;
|
|
378
|
+
if (strengthPercent >= 1) strengthLabel = "Very Strong";
|
|
379
|
+
else if (strengthPercent >= 0.75) strengthLabel = "Strong";
|
|
380
|
+
else if (strengthPercent >= 0.5) strengthLabel = "Medium";
|
|
381
|
+
else if (strengthPercent > 0) strengthLabel = "Weak";
|
|
382
|
+
else strengthLabel = "Very Weak";
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
isValid: total > 0 && passedCount === total,
|
|
386
|
+
policyState,
|
|
387
|
+
requirements,
|
|
388
|
+
errors,
|
|
389
|
+
strengthScore: passedCount,
|
|
390
|
+
strengthPercent,
|
|
391
|
+
strengthLabel,
|
|
392
|
+
...estimate ? { estimate } : {}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function fromZxcvbn(zxcvbn, userInputs) {
|
|
396
|
+
const run = typeof zxcvbn === "function" ? zxcvbn : (pw, inputs) => zxcvbn.check(pw, inputs);
|
|
397
|
+
return (password) => {
|
|
398
|
+
var _a, _b, _c;
|
|
399
|
+
const result = run(password, userInputs);
|
|
400
|
+
const score = Math.max(0, Math.min(4, Math.round(result.score)));
|
|
401
|
+
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;
|
|
402
|
+
return feedback ? { score, feedback } : { score };
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function zodPasswordRule(options = {}, { allErrors = true } = {}) {
|
|
406
|
+
return (value, ctx) => {
|
|
407
|
+
const { errors } = validatePassword(value, options);
|
|
408
|
+
for (const message of allErrors ? errors : errors.slice(0, 1)) {
|
|
409
|
+
ctx.addIssue({ code: "custom", message });
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function passwordValidator(options = {}) {
|
|
414
|
+
return (value) => {
|
|
415
|
+
const { errors } = validatePassword(value != null ? value : "", options);
|
|
416
|
+
return errors.length === 0 ? true : errors[0];
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
export {
|
|
420
|
+
COMMON_PASSWORDS,
|
|
421
|
+
DEFAULT_MESSAGES,
|
|
422
|
+
DEFAULT_OPTIONS,
|
|
423
|
+
checkPwnedPassword,
|
|
424
|
+
fromZxcvbn,
|
|
425
|
+
isCommonPassword,
|
|
426
|
+
passwordValidator,
|
|
427
|
+
presets,
|
|
428
|
+
resolveOptions,
|
|
429
|
+
validatePassword,
|
|
430
|
+
zodPasswordRule
|
|
431
|
+
};
|