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/index.mjs
CHANGED
|
@@ -1,240 +1,673 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
// src/use-password-policy.ts
|
|
2
|
-
import { useMemo } from "react";
|
|
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
|
-
|
|
4
|
+
import { useEffect, useMemo, useState } from "react";
|
|
5
|
+
|
|
6
|
+
// src/common-passwords.ts
|
|
7
|
+
var COMMON_PASSWORDS = [
|
|
8
|
+
// number sequences & repeats
|
|
9
|
+
"123456",
|
|
10
|
+
"1234567",
|
|
11
|
+
"12345678",
|
|
12
|
+
"123456789",
|
|
13
|
+
"1234567890",
|
|
14
|
+
"0123456789",
|
|
15
|
+
"111111",
|
|
16
|
+
"1111111",
|
|
17
|
+
"11111111",
|
|
18
|
+
"000000",
|
|
19
|
+
"00000000",
|
|
20
|
+
"123123",
|
|
21
|
+
"123321",
|
|
22
|
+
"654321",
|
|
23
|
+
"666666",
|
|
24
|
+
"777777",
|
|
25
|
+
"7777777",
|
|
26
|
+
"888888",
|
|
27
|
+
"88888888",
|
|
28
|
+
"999999",
|
|
29
|
+
"121212",
|
|
30
|
+
"112233",
|
|
31
|
+
"123654",
|
|
32
|
+
"987654321",
|
|
33
|
+
"147258369",
|
|
34
|
+
"159753",
|
|
35
|
+
"12341234",
|
|
36
|
+
"1q2w3e",
|
|
37
|
+
"1q2w3e4r",
|
|
38
|
+
"1q2w3e4r5t",
|
|
39
|
+
"1qaz2wsx",
|
|
40
|
+
"1qazxsw2",
|
|
41
|
+
"zaq12wsx",
|
|
42
|
+
// keyboard walks
|
|
43
|
+
"qwerty",
|
|
44
|
+
"qwertyu",
|
|
45
|
+
"qwertyuiop",
|
|
46
|
+
"qwe",
|
|
47
|
+
"qweasd",
|
|
48
|
+
"qweasdzxc",
|
|
49
|
+
"qwer",
|
|
50
|
+
"qazwsx",
|
|
51
|
+
"asdf",
|
|
52
|
+
"asdfgh",
|
|
53
|
+
"asdfghjkl",
|
|
54
|
+
"zxcvbn",
|
|
55
|
+
"zxcvbnm",
|
|
56
|
+
"azerty",
|
|
57
|
+
"qwertz",
|
|
58
|
+
"abc",
|
|
59
|
+
"abcd",
|
|
60
|
+
"abcde",
|
|
61
|
+
"abcdef",
|
|
62
|
+
"abcdefg",
|
|
63
|
+
"abcdefgh",
|
|
64
|
+
"aaaaaa",
|
|
65
|
+
// the classics
|
|
66
|
+
"password",
|
|
67
|
+
"pass",
|
|
68
|
+
"passwort",
|
|
69
|
+
"passpass",
|
|
70
|
+
"secret",
|
|
71
|
+
"letmein",
|
|
72
|
+
"welcome",
|
|
73
|
+
"admin",
|
|
74
|
+
"administrator",
|
|
75
|
+
"root",
|
|
76
|
+
"login",
|
|
77
|
+
"user",
|
|
78
|
+
"guest",
|
|
79
|
+
"test",
|
|
80
|
+
"default",
|
|
81
|
+
"changeme",
|
|
82
|
+
"access",
|
|
83
|
+
"master",
|
|
84
|
+
"trustno",
|
|
85
|
+
"whatever",
|
|
86
|
+
"nothing",
|
|
87
|
+
"unknown",
|
|
88
|
+
"iloveyou",
|
|
89
|
+
"loveme",
|
|
90
|
+
"love",
|
|
91
|
+
"lovely",
|
|
92
|
+
"hello",
|
|
93
|
+
"hellokitty",
|
|
94
|
+
"freedom",
|
|
95
|
+
"monkey",
|
|
96
|
+
"dragon",
|
|
97
|
+
"shadow",
|
|
98
|
+
"sunshine",
|
|
99
|
+
"princess",
|
|
100
|
+
"superman",
|
|
101
|
+
"batman",
|
|
102
|
+
"spiderman",
|
|
103
|
+
"starwars",
|
|
104
|
+
"pokemon",
|
|
105
|
+
"naruto",
|
|
106
|
+
"matrix",
|
|
107
|
+
"ninja",
|
|
108
|
+
"killer",
|
|
109
|
+
"football",
|
|
110
|
+
"baseball",
|
|
111
|
+
"basketball",
|
|
112
|
+
"soccer",
|
|
113
|
+
"hockey",
|
|
114
|
+
"golf",
|
|
115
|
+
"tennis",
|
|
116
|
+
"liverpool",
|
|
117
|
+
"arsenal",
|
|
118
|
+
"chelsea",
|
|
119
|
+
"barcelona",
|
|
120
|
+
"realmadrid",
|
|
121
|
+
"juventus",
|
|
122
|
+
"mustang",
|
|
123
|
+
"ferrari",
|
|
124
|
+
"porsche",
|
|
125
|
+
"harley",
|
|
126
|
+
"corvette",
|
|
127
|
+
"mercedes",
|
|
128
|
+
"computer",
|
|
129
|
+
"internet",
|
|
130
|
+
"google",
|
|
131
|
+
"apple",
|
|
132
|
+
"samsung",
|
|
133
|
+
"facebook",
|
|
134
|
+
"linkedin",
|
|
135
|
+
"myspace",
|
|
136
|
+
"youtube",
|
|
137
|
+
"twitter",
|
|
138
|
+
"instagram",
|
|
139
|
+
"microsoft",
|
|
140
|
+
"windows",
|
|
141
|
+
"summer",
|
|
142
|
+
"winter",
|
|
143
|
+
"spring",
|
|
144
|
+
"autumn",
|
|
145
|
+
"monday",
|
|
146
|
+
"friday",
|
|
147
|
+
"sunday",
|
|
148
|
+
"flower",
|
|
149
|
+
"cheese",
|
|
150
|
+
"cookie",
|
|
151
|
+
"chocolate",
|
|
152
|
+
"pepper",
|
|
153
|
+
"ginger",
|
|
154
|
+
"banana",
|
|
155
|
+
"orange",
|
|
156
|
+
"purple",
|
|
157
|
+
"silver",
|
|
158
|
+
"golden",
|
|
159
|
+
"diamond",
|
|
160
|
+
"money",
|
|
161
|
+
"angel",
|
|
162
|
+
"michael",
|
|
163
|
+
"jennifer",
|
|
164
|
+
"jessica",
|
|
165
|
+
"ashley",
|
|
166
|
+
"jordan",
|
|
167
|
+
"hunter",
|
|
168
|
+
"ranger",
|
|
169
|
+
"buster",
|
|
170
|
+
"tigger",
|
|
171
|
+
"charlie",
|
|
172
|
+
"robert",
|
|
173
|
+
"thomas",
|
|
174
|
+
"daniel",
|
|
175
|
+
"andrew",
|
|
176
|
+
"joshua",
|
|
177
|
+
"maggie",
|
|
178
|
+
"bailey",
|
|
179
|
+
"matthew",
|
|
180
|
+
"anthony"
|
|
29
181
|
];
|
|
182
|
+
|
|
183
|
+
// src/pwned.ts
|
|
184
|
+
var HIBP_RANGE = "https://api.pwnedpasswords.com/range/";
|
|
185
|
+
async function sha1Hex(input) {
|
|
186
|
+
var _a2;
|
|
187
|
+
const subtle = (_a2 = globalThis.crypto) == null ? void 0 : _a2.subtle;
|
|
188
|
+
if (!subtle) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"use-password-policy: crypto.subtle is not available. It needs a secure context (https or localhost) in browsers, or Node 20+."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const digest = await subtle.digest("SHA-1", new TextEncoder().encode(input));
|
|
194
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
195
|
+
}
|
|
196
|
+
async function checkPwnedPassword(password, options = {}) {
|
|
197
|
+
var _a2, _b;
|
|
198
|
+
if (!password) return 0;
|
|
199
|
+
const doFetch = (_a2 = options.fetch) != null ? _a2 : globalThis.fetch;
|
|
200
|
+
if (!doFetch) throw new Error("use-password-policy: no fetch implementation available.");
|
|
201
|
+
const hash = await sha1Hex(password);
|
|
202
|
+
const prefix = hash.slice(0, 5);
|
|
203
|
+
const suffix = hash.slice(5);
|
|
204
|
+
const res = await doFetch(`${(_b = options.endpoint) != null ? _b : HIBP_RANGE}${prefix}`, {
|
|
205
|
+
signal: options.signal,
|
|
206
|
+
headers: options.padding ? { "Add-Padding": "true" } : void 0
|
|
207
|
+
});
|
|
208
|
+
if (!res.ok) throw new Error(`use-password-policy: breach check failed (HTTP ${res.status}).`);
|
|
209
|
+
const body = await res.text();
|
|
210
|
+
for (const line of body.split("\n")) {
|
|
211
|
+
const [lineSuffix, count] = line.trim().split(":");
|
|
212
|
+
if (lineSuffix === suffix) return parseInt(count, 10) || 0;
|
|
213
|
+
}
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/core.ts
|
|
30
218
|
var DEFAULT_OPTIONS = {
|
|
31
219
|
minLength: 8,
|
|
220
|
+
maxLength: 0,
|
|
32
221
|
lowercaseCheck: true,
|
|
33
222
|
uppercaseCheck: true,
|
|
34
223
|
numberCheck: true,
|
|
35
224
|
specialCharCheck: true,
|
|
225
|
+
commonPasswordCheck: false,
|
|
226
|
+
commonPasswords: COMMON_PASSWORDS,
|
|
36
227
|
customRules: [],
|
|
228
|
+
messages: {},
|
|
37
229
|
lowercaseRegex: /[a-z]/,
|
|
38
230
|
uppercaseRegex: /[A-Z]/,
|
|
39
231
|
numberRegex: /\d/,
|
|
40
|
-
specialCharRegex: /[!@#$%^&*()_
|
|
232
|
+
specialCharRegex: /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?`~]/
|
|
41
233
|
};
|
|
42
|
-
var
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
234
|
+
var presets = {
|
|
235
|
+
/** The classic checklist (these are also the defaults): 8+ chars, upper, lower, number, symbol. */
|
|
236
|
+
classic: {
|
|
237
|
+
minLength: 8,
|
|
238
|
+
lowercaseCheck: true,
|
|
239
|
+
uppercaseCheck: true,
|
|
240
|
+
numberCheck: true,
|
|
241
|
+
specialCharCheck: true
|
|
242
|
+
},
|
|
243
|
+
/**
|
|
244
|
+
* NIST SP 800-63B-4 (Aug 2025) for passwords used on their own:
|
|
245
|
+
* at least 15 characters, allow up to 64, no composition rules, block common passwords.
|
|
246
|
+
*/
|
|
247
|
+
nist: {
|
|
248
|
+
minLength: 15,
|
|
249
|
+
maxLength: 64,
|
|
250
|
+
lowercaseCheck: false,
|
|
251
|
+
uppercaseCheck: false,
|
|
252
|
+
numberCheck: false,
|
|
253
|
+
specialCharCheck: false,
|
|
254
|
+
commonPasswordCheck: true
|
|
255
|
+
},
|
|
256
|
+
/** NIST SP 800-63B-4 when the password is one factor of MFA: at least 8 characters. */
|
|
257
|
+
nistMfa: {
|
|
258
|
+
minLength: 8,
|
|
259
|
+
maxLength: 64,
|
|
260
|
+
lowercaseCheck: false,
|
|
261
|
+
uppercaseCheck: false,
|
|
262
|
+
numberCheck: false,
|
|
263
|
+
specialCharCheck: false,
|
|
264
|
+
commonPasswordCheck: true
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
var DEFAULT_MESSAGES = {
|
|
268
|
+
minLength: (o) => `At least ${o.minLength} characters`,
|
|
269
|
+
maxLength: (o) => `At most ${o.maxLength} characters`,
|
|
270
|
+
uppercase: "An uppercase letter",
|
|
271
|
+
lowercase: "A lowercase letter",
|
|
272
|
+
number: "A number",
|
|
273
|
+
specialChar: "A special character",
|
|
274
|
+
notCommon: "Not a commonly used password",
|
|
275
|
+
match: "Passwords match",
|
|
276
|
+
strength: "Hard to guess"
|
|
277
|
+
};
|
|
278
|
+
var STRENGTH_LABELS = ["Very Weak", "Weak", "Medium", "Strong", "Very Strong"];
|
|
279
|
+
function resolveOptions(options = {}) {
|
|
280
|
+
const merged = { ...DEFAULT_OPTIONS };
|
|
281
|
+
for (const [key, value] of Object.entries(options)) {
|
|
282
|
+
if (key !== "password" && value !== void 0) merged[key] = value;
|
|
283
|
+
}
|
|
284
|
+
return merged;
|
|
285
|
+
}
|
|
286
|
+
var LEET = { "@": "a", "4": "a", "8": "b", "3": "e", "1": "i", "!": "i", "0": "o", "$": "s", "5": "s", "7": "t" };
|
|
287
|
+
var unleet = (s) => s.replace(/[@483!1$057]/g, (c) => {
|
|
288
|
+
var _a2;
|
|
289
|
+
return (_a2 = LEET[c]) != null ? _a2 : c;
|
|
290
|
+
});
|
|
291
|
+
var commonSetCache = /* @__PURE__ */ new WeakMap();
|
|
292
|
+
function getCommonSet(list) {
|
|
293
|
+
let set = commonSetCache.get(list);
|
|
294
|
+
if (!set) {
|
|
295
|
+
set = new Set(list.map((p) => p.toLowerCase()));
|
|
296
|
+
commonSetCache.set(list, set);
|
|
297
|
+
}
|
|
298
|
+
return set;
|
|
299
|
+
}
|
|
300
|
+
function isCommonPassword(password, list = COMMON_PASSWORDS) {
|
|
301
|
+
if (!password) return false;
|
|
302
|
+
const set = getCommonSet(list);
|
|
303
|
+
const lower = password.toLowerCase();
|
|
304
|
+
const noTrailingSymbols = lower.replace(/[^a-z0-9]+$/, "");
|
|
305
|
+
const trimmed = lower.replace(/[^a-z]+$/, "");
|
|
306
|
+
const base = unleet(trimmed);
|
|
307
|
+
const baseNoLeading = unleet(trimmed.replace(/^[^a-z]+/, ""));
|
|
308
|
+
return [lower, noTrailingSymbols, base, baseNoLeading].some((c) => c.length > 0 && set.has(c));
|
|
309
|
+
}
|
|
310
|
+
var prettify = (name) => name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()).trim();
|
|
311
|
+
function resolveMessage(rule, options) {
|
|
312
|
+
var _a2, _b;
|
|
313
|
+
const msg = (_b = (_a2 = options.messages[rule.name]) != null ? _a2 : rule.message) != null ? _b : DEFAULT_MESSAGES[rule.name];
|
|
314
|
+
if (typeof msg === "function") return msg(options);
|
|
315
|
+
return msg != null ? msg : prettify(rule.name);
|
|
316
|
+
}
|
|
317
|
+
function builtInRules(options, estimate) {
|
|
318
|
+
const rules = [];
|
|
319
|
+
if (options.minLength > 0) {
|
|
320
|
+
rules.push({ name: "minLength", optionsKey: "minLength", test: (p, o) => p.length >= o.minLength });
|
|
321
|
+
}
|
|
322
|
+
if (options.maxLength > 0) {
|
|
323
|
+
rules.push({ name: "maxLength", optionsKey: "maxLength", test: (p, o) => p.length <= o.maxLength });
|
|
324
|
+
}
|
|
325
|
+
if (options.uppercaseCheck) {
|
|
326
|
+
rules.push({ name: "uppercase", optionsKey: "uppercaseCheck", test: (p, o) => o.uppercaseRegex.test(p) });
|
|
327
|
+
}
|
|
328
|
+
if (options.lowercaseCheck) {
|
|
329
|
+
rules.push({ name: "lowercase", optionsKey: "lowercaseCheck", test: (p, o) => o.lowercaseRegex.test(p) });
|
|
330
|
+
}
|
|
331
|
+
if (options.numberCheck) {
|
|
332
|
+
rules.push({ name: "number", optionsKey: "numberCheck", test: (p, o) => o.numberRegex.test(p) });
|
|
333
|
+
}
|
|
334
|
+
if (options.specialCharCheck) {
|
|
335
|
+
rules.push({ name: "specialChar", optionsKey: "specialCharCheck", test: (p, o) => o.specialCharRegex.test(p) });
|
|
336
|
+
}
|
|
337
|
+
if (options.commonPasswordCheck) {
|
|
338
|
+
rules.push({
|
|
339
|
+
name: "notCommon",
|
|
340
|
+
optionsKey: "commonPasswordCheck",
|
|
341
|
+
test: (p, o) => p.length > 0 && !isCommonPassword(p, o.commonPasswords)
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (options.confirmPassword !== void 0) {
|
|
345
|
+
rules.push({
|
|
346
|
+
name: "match",
|
|
347
|
+
optionsKey: "confirmPassword",
|
|
348
|
+
test: (p, o) => p.length > 0 && p === o.confirmPassword
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (estimate && options.minStrength !== void 0) {
|
|
352
|
+
const min = options.minStrength;
|
|
353
|
+
rules.push({ name: "strength", optionsKey: "minStrength", test: () => estimate.score >= min });
|
|
354
|
+
}
|
|
355
|
+
return rules;
|
|
356
|
+
}
|
|
357
|
+
function validatePassword(password, options = {}) {
|
|
358
|
+
const pw = password != null ? password : "";
|
|
359
|
+
const resolved = resolveOptions(options);
|
|
360
|
+
const estimate = resolved.strengthEstimator ? resolved.strengthEstimator(pw) : void 0;
|
|
361
|
+
const rules = [...builtInRules(resolved, estimate), ...resolved.customRules];
|
|
362
|
+
const policyState = {};
|
|
363
|
+
const requirements = [];
|
|
364
|
+
const errors = [];
|
|
365
|
+
let passedCount = 0;
|
|
366
|
+
for (const rule of rules) {
|
|
367
|
+
const passed = Boolean(rule.test(pw, resolved));
|
|
368
|
+
const message = resolveMessage(rule, resolved);
|
|
369
|
+
policyState[rule.name] = passed;
|
|
370
|
+
requirements.push({ name: rule.name, passed, message });
|
|
371
|
+
if (passed) passedCount++;
|
|
372
|
+
else errors.push(message);
|
|
373
|
+
}
|
|
374
|
+
const total = rules.length;
|
|
375
|
+
let strengthPercent;
|
|
376
|
+
let strengthLabel;
|
|
377
|
+
if (estimate) {
|
|
378
|
+
const score = Math.max(0, Math.min(4, Math.round(estimate.score)));
|
|
379
|
+
strengthPercent = pw.length === 0 ? 0 : score / 4;
|
|
380
|
+
strengthLabel = STRENGTH_LABELS[score];
|
|
381
|
+
} else {
|
|
382
|
+
strengthPercent = total > 0 ? passedCount / total : 0;
|
|
383
|
+
if (strengthPercent >= 1) strengthLabel = "Very Strong";
|
|
384
|
+
else if (strengthPercent >= 0.75) strengthLabel = "Strong";
|
|
385
|
+
else if (strengthPercent >= 0.5) strengthLabel = "Medium";
|
|
386
|
+
else if (strengthPercent > 0) strengthLabel = "Weak";
|
|
387
|
+
else strengthLabel = "Very Weak";
|
|
388
|
+
}
|
|
82
389
|
return {
|
|
83
|
-
|
|
390
|
+
isValid: total > 0 && passedCount === total,
|
|
84
391
|
policyState,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
392
|
+
requirements,
|
|
393
|
+
errors,
|
|
394
|
+
strengthScore: passedCount,
|
|
395
|
+
strengthPercent,
|
|
396
|
+
strengthLabel,
|
|
397
|
+
...estimate ? { estimate } : {}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function fromZxcvbn(zxcvbn, userInputs) {
|
|
401
|
+
const run = typeof zxcvbn === "function" ? zxcvbn : (pw, inputs) => zxcvbn.check(pw, inputs);
|
|
402
|
+
return (password) => {
|
|
403
|
+
var _a2, _b, _c;
|
|
404
|
+
const result = run(password, userInputs);
|
|
405
|
+
const score = Math.max(0, Math.min(4, Math.round(result.score)));
|
|
406
|
+
const feedback = ((_a2 = result.feedback) == null ? void 0 : _a2.warning) || ((_c = (_b = result.feedback) == null ? void 0 : _b.suggestions) == null ? void 0 : _c[0]) || void 0;
|
|
407
|
+
return feedback ? { score, feedback } : { score };
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function zodPasswordRule(options = {}, { allErrors = true } = {}) {
|
|
411
|
+
return (value, ctx) => {
|
|
412
|
+
const { errors } = validatePassword(value, options);
|
|
413
|
+
for (const message of allErrors ? errors : errors.slice(0, 1)) {
|
|
414
|
+
ctx.addIssue({ code: "custom", message });
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function passwordValidator(options = {}) {
|
|
419
|
+
return (value) => {
|
|
420
|
+
const { errors } = validatePassword(value != null ? value : "", options);
|
|
421
|
+
return errors.length === 0 ? true : errors[0];
|
|
88
422
|
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/use-password-policy.ts
|
|
426
|
+
var usePasswordPolicy = (options = {}) => {
|
|
427
|
+
const { password = "" } = options;
|
|
428
|
+
const result = useMemo(() => validatePassword(password, options), [password, options]);
|
|
429
|
+
return { password, ...result };
|
|
89
430
|
};
|
|
431
|
+
function usePwnedPassword(password, options = {}) {
|
|
432
|
+
const { enabled = true, debounceMs = 500, fetch: customFetch, endpoint, padding } = options;
|
|
433
|
+
const [state, setState] = useState({ status: "idle", count: 0, isPwned: false });
|
|
434
|
+
useEffect(() => {
|
|
435
|
+
if (!enabled || !password) {
|
|
436
|
+
setState({ status: "idle", count: 0, isPwned: false });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const controller = new AbortController();
|
|
440
|
+
setState((s) => s.status === "checking" ? s : { status: "checking", count: 0, isPwned: false });
|
|
441
|
+
const timer = setTimeout(() => {
|
|
442
|
+
checkPwnedPassword(password, { signal: controller.signal, fetch: customFetch, endpoint, padding }).then((count) => {
|
|
443
|
+
if (!controller.signal.aborted) {
|
|
444
|
+
setState({ status: count > 0 ? "pwned" : "safe", count, isPwned: count > 0 });
|
|
445
|
+
}
|
|
446
|
+
}).catch((error) => {
|
|
447
|
+
if (!controller.signal.aborted) setState({ status: "error", count: 0, isPwned: false, error });
|
|
448
|
+
});
|
|
449
|
+
}, debounceMs);
|
|
450
|
+
return () => {
|
|
451
|
+
clearTimeout(timer);
|
|
452
|
+
controller.abort();
|
|
453
|
+
};
|
|
454
|
+
}, [password, enabled, debounceMs, customFetch, endpoint, padding]);
|
|
455
|
+
return state;
|
|
456
|
+
}
|
|
90
457
|
|
|
91
458
|
// src/PasswordPolicyInput.tsx
|
|
92
|
-
import
|
|
93
|
-
import
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"Medium": "var(--rpp-medium)",
|
|
99
|
-
"Strong": "var(--rpp-success)",
|
|
100
|
-
"Very Strong": "var(--rpp-success)"
|
|
101
|
-
};
|
|
102
|
-
var Container = styled.div`
|
|
103
|
-
/* CSS variables define the component's internal theme */
|
|
459
|
+
import * as React from "react";
|
|
460
|
+
import { forwardRef, useState as useState2 } from "react";
|
|
461
|
+
|
|
462
|
+
// src/styles.ts
|
|
463
|
+
var passwordPolicyInputCss = `
|
|
464
|
+
:where(.rpp-root) {
|
|
104
465
|
--rpp-accent: #646cff;
|
|
105
|
-
--rpp-success: #
|
|
466
|
+
--rpp-success: #1e8449;
|
|
106
467
|
--rpp-danger: #c0392b;
|
|
107
|
-
--rpp-weak: #
|
|
108
|
-
--rpp-medium: #
|
|
109
|
-
--rpp-bg: #
|
|
110
|
-
--rpp-border: #
|
|
111
|
-
--rpp-text: #
|
|
112
|
-
|
|
113
|
-
|
|
468
|
+
--rpp-weak: #d35400;
|
|
469
|
+
--rpp-medium: #b8860b;
|
|
470
|
+
--rpp-bg: #ffffff;
|
|
471
|
+
--rpp-border: #d0d0d0;
|
|
472
|
+
--rpp-text: #222222;
|
|
473
|
+
--rpp-muted: #6b6b6b;
|
|
474
|
+
--rpp-radius: 6px;
|
|
475
|
+
font-family: inherit;
|
|
476
|
+
color: var(--rpp-text);
|
|
114
477
|
display: flex;
|
|
115
478
|
flex-direction: column;
|
|
116
479
|
gap: 0.75rem;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`;
|
|
121
|
-
var Input = styled.input`
|
|
480
|
+
}
|
|
481
|
+
:where(.rpp-root) .rpp-field { position: relative; }
|
|
482
|
+
:where(.rpp-root) .rpp-input {
|
|
122
483
|
box-sizing: border-box;
|
|
123
484
|
width: 100%;
|
|
124
|
-
padding: 0.75rem;
|
|
485
|
+
padding: 0.75rem 2.75rem 0.75rem 0.75rem;
|
|
125
486
|
border: 1px solid var(--rpp-border);
|
|
126
|
-
border-radius:
|
|
487
|
+
border-radius: var(--rpp-radius);
|
|
488
|
+
background: var(--rpp-bg);
|
|
489
|
+
color: var(--rpp-text);
|
|
490
|
+
font: inherit;
|
|
127
491
|
font-size: 1rem;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
492
|
+
}
|
|
493
|
+
:where(.rpp-root) .rpp-input:focus-visible {
|
|
494
|
+
outline: 2px solid var(--rpp-accent);
|
|
495
|
+
outline-offset: 1px;
|
|
496
|
+
border-color: var(--rpp-accent);
|
|
497
|
+
}
|
|
498
|
+
:where(.rpp-root) .rpp-toggle {
|
|
135
499
|
position: absolute;
|
|
136
500
|
top: 50%;
|
|
137
|
-
right: 0.
|
|
501
|
+
right: 0.375rem;
|
|
138
502
|
transform: translateY(-50%);
|
|
139
|
-
background: none;
|
|
140
|
-
border: none;
|
|
141
|
-
cursor: pointer;
|
|
142
|
-
padding: 0.5rem;
|
|
143
|
-
color: #888;
|
|
144
503
|
display: flex;
|
|
145
504
|
align-items: center;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
505
|
+
padding: 0.375rem;
|
|
506
|
+
border: 0;
|
|
507
|
+
border-radius: var(--rpp-radius);
|
|
508
|
+
background: none;
|
|
509
|
+
color: var(--rpp-muted);
|
|
510
|
+
cursor: pointer;
|
|
511
|
+
}
|
|
512
|
+
:where(.rpp-root) .rpp-toggle:focus-visible { outline: 2px solid var(--rpp-accent); }
|
|
513
|
+
:where(.rpp-root) .rpp-toggle:focus:not(:focus-visible) { outline: none; }
|
|
514
|
+
:where(.rpp-root) .rpp-meter { display: flex; gap: 0.25rem; height: 6px; }
|
|
515
|
+
:where(.rpp-root) .rpp-segment {
|
|
153
516
|
flex: 1;
|
|
154
|
-
background-color: var(--rpp-border);
|
|
155
517
|
border-radius: 3px;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)}
|
|
164
|
-
|
|
165
|
-
|
|
518
|
+
background: var(--rpp-border);
|
|
519
|
+
transition: background-color 0.25s;
|
|
520
|
+
}
|
|
521
|
+
:where(.rpp-root[data-strength="very-weak"]) .rpp-segment[data-filled] { background: var(--rpp-danger); }
|
|
522
|
+
:where(.rpp-root[data-strength="weak"]) .rpp-segment[data-filled] { background: var(--rpp-weak); }
|
|
523
|
+
:where(.rpp-root[data-strength="medium"]) .rpp-segment[data-filled] { background: var(--rpp-medium); }
|
|
524
|
+
:where(.rpp-root[data-strength="strong"]) .rpp-segment[data-filled],
|
|
525
|
+
:where(.rpp-root[data-strength="very-strong"]) .rpp-segment[data-filled] { background: var(--rpp-success); }
|
|
526
|
+
:where(.rpp-root) .rpp-strength-label,
|
|
527
|
+
:where(.rpp-root) .rpp-feedback { margin: 0; font-size: 0.8125rem; color: var(--rpp-muted); }
|
|
528
|
+
:where(.rpp-root) .rpp-requirements {
|
|
166
529
|
list-style: none;
|
|
167
|
-
padding: 0;
|
|
168
530
|
margin: 0;
|
|
531
|
+
padding: 0;
|
|
169
532
|
display: flex;
|
|
170
533
|
flex-direction: column;
|
|
171
|
-
gap: 0.
|
|
534
|
+
gap: 0.375rem;
|
|
172
535
|
font-size: 0.875rem;
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
536
|
+
}
|
|
537
|
+
:where(.rpp-root) .rpp-requirement { display: flex; align-items: center; gap: 0.5rem; color: var(--rpp-danger); }
|
|
538
|
+
:where(.rpp-root) .rpp-requirement[data-passed] { color: var(--rpp-success); }
|
|
539
|
+
:where(.rpp-root) .rpp-icon { width: 1em; text-align: center; font-weight: 700; }
|
|
540
|
+
:where(.rpp-root) .rpp-sr-only {
|
|
541
|
+
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
542
|
+
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
|
543
|
+
}
|
|
544
|
+
`.trim();
|
|
545
|
+
|
|
546
|
+
// src/PasswordPolicyInput.tsx
|
|
547
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
548
|
+
var idCounter = 0;
|
|
549
|
+
var useFallbackId = () => {
|
|
550
|
+
const [id] = useState2(() => `rpp-${++idCounter}`);
|
|
551
|
+
return id;
|
|
552
|
+
};
|
|
553
|
+
var _a;
|
|
554
|
+
var useStableId = (_a = React.useId) != null ? _a : useFallbackId;
|
|
555
|
+
var EyeIcon = () => /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", focusable: "false", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
183
556
|
/* @__PURE__ */ jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
|
|
184
557
|
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
|
|
185
558
|
] });
|
|
186
|
-
var EyeOffIcon = () => /* @__PURE__ */ jsxs("svg", {
|
|
559
|
+
var EyeOffIcon = () => /* @__PURE__ */ jsxs("svg", { "aria-hidden": "true", focusable: "false", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
187
560
|
/* @__PURE__ */ jsx("path", { d: "M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" }),
|
|
188
561
|
/* @__PURE__ */ jsx("line", { x1: "1", y1: "1", x2: "23", y2: "23" })
|
|
189
562
|
] });
|
|
190
|
-
var
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
563
|
+
var strengthKey = (label) => label.toLowerCase().replace(/\s+/g, "-");
|
|
564
|
+
var PasswordPolicyInput = forwardRef(
|
|
565
|
+
function PasswordPolicyInput2({
|
|
566
|
+
value,
|
|
567
|
+
defaultValue,
|
|
568
|
+
policyOptions,
|
|
569
|
+
onPasswordChange,
|
|
570
|
+
onChange,
|
|
571
|
+
showStrengthMeter = true,
|
|
572
|
+
showStrengthLabel = false,
|
|
573
|
+
showRequirementsList = true,
|
|
574
|
+
showToggleButton = true,
|
|
575
|
+
toggleLabels = { show: "Show password", hide: "Hide password" },
|
|
576
|
+
unstyled = false,
|
|
577
|
+
className,
|
|
578
|
+
inputClassName,
|
|
579
|
+
id,
|
|
580
|
+
"aria-describedby": ariaDescribedBy,
|
|
581
|
+
...inputProps
|
|
582
|
+
}, ref) {
|
|
583
|
+
const generatedId = useStableId();
|
|
584
|
+
const inputId = id != null ? id : `${generatedId}-input`;
|
|
585
|
+
const listId = `${generatedId}-requirements`;
|
|
586
|
+
const feedbackId = `${generatedId}-feedback`;
|
|
587
|
+
const isControlled = value !== void 0;
|
|
588
|
+
const [internalValue, setInternalValue] = useState2(defaultValue != null ? defaultValue : "");
|
|
589
|
+
const password = isControlled ? value : internalValue;
|
|
590
|
+
const [visible, setVisible] = useState2(false);
|
|
591
|
+
const validation = validatePassword(password, policyOptions);
|
|
592
|
+
const { requirements, strengthPercent, strengthLabel, isValid, estimate } = validation;
|
|
593
|
+
const handleChange = (e) => {
|
|
594
|
+
const next = e.target.value;
|
|
595
|
+
if (!isControlled) setInternalValue(next);
|
|
596
|
+
onChange == null ? void 0 : onChange(e);
|
|
597
|
+
onPasswordChange == null ? void 0 : onPasswordChange(next, { password: next, ...validatePassword(next, policyOptions) });
|
|
598
|
+
};
|
|
599
|
+
const segments = 5;
|
|
600
|
+
const filled = strengthPercent > 0 ? Math.max(1, Math.round(strengthPercent * segments)) : 0;
|
|
601
|
+
const feedback = password && (estimate == null ? void 0 : estimate.feedback);
|
|
602
|
+
const describedBy = [ariaDescribedBy, showRequirementsList ? listId : null, feedback ? feedbackId : null].filter(Boolean).join(" ") || void 0;
|
|
603
|
+
const rootClass = className ? `rpp-root ${className}` : "rpp-root";
|
|
604
|
+
return /* @__PURE__ */ jsxs("div", { className: rootClass, "data-strength": strengthKey(strengthLabel), "data-valid": isValid || void 0, children: [
|
|
605
|
+
!unstyled && /* @__PURE__ */ jsx("style", { children: passwordPolicyInputCss }),
|
|
606
|
+
/* @__PURE__ */ jsxs("div", { className: "rpp-field", children: [
|
|
607
|
+
/* @__PURE__ */ jsx(
|
|
608
|
+
"input",
|
|
609
|
+
{
|
|
610
|
+
...inputProps,
|
|
611
|
+
ref,
|
|
612
|
+
id: inputId,
|
|
613
|
+
className: inputClassName ? `rpp-input ${inputClassName}` : "rpp-input",
|
|
614
|
+
type: visible ? "text" : "password",
|
|
615
|
+
value: password,
|
|
616
|
+
onChange: handleChange,
|
|
617
|
+
"aria-describedby": describedBy,
|
|
618
|
+
"aria-invalid": password.length > 0 && !isValid ? true : void 0
|
|
619
|
+
}
|
|
620
|
+
),
|
|
621
|
+
showToggleButton && /* @__PURE__ */ jsx(
|
|
622
|
+
"button",
|
|
623
|
+
{
|
|
624
|
+
type: "button",
|
|
625
|
+
className: "rpp-toggle",
|
|
626
|
+
onClick: () => setVisible((v) => !v),
|
|
627
|
+
"aria-label": visible ? toggleLabels.hide : toggleLabels.show,
|
|
628
|
+
"aria-pressed": visible,
|
|
629
|
+
"aria-controls": inputId,
|
|
630
|
+
children: visible ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {})
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
] }),
|
|
634
|
+
showStrengthMeter && /* @__PURE__ */ jsx(
|
|
635
|
+
"div",
|
|
214
636
|
{
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
637
|
+
className: "rpp-meter",
|
|
638
|
+
role: "meter",
|
|
639
|
+
"aria-label": "Password strength",
|
|
640
|
+
"aria-valuemin": 0,
|
|
641
|
+
"aria-valuemax": 100,
|
|
642
|
+
"aria-valuenow": Math.round(strengthPercent * 100),
|
|
643
|
+
"aria-valuetext": strengthLabel,
|
|
644
|
+
children: Array.from({ length: segments }, (_, i) => /* @__PURE__ */ jsx("div", { className: "rpp-segment", "data-filled": i < filled || void 0 }, i))
|
|
219
645
|
}
|
|
220
646
|
),
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
showRequirementsList && /* @__PURE__ */ jsx(RequirementsList, { children: Object.entries(policyState).map(([name, passed]) => /* @__PURE__ */ jsxs(RequirementItem, { passed, children: [
|
|
232
|
-
passed ? "\u2713" : "\u2717",
|
|
233
|
-
/* @__PURE__ */ jsx("span", { children: formatPolicyName(name) })
|
|
234
|
-
] }, name)) })
|
|
235
|
-
] });
|
|
236
|
-
};
|
|
647
|
+
showStrengthMeter && showStrengthLabel && /* @__PURE__ */ jsx("p", { className: "rpp-strength-label", children: strengthLabel }),
|
|
648
|
+
showStrengthMeter && feedback && /* @__PURE__ */ jsx("p", { className: "rpp-feedback", id: feedbackId, children: feedback }),
|
|
649
|
+
showRequirementsList && /* @__PURE__ */ jsx("ul", { className: "rpp-requirements", id: listId, "aria-label": "Password requirements", children: requirements.map(({ name, passed, message }) => /* @__PURE__ */ jsxs("li", { className: "rpp-requirement", "data-passed": passed || void 0, "data-rule": name, children: [
|
|
650
|
+
/* @__PURE__ */ jsx("span", { className: "rpp-icon", "aria-hidden": "true", children: passed ? "\u2713" : "\u2717" }),
|
|
651
|
+
/* @__PURE__ */ jsx("span", { children: message }),
|
|
652
|
+
/* @__PURE__ */ jsx("span", { className: "rpp-sr-only", children: passed ? " (met)" : " (not met)" })
|
|
653
|
+
] }, name)) })
|
|
654
|
+
] });
|
|
655
|
+
}
|
|
656
|
+
);
|
|
237
657
|
export {
|
|
658
|
+
COMMON_PASSWORDS,
|
|
659
|
+
DEFAULT_MESSAGES,
|
|
660
|
+
DEFAULT_OPTIONS,
|
|
238
661
|
PasswordPolicyInput,
|
|
239
|
-
|
|
662
|
+
checkPwnedPassword,
|
|
663
|
+
fromZxcvbn,
|
|
664
|
+
isCommonPassword,
|
|
665
|
+
passwordPolicyInputCss,
|
|
666
|
+
passwordValidator,
|
|
667
|
+
presets,
|
|
668
|
+
resolveOptions,
|
|
669
|
+
usePasswordPolicy,
|
|
670
|
+
usePwnedPassword,
|
|
671
|
+
validatePassword,
|
|
672
|
+
zodPasswordRule
|
|
240
673
|
};
|