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.mjs CHANGED
@@ -1,240 +1,673 @@
1
+ 'use client';
2
+
1
3
  // src/use-password-policy.ts
2
- import { useMemo } from "react";
3
- var DEFAULT_POLICIES = [
4
- {
5
- name: "minLength",
6
- optionsKey: "minLength",
7
- test: (password, options) => password.length >= options.minLength
8
- },
9
- {
10
- name: "uppercase",
11
- optionsKey: "uppercaseCheck",
12
- test: (password, options) => options.uppercaseRegex.test(password)
13
- },
14
- {
15
- name: "lowercase",
16
- optionsKey: "lowercaseCheck",
17
- test: (password, options) => options.lowercaseRegex.test(password)
18
- },
19
- {
20
- name: "number",
21
- optionsKey: "numberCheck",
22
- test: (password, options) => options.numberRegex.test(password)
23
- },
24
- {
25
- name: "specialChar",
26
- optionsKey: "specialCharCheck",
27
- test: (password, options) => options.specialCharRegex.test(password)
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 usePasswordPolicy = (options = {}) => {
43
- const { password = "" } = options;
44
- const mergedOptions = useMemo(
45
- () => ({ ...DEFAULT_OPTIONS, ...options }),
46
- [options]
47
- );
48
- const activePolicies = useMemo(() => {
49
- const activeDefaultPolicies = DEFAULT_POLICIES.filter(
50
- (policy) => policy.optionsKey && mergedOptions[policy.optionsKey]
51
- );
52
- return [...activeDefaultPolicies, ...mergedOptions.customRules];
53
- }, [mergedOptions]);
54
- const policyState = useMemo(() => {
55
- const state = {};
56
- for (const policy of activePolicies) {
57
- state[policy.name] = policy.test(password, mergedOptions);
58
- }
59
- return state;
60
- }, [password, activePolicies, mergedOptions]);
61
- const { score, label, isValid } = useMemo(() => {
62
- const passedPolicies = Object.values(policyState).filter(Boolean);
63
- const score2 = passedPolicies.length;
64
- const totalPolicies = activePolicies.length;
65
- const strengthPercentage = totalPolicies > 0 ? score2 / totalPolicies : 0;
66
- let label2 = "Very Weak";
67
- if (strengthPercentage >= 1) {
68
- label2 = "Very Strong";
69
- } else if (strengthPercentage >= 0.75) {
70
- label2 = "Strong";
71
- } else if (strengthPercentage >= 0.5) {
72
- label2 = "Medium";
73
- } else if (strengthPercentage > 0) {
74
- label2 = "Weak";
75
- }
76
- return {
77
- score: score2,
78
- label: label2,
79
- isValid: score2 === totalPolicies && totalPolicies > 0
80
- };
81
- }, [policyState, activePolicies]);
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
- password,
390
+ isValid: total > 0 && passedCount === total,
84
391
  policyState,
85
- isValid,
86
- strengthScore: score,
87
- strengthLabel: label
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 { useState, useEffect } from "react";
93
- import styled, { css } from "styled-components";
94
- import { jsx, jsxs } from "react/jsx-runtime";
95
- var STRENGTH_COLOR_MAP = {
96
- "Very Weak": "var(--rpp-weak)",
97
- "Weak": "var(--rpp-weak)",
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: #27ae60;
466
+ --rpp-success: #1e8449;
106
467
  --rpp-danger: #c0392b;
107
- --rpp-weak: #f39c12;
108
- --rpp-medium: #d35400;
109
- --rpp-bg: #f9f9f9;
110
- --rpp-border: #e0e0e0;
111
- --rpp-text: #333;
112
-
113
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
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
- var InputWrapper = styled.div`
119
- position: relative;
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: 6px;
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
- &:focus {
130
- border-color: var(--rpp-accent);
131
- outline: none;
132
- }
133
- `;
134
- var ToggleButton = styled.button`
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.5rem;
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
- var StrengthMeter = styled.div`
148
- display: flex;
149
- gap: 0.25rem;
150
- height: 6px;
151
- `;
152
- var StrengthMeterSegment = styled.div`
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
- transition: background-color 0.3s;
157
-
158
- ${({ isFilled, strengthLabel }) => (
159
- // FIX: Explicitly type props
160
- isFilled && css`
161
- background-color: ${STRENGTH_COLOR_MAP[strengthLabel]};
162
- `
163
- )}
164
- `;
165
- var RequirementsList = styled.ul`
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.5rem;
534
+ gap: 0.375rem;
172
535
  font-size: 0.875rem;
173
- `;
174
- var RequirementItem = styled.li`
175
- display: flex;
176
- align-items: center;
177
- gap: 0.5rem;
178
- transition: color 0.2s;
179
- /* FIX: Explicitly type props */
180
- color: ${({ passed }) => passed ? "var(--rpp-success)" : "var(--rpp-danger)"};
181
- `;
182
- var EyeIcon = () => /* @__PURE__ */ 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: [
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", { 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: [
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 PasswordPolicyInput = ({
191
- policyOptions,
192
- onPasswordChange,
193
- showStrengthMeter = true,
194
- showRequirementsList = true,
195
- showToggleButton = true,
196
- className,
197
- ...restInputProps
198
- }) => {
199
- const [password, setPassword] = useState("");
200
- const [showPassword, setShowPassword] = useState(false);
201
- const validation = usePasswordPolicy({ ...policyOptions, password });
202
- const { policyState, strengthScore, strengthLabel } = validation;
203
- useEffect(() => {
204
- onPasswordChange?.(password, validation);
205
- }, [password, validation, onPasswordChange]);
206
- const formatPolicyName = (name) => name.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
207
- const handlePasswordChange = (e) => {
208
- setPassword(e.target.value);
209
- };
210
- return /* @__PURE__ */ jsxs(Container, { className, children: [
211
- /* @__PURE__ */ jsxs(InputWrapper, { children: [
212
- /* @__PURE__ */ jsx(
213
- Input,
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
- type: showPassword ? "text" : "password",
216
- value: password,
217
- onChange: handlePasswordChange,
218
- ...restInputProps
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
- showToggleButton && /* @__PURE__ */ jsx(ToggleButton, { type: "button", onClick: () => setShowPassword(!showPassword), children: showPassword ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {}) })
222
- ] }),
223
- showStrengthMeter && /* @__PURE__ */ jsx(StrengthMeter, { children: Array.from({ length: 5 }).map((_, index) => /* @__PURE__ */ jsx(
224
- StrengthMeterSegment,
225
- {
226
- isFilled: strengthScore > index,
227
- strengthLabel
228
- },
229
- index
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
- usePasswordPolicy
662
+ checkPwnedPassword,
663
+ fromZxcvbn,
664
+ isCommonPassword,
665
+ passwordPolicyInputCss,
666
+ passwordValidator,
667
+ presets,
668
+ resolveOptions,
669
+ usePasswordPolicy,
670
+ usePwnedPassword,
671
+ validatePassword,
672
+ zodPasswordRule
240
673
  };