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/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 index_exports = {};
32
- __export(index_exports, {
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
- usePasswordPolicy: () => usePasswordPolicy
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(index_exports);
50
+ module.exports = __toCommonJS(src_exports);
37
51
 
38
52
  // src/use-password-policy.ts
39
53
  var import_react = require("react");
40
- var DEFAULT_POLICIES = [
41
- {
42
- name: "minLength",
43
- optionsKey: "minLength",
44
- test: (password, options) => password.length >= options.minLength
45
- },
46
- {
47
- name: "uppercase",
48
- optionsKey: "uppercaseCheck",
49
- test: (password, options) => options.uppercaseRegex.test(password)
50
- },
51
- {
52
- name: "lowercase",
53
- optionsKey: "lowercaseCheck",
54
- test: (password, options) => options.lowercaseRegex.test(password)
55
- },
56
- {
57
- name: "number",
58
- optionsKey: "numberCheck",
59
- test: (password, options) => options.numberRegex.test(password)
60
- },
61
- {
62
- name: "specialChar",
63
- optionsKey: "specialCharCheck",
64
- test: (password, options) => options.specialCharRegex.test(password)
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 usePasswordPolicy = (options = {}) => {
80
- const { password = "" } = options;
81
- const mergedOptions = (0, import_react.useMemo)(
82
- () => ({ ...DEFAULT_OPTIONS, ...options }),
83
- [options]
84
- );
85
- const activePolicies = (0, import_react.useMemo)(() => {
86
- const activeDefaultPolicies = DEFAULT_POLICIES.filter(
87
- (policy) => policy.optionsKey && mergedOptions[policy.optionsKey]
88
- );
89
- return [...activeDefaultPolicies, ...mergedOptions.customRules];
90
- }, [mergedOptions]);
91
- const policyState = (0, import_react.useMemo)(() => {
92
- const state = {};
93
- for (const policy of activePolicies) {
94
- state[policy.name] = policy.test(password, mergedOptions);
95
- }
96
- return state;
97
- }, [password, activePolicies, mergedOptions]);
98
- const { score, label, isValid } = (0, import_react.useMemo)(() => {
99
- const passedPolicies = Object.values(policyState).filter(Boolean);
100
- const score2 = passedPolicies.length;
101
- const totalPolicies = activePolicies.length;
102
- const strengthPercentage = totalPolicies > 0 ? score2 / totalPolicies : 0;
103
- let label2 = "Very Weak";
104
- if (strengthPercentage >= 1) {
105
- label2 = "Very Strong";
106
- } else if (strengthPercentage >= 0.75) {
107
- label2 = "Strong";
108
- } else if (strengthPercentage >= 0.5) {
109
- label2 = "Medium";
110
- } else if (strengthPercentage > 0) {
111
- label2 = "Weak";
112
- }
113
- return {
114
- score: score2,
115
- label: label2,
116
- isValid: score2 === totalPolicies && totalPolicies > 0
117
- };
118
- }, [policyState, activePolicies]);
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
- password,
439
+ isValid: total > 0 && passedCount === total,
121
440
  policyState,
122
- isValid,
123
- strengthScore: score,
124
- strengthLabel: label
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
- var import_styled_components = __toESM(require("styled-components"));
131
- var import_jsx_runtime = require("react/jsx-runtime");
132
- var STRENGTH_COLOR_MAP = {
133
- "Very Weak": "var(--rpp-weak)",
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: #27ae60;
515
+ --rpp-success: #1e8449;
143
516
  --rpp-danger: #c0392b;
144
- --rpp-weak: #f39c12;
145
- --rpp-medium: #d35400;
146
- --rpp-bg: #f9f9f9;
147
- --rpp-border: #e0e0e0;
148
- --rpp-text: #333;
149
-
150
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
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
- var InputWrapper = import_styled_components.default.div`
156
- position: relative;
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: 6px;
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
- &:focus {
167
- border-color: var(--rpp-accent);
168
- outline: none;
169
- }
170
- `;
171
- var ToggleButton = import_styled_components.default.button`
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.5rem;
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
- var StrengthMeter = import_styled_components.default.div`
185
- display: flex;
186
- gap: 0.25rem;
187
- height: 6px;
188
- `;
189
- var StrengthMeterSegment = import_styled_components.default.div`
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
- transition: background-color 0.3s;
194
-
195
- ${({ isFilled, strengthLabel }) => (
196
- // FIX: Explicitly type props
197
- isFilled && import_styled_components.css`
198
- background-color: ${STRENGTH_COLOR_MAP[strengthLabel]};
199
- `
200
- )}
201
- `;
202
- var RequirementsList = import_styled_components.default.ul`
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.5rem;
583
+ gap: 0.375rem;
209
584
  font-size: 0.875rem;
210
- `;
211
- var RequirementItem = import_styled_components.default.li`
212
- display: flex;
213
- align-items: center;
214
- gap: 0.5rem;
215
- transition: color 0.2s;
216
- /* FIX: Explicitly type props */
217
- color: ${({ passed }) => passed ? "var(--rpp-success)" : "var(--rpp-danger)"};
218
- `;
219
- var EyeIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
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", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
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 PasswordPolicyInput = ({
228
- policyOptions,
229
- onPasswordChange,
230
- showStrengthMeter = true,
231
- showRequirementsList = true,
232
- showToggleButton = true,
233
- className,
234
- ...restInputProps
235
- }) => {
236
- const [password, setPassword] = (0, import_react2.useState)("");
237
- const [showPassword, setShowPassword] = (0, import_react2.useState)(false);
238
- const validation = usePasswordPolicy({ ...policyOptions, password });
239
- const { policyState, strengthScore, strengthLabel } = validation;
240
- (0, import_react2.useEffect)(() => {
241
- onPasswordChange?.(password, validation);
242
- }, [password, validation, onPasswordChange]);
243
- const formatPolicyName = (name) => name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
244
- const handlePasswordChange = (e) => {
245
- setPassword(e.target.value);
246
- };
247
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Container, { className, children: [
248
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(InputWrapper, { children: [
249
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
250
- Input,
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
- type: showPassword ? "text" : "password",
253
- value: password,
254
- onChange: handlePasswordChange,
255
- ...restInputProps
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
- showToggleButton && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToggleButton, { type: "button", onClick: () => setShowPassword(!showPassword), children: showPassword ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeOffIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeIcon, {}) })
259
- ] }),
260
- showStrengthMeter && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StrengthMeter, { children: Array.from({ length: 5 }).map((_, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
261
- StrengthMeterSegment,
262
- {
263
- isFilled: strengthScore > index,
264
- strengthLabel
265
- },
266
- index
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
- usePasswordPolicy
712
+ checkPwnedPassword,
713
+ fromZxcvbn,
714
+ isCommonPassword,
715
+ passwordPolicyInputCss,
716
+ passwordValidator,
717
+ presets,
718
+ resolveOptions,
719
+ usePasswordPolicy,
720
+ usePwnedPassword,
721
+ validatePassword,
722
+ zodPasswordRule
278
723
  });