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