@pwd-meter/core 2.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.cjs ADDED
@@ -0,0 +1,323 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ analyzePassword: () => analyzePassword,
34
+ analyzePasswordAsync: () => analyzePasswordAsync,
35
+ checkPwnedPassword: () => checkPwnedPassword,
36
+ generateSecurePassword: () => generateSecurePassword,
37
+ getPasswordChecks: () => getPasswordChecks,
38
+ getStrengthColor: () => getStrengthColor,
39
+ getStrengthMessage: () => getStrengthMessage
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+ var import_zxcvbn = __toESM(require("zxcvbn"), 1);
43
+
44
+ // src/hibp.ts
45
+ var DEFAULT_BASE_URL = "https://api.pwnedpasswords.com/range";
46
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
47
+ async function sha1Hex(value) {
48
+ const data = new TextEncoder().encode(value);
49
+ const digest = await crypto.subtle.digest("SHA-1", data);
50
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join("");
51
+ }
52
+ function parseCount(line) {
53
+ const [, count = "0"] = line.split(":");
54
+ return Number.parseInt(count, 10) || 0;
55
+ }
56
+ function sleep(ms) {
57
+ return new Promise((resolve) => {
58
+ setTimeout(resolve, ms);
59
+ });
60
+ }
61
+ async function fetchRange(fetchImpl, url, timeoutMs, retryCount, retryDelayMs) {
62
+ const maxAttempts = retryCount + 1;
63
+ let delayMs = retryDelayMs;
64
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
65
+ const controller = new AbortController();
66
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
67
+ try {
68
+ const response = await fetchImpl(url, {
69
+ method: "GET",
70
+ headers: { Accept: "text/plain" },
71
+ signal: controller.signal
72
+ });
73
+ if (RETRYABLE_STATUSES.has(response.status) && attempt < maxAttempts - 1) {
74
+ await sleep(delayMs);
75
+ delayMs *= 2;
76
+ continue;
77
+ }
78
+ return response;
79
+ } catch (error) {
80
+ if (attempt < maxAttempts - 1) {
81
+ await sleep(delayMs);
82
+ delayMs *= 2;
83
+ continue;
84
+ }
85
+ throw error;
86
+ } finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+ throw new Error("HIBP request failed after retries.");
91
+ }
92
+ async function checkPwnedPassword(password, options = {}) {
93
+ if (!password) {
94
+ return { isPwned: false, count: 0 };
95
+ }
96
+ const fetchImpl = options.fetch ?? globalThis.fetch;
97
+ if (!fetchImpl) {
98
+ throw new Error("fetch is not available. Provide options.fetch for this environment.");
99
+ }
100
+ const hash = await sha1Hex(password);
101
+ const prefix = hash.slice(0, 5);
102
+ const suffix = hash.slice(5);
103
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
104
+ const url = new URL(`${baseUrl}/${prefix}`);
105
+ if (options.addPadding ?? true) {
106
+ url.searchParams.set("padding", "true");
107
+ }
108
+ const response = await fetchRange(
109
+ fetchImpl,
110
+ url.toString(),
111
+ options.timeoutMs ?? 5e3,
112
+ options.retryCount ?? 2,
113
+ options.retryDelayMs ?? 1e3
114
+ );
115
+ if (!response.ok) {
116
+ throw new Error(`HIBP request failed with status ${response.status}.`);
117
+ }
118
+ const body = await response.text();
119
+ for (const line of body.split("\n")) {
120
+ const trimmed = line.trim();
121
+ if (!trimmed) continue;
122
+ const [hashSuffix] = trimmed.split(":");
123
+ if (hashSuffix.toUpperCase() === suffix) {
124
+ return { isPwned: true, count: parseCount(trimmed) };
125
+ }
126
+ }
127
+ return { isPwned: false, count: 0 };
128
+ }
129
+
130
+ // src/index.ts
131
+ var LABELS = {
132
+ 0: "weak",
133
+ 1: "fair",
134
+ 2: "good",
135
+ 3: "strong",
136
+ 4: "very-strong"
137
+ };
138
+ var DEFAULT_CHARSET = {
139
+ lowercase: "abcdefghijklmnopqrstuvwxyz",
140
+ uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
141
+ numbers: "0123456789",
142
+ symbols: "!@#$%^&*()-_=+[]{}"
143
+ };
144
+ var PWNED_FEEDBACK = {
145
+ warning: "This password has appeared in a known data breach.",
146
+ suggestions: [
147
+ "Choose a unique password that has not been exposed online.",
148
+ "Use a passphrase or the password generator for a safer option."
149
+ ]
150
+ };
151
+ function getPasswordChecks(password) {
152
+ return {
153
+ length: password.length >= 12,
154
+ lowercase: /[a-z]/.test(password),
155
+ uppercase: /[A-Z]/.test(password),
156
+ number: /\d/.test(password),
157
+ symbol: /[^\w\s]/.test(password)
158
+ };
159
+ }
160
+ function buildZxcvbnResult(password) {
161
+ const result = (0, import_zxcvbn.default)(password);
162
+ return {
163
+ password,
164
+ score: result.score,
165
+ label: LABELS[result.score],
166
+ crackTimeDisplay: String(result.crack_times_display.offline_slow_hashing_1e4_per_second),
167
+ isPwned: false,
168
+ feedback: {
169
+ warning: result.feedback.warning ?? void 0,
170
+ suggestions: result.feedback.suggestions
171
+ },
172
+ checks: getPasswordChecks(password)
173
+ };
174
+ }
175
+ function buildPwnedResult(password, pwned) {
176
+ return {
177
+ password,
178
+ score: 0,
179
+ label: "pwned",
180
+ crackTimeDisplay: "instant",
181
+ isPwned: true,
182
+ pwnedCount: pwned.count,
183
+ feedback: {
184
+ warning: PWNED_FEEDBACK.warning,
185
+ suggestions: PWNED_FEEDBACK.suggestions
186
+ },
187
+ checks: getPasswordChecks(password)
188
+ };
189
+ }
190
+ function analyzePassword(password) {
191
+ if (!password) {
192
+ return {
193
+ password,
194
+ score: 0,
195
+ label: "empty",
196
+ crackTimeDisplay: "instant",
197
+ isPwned: false,
198
+ feedback: { suggestions: [] },
199
+ checks: getPasswordChecks("")
200
+ };
201
+ }
202
+ return buildZxcvbnResult(password);
203
+ }
204
+ async function analyzePasswordAsync(password, options = {}) {
205
+ const base = analyzePassword(password);
206
+ if (!password || !options.checkPwned) {
207
+ return base;
208
+ }
209
+ try {
210
+ const pwned = await checkPwnedPassword(password, options.hibp);
211
+ if (pwned.isPwned) {
212
+ return buildPwnedResult(password, pwned);
213
+ }
214
+ return base;
215
+ } catch {
216
+ return {
217
+ ...base,
218
+ feedback: {
219
+ ...base.feedback,
220
+ warning: base.feedback.warning || "Could not verify breach status. Check your connection."
221
+ }
222
+ };
223
+ }
224
+ }
225
+ function randomIndex(max) {
226
+ const array = new Uint32Array(1);
227
+ crypto.getRandomValues(array);
228
+ return array[0] % max;
229
+ }
230
+ function pick(charset) {
231
+ return charset.charAt(randomIndex(charset.length));
232
+ }
233
+ function shuffle(value) {
234
+ const chars = value.split("");
235
+ for (let i = chars.length - 1; i > 0; i -= 1) {
236
+ const j = randomIndex(i + 1);
237
+ [chars[i], chars[j]] = [chars[j], chars[i]];
238
+ }
239
+ return chars.join("");
240
+ }
241
+ function generateSecurePassword(options = {}) {
242
+ const {
243
+ length = 16,
244
+ includeUppercase = true,
245
+ includeLowercase = true,
246
+ includeNumbers = true,
247
+ includeSymbols = true,
248
+ minScore = 3,
249
+ maxAttempts = 25
250
+ } = options;
251
+ if (length < 8) {
252
+ throw new Error("Password length must be at least 8 characters.");
253
+ }
254
+ const pools = [
255
+ includeLowercase ? DEFAULT_CHARSET.lowercase : "",
256
+ includeUppercase ? DEFAULT_CHARSET.uppercase : "",
257
+ includeNumbers ? DEFAULT_CHARSET.numbers : "",
258
+ includeSymbols ? DEFAULT_CHARSET.symbols : ""
259
+ ].filter(Boolean);
260
+ if (!pools.length) {
261
+ throw new Error("At least one character set must be enabled.");
262
+ }
263
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
264
+ let password = pools.map((pool) => pick(pool)).join("");
265
+ while (password.length < length) {
266
+ password += pick(pools.join(""));
267
+ }
268
+ password = shuffle(password.slice(0, length));
269
+ const analysis = analyzePassword(password);
270
+ if (analysis.score >= minScore) {
271
+ return password;
272
+ }
273
+ }
274
+ throw new Error("Could not generate a password that meets the requested strength.");
275
+ }
276
+ function getStrengthColor(label) {
277
+ switch (label) {
278
+ case "pwned":
279
+ case "weak":
280
+ return "#dc2626";
281
+ case "fair":
282
+ return "#ea580c";
283
+ case "good":
284
+ return "#ca8a04";
285
+ case "strong":
286
+ return "#16a34a";
287
+ case "very-strong":
288
+ return "#059669";
289
+ default:
290
+ return "#94a3b8";
291
+ }
292
+ }
293
+ function getStrengthMessage(result) {
294
+ if (result.pwnedCheckPending) {
295
+ return "Checking breach database\u2026";
296
+ }
297
+ switch (result.label) {
298
+ case "empty":
299
+ return "Enter a password to check strength.";
300
+ case "pwned":
301
+ return result.pwnedCount ? `Found in ${result.pwnedCount.toLocaleString()} breaches \u2014 choose another password.` : "Found in a data breach \u2014 choose another password.";
302
+ case "weak":
303
+ return "Weak password.";
304
+ case "fair":
305
+ return "Fair, but could be stronger.";
306
+ case "good":
307
+ return "Good password.";
308
+ case "strong":
309
+ return "Strong password.";
310
+ case "very-strong":
311
+ return "Very strong password.";
312
+ }
313
+ }
314
+ // Annotate the CommonJS export names for ESM import in node:
315
+ 0 && (module.exports = {
316
+ analyzePassword,
317
+ analyzePasswordAsync,
318
+ checkPwnedPassword,
319
+ generateSecurePassword,
320
+ getPasswordChecks,
321
+ getStrengthColor,
322
+ getStrengthMessage
323
+ });
@@ -0,0 +1,65 @@
1
+ type HibpOptions = {
2
+ fetch?: typeof fetch;
3
+ baseUrl?: string;
4
+ timeoutMs?: number;
5
+ /** Add padding to the range request to reduce response fingerprinting. Default: true */
6
+ addPadding?: boolean;
7
+ /** Retries after HTTP 429/503. Default: 2 */
8
+ retryCount?: number;
9
+ /** Initial delay before retry in ms. Doubles each attempt. Default: 1000 */
10
+ retryDelayMs?: number;
11
+ };
12
+ type PwnedPasswordResult = {
13
+ isPwned: boolean;
14
+ count: number;
15
+ };
16
+ declare function checkPwnedPassword(password: string, options?: HibpOptions): Promise<PwnedPasswordResult>;
17
+
18
+ type StrengthLabel = "empty" | "weak" | "fair" | "good" | "strong" | "very-strong" | "pwned";
19
+ type PasswordStrengthResult = {
20
+ password: string;
21
+ score: 0 | 1 | 2 | 3 | 4;
22
+ label: StrengthLabel;
23
+ crackTimeDisplay: string;
24
+ isPwned: boolean;
25
+ pwnedCount?: number;
26
+ pwnedCheckPending?: boolean;
27
+ feedback: {
28
+ warning?: string;
29
+ suggestions: string[];
30
+ };
31
+ checks: {
32
+ length: boolean;
33
+ lowercase: boolean;
34
+ uppercase: boolean;
35
+ number: boolean;
36
+ symbol: boolean;
37
+ };
38
+ };
39
+ type AnalyzePasswordOptions = {
40
+ checkPwned?: boolean;
41
+ hibp?: HibpOptions;
42
+ };
43
+ type GeneratePasswordOptions = {
44
+ length?: number;
45
+ includeUppercase?: boolean;
46
+ includeLowercase?: boolean;
47
+ includeNumbers?: boolean;
48
+ includeSymbols?: boolean;
49
+ minScore?: 0 | 1 | 2 | 3 | 4;
50
+ maxAttempts?: number;
51
+ };
52
+ declare function getPasswordChecks(password: string): {
53
+ length: boolean;
54
+ lowercase: boolean;
55
+ uppercase: boolean;
56
+ number: boolean;
57
+ symbol: boolean;
58
+ };
59
+ declare function analyzePassword(password: string): PasswordStrengthResult;
60
+ declare function analyzePasswordAsync(password: string, options?: AnalyzePasswordOptions): Promise<PasswordStrengthResult>;
61
+ declare function generateSecurePassword(options?: GeneratePasswordOptions): string;
62
+ declare function getStrengthColor(label: StrengthLabel): string;
63
+ declare function getStrengthMessage(result: PasswordStrengthResult): string;
64
+
65
+ export { type AnalyzePasswordOptions, type GeneratePasswordOptions, type HibpOptions, type PasswordStrengthResult, type PwnedPasswordResult, type StrengthLabel, analyzePassword, analyzePasswordAsync, checkPwnedPassword, generateSecurePassword, getPasswordChecks, getStrengthColor, getStrengthMessage };
@@ -0,0 +1,65 @@
1
+ type HibpOptions = {
2
+ fetch?: typeof fetch;
3
+ baseUrl?: string;
4
+ timeoutMs?: number;
5
+ /** Add padding to the range request to reduce response fingerprinting. Default: true */
6
+ addPadding?: boolean;
7
+ /** Retries after HTTP 429/503. Default: 2 */
8
+ retryCount?: number;
9
+ /** Initial delay before retry in ms. Doubles each attempt. Default: 1000 */
10
+ retryDelayMs?: number;
11
+ };
12
+ type PwnedPasswordResult = {
13
+ isPwned: boolean;
14
+ count: number;
15
+ };
16
+ declare function checkPwnedPassword(password: string, options?: HibpOptions): Promise<PwnedPasswordResult>;
17
+
18
+ type StrengthLabel = "empty" | "weak" | "fair" | "good" | "strong" | "very-strong" | "pwned";
19
+ type PasswordStrengthResult = {
20
+ password: string;
21
+ score: 0 | 1 | 2 | 3 | 4;
22
+ label: StrengthLabel;
23
+ crackTimeDisplay: string;
24
+ isPwned: boolean;
25
+ pwnedCount?: number;
26
+ pwnedCheckPending?: boolean;
27
+ feedback: {
28
+ warning?: string;
29
+ suggestions: string[];
30
+ };
31
+ checks: {
32
+ length: boolean;
33
+ lowercase: boolean;
34
+ uppercase: boolean;
35
+ number: boolean;
36
+ symbol: boolean;
37
+ };
38
+ };
39
+ type AnalyzePasswordOptions = {
40
+ checkPwned?: boolean;
41
+ hibp?: HibpOptions;
42
+ };
43
+ type GeneratePasswordOptions = {
44
+ length?: number;
45
+ includeUppercase?: boolean;
46
+ includeLowercase?: boolean;
47
+ includeNumbers?: boolean;
48
+ includeSymbols?: boolean;
49
+ minScore?: 0 | 1 | 2 | 3 | 4;
50
+ maxAttempts?: number;
51
+ };
52
+ declare function getPasswordChecks(password: string): {
53
+ length: boolean;
54
+ lowercase: boolean;
55
+ uppercase: boolean;
56
+ number: boolean;
57
+ symbol: boolean;
58
+ };
59
+ declare function analyzePassword(password: string): PasswordStrengthResult;
60
+ declare function analyzePasswordAsync(password: string, options?: AnalyzePasswordOptions): Promise<PasswordStrengthResult>;
61
+ declare function generateSecurePassword(options?: GeneratePasswordOptions): string;
62
+ declare function getStrengthColor(label: StrengthLabel): string;
63
+ declare function getStrengthMessage(result: PasswordStrengthResult): string;
64
+
65
+ export { type AnalyzePasswordOptions, type GeneratePasswordOptions, type HibpOptions, type PasswordStrengthResult, type PwnedPasswordResult, type StrengthLabel, analyzePassword, analyzePasswordAsync, checkPwnedPassword, generateSecurePassword, getPasswordChecks, getStrengthColor, getStrengthMessage };
package/dist/index.js ADDED
@@ -0,0 +1,282 @@
1
+ // src/index.ts
2
+ import zxcvbn from "zxcvbn";
3
+
4
+ // src/hibp.ts
5
+ var DEFAULT_BASE_URL = "https://api.pwnedpasswords.com/range";
6
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503]);
7
+ async function sha1Hex(value) {
8
+ const data = new TextEncoder().encode(value);
9
+ const digest = await crypto.subtle.digest("SHA-1", data);
10
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join("");
11
+ }
12
+ function parseCount(line) {
13
+ const [, count = "0"] = line.split(":");
14
+ return Number.parseInt(count, 10) || 0;
15
+ }
16
+ function sleep(ms) {
17
+ return new Promise((resolve) => {
18
+ setTimeout(resolve, ms);
19
+ });
20
+ }
21
+ async function fetchRange(fetchImpl, url, timeoutMs, retryCount, retryDelayMs) {
22
+ const maxAttempts = retryCount + 1;
23
+ let delayMs = retryDelayMs;
24
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
25
+ const controller = new AbortController();
26
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
27
+ try {
28
+ const response = await fetchImpl(url, {
29
+ method: "GET",
30
+ headers: { Accept: "text/plain" },
31
+ signal: controller.signal
32
+ });
33
+ if (RETRYABLE_STATUSES.has(response.status) && attempt < maxAttempts - 1) {
34
+ await sleep(delayMs);
35
+ delayMs *= 2;
36
+ continue;
37
+ }
38
+ return response;
39
+ } catch (error) {
40
+ if (attempt < maxAttempts - 1) {
41
+ await sleep(delayMs);
42
+ delayMs *= 2;
43
+ continue;
44
+ }
45
+ throw error;
46
+ } finally {
47
+ clearTimeout(timeout);
48
+ }
49
+ }
50
+ throw new Error("HIBP request failed after retries.");
51
+ }
52
+ async function checkPwnedPassword(password, options = {}) {
53
+ if (!password) {
54
+ return { isPwned: false, count: 0 };
55
+ }
56
+ const fetchImpl = options.fetch ?? globalThis.fetch;
57
+ if (!fetchImpl) {
58
+ throw new Error("fetch is not available. Provide options.fetch for this environment.");
59
+ }
60
+ const hash = await sha1Hex(password);
61
+ const prefix = hash.slice(0, 5);
62
+ const suffix = hash.slice(5);
63
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
64
+ const url = new URL(`${baseUrl}/${prefix}`);
65
+ if (options.addPadding ?? true) {
66
+ url.searchParams.set("padding", "true");
67
+ }
68
+ const response = await fetchRange(
69
+ fetchImpl,
70
+ url.toString(),
71
+ options.timeoutMs ?? 5e3,
72
+ options.retryCount ?? 2,
73
+ options.retryDelayMs ?? 1e3
74
+ );
75
+ if (!response.ok) {
76
+ throw new Error(`HIBP request failed with status ${response.status}.`);
77
+ }
78
+ const body = await response.text();
79
+ for (const line of body.split("\n")) {
80
+ const trimmed = line.trim();
81
+ if (!trimmed) continue;
82
+ const [hashSuffix] = trimmed.split(":");
83
+ if (hashSuffix.toUpperCase() === suffix) {
84
+ return { isPwned: true, count: parseCount(trimmed) };
85
+ }
86
+ }
87
+ return { isPwned: false, count: 0 };
88
+ }
89
+
90
+ // src/index.ts
91
+ var LABELS = {
92
+ 0: "weak",
93
+ 1: "fair",
94
+ 2: "good",
95
+ 3: "strong",
96
+ 4: "very-strong"
97
+ };
98
+ var DEFAULT_CHARSET = {
99
+ lowercase: "abcdefghijklmnopqrstuvwxyz",
100
+ uppercase: "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
101
+ numbers: "0123456789",
102
+ symbols: "!@#$%^&*()-_=+[]{}"
103
+ };
104
+ var PWNED_FEEDBACK = {
105
+ warning: "This password has appeared in a known data breach.",
106
+ suggestions: [
107
+ "Choose a unique password that has not been exposed online.",
108
+ "Use a passphrase or the password generator for a safer option."
109
+ ]
110
+ };
111
+ function getPasswordChecks(password) {
112
+ return {
113
+ length: password.length >= 12,
114
+ lowercase: /[a-z]/.test(password),
115
+ uppercase: /[A-Z]/.test(password),
116
+ number: /\d/.test(password),
117
+ symbol: /[^\w\s]/.test(password)
118
+ };
119
+ }
120
+ function buildZxcvbnResult(password) {
121
+ const result = zxcvbn(password);
122
+ return {
123
+ password,
124
+ score: result.score,
125
+ label: LABELS[result.score],
126
+ crackTimeDisplay: String(result.crack_times_display.offline_slow_hashing_1e4_per_second),
127
+ isPwned: false,
128
+ feedback: {
129
+ warning: result.feedback.warning ?? void 0,
130
+ suggestions: result.feedback.suggestions
131
+ },
132
+ checks: getPasswordChecks(password)
133
+ };
134
+ }
135
+ function buildPwnedResult(password, pwned) {
136
+ return {
137
+ password,
138
+ score: 0,
139
+ label: "pwned",
140
+ crackTimeDisplay: "instant",
141
+ isPwned: true,
142
+ pwnedCount: pwned.count,
143
+ feedback: {
144
+ warning: PWNED_FEEDBACK.warning,
145
+ suggestions: PWNED_FEEDBACK.suggestions
146
+ },
147
+ checks: getPasswordChecks(password)
148
+ };
149
+ }
150
+ function analyzePassword(password) {
151
+ if (!password) {
152
+ return {
153
+ password,
154
+ score: 0,
155
+ label: "empty",
156
+ crackTimeDisplay: "instant",
157
+ isPwned: false,
158
+ feedback: { suggestions: [] },
159
+ checks: getPasswordChecks("")
160
+ };
161
+ }
162
+ return buildZxcvbnResult(password);
163
+ }
164
+ async function analyzePasswordAsync(password, options = {}) {
165
+ const base = analyzePassword(password);
166
+ if (!password || !options.checkPwned) {
167
+ return base;
168
+ }
169
+ try {
170
+ const pwned = await checkPwnedPassword(password, options.hibp);
171
+ if (pwned.isPwned) {
172
+ return buildPwnedResult(password, pwned);
173
+ }
174
+ return base;
175
+ } catch {
176
+ return {
177
+ ...base,
178
+ feedback: {
179
+ ...base.feedback,
180
+ warning: base.feedback.warning || "Could not verify breach status. Check your connection."
181
+ }
182
+ };
183
+ }
184
+ }
185
+ function randomIndex(max) {
186
+ const array = new Uint32Array(1);
187
+ crypto.getRandomValues(array);
188
+ return array[0] % max;
189
+ }
190
+ function pick(charset) {
191
+ return charset.charAt(randomIndex(charset.length));
192
+ }
193
+ function shuffle(value) {
194
+ const chars = value.split("");
195
+ for (let i = chars.length - 1; i > 0; i -= 1) {
196
+ const j = randomIndex(i + 1);
197
+ [chars[i], chars[j]] = [chars[j], chars[i]];
198
+ }
199
+ return chars.join("");
200
+ }
201
+ function generateSecurePassword(options = {}) {
202
+ const {
203
+ length = 16,
204
+ includeUppercase = true,
205
+ includeLowercase = true,
206
+ includeNumbers = true,
207
+ includeSymbols = true,
208
+ minScore = 3,
209
+ maxAttempts = 25
210
+ } = options;
211
+ if (length < 8) {
212
+ throw new Error("Password length must be at least 8 characters.");
213
+ }
214
+ const pools = [
215
+ includeLowercase ? DEFAULT_CHARSET.lowercase : "",
216
+ includeUppercase ? DEFAULT_CHARSET.uppercase : "",
217
+ includeNumbers ? DEFAULT_CHARSET.numbers : "",
218
+ includeSymbols ? DEFAULT_CHARSET.symbols : ""
219
+ ].filter(Boolean);
220
+ if (!pools.length) {
221
+ throw new Error("At least one character set must be enabled.");
222
+ }
223
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
224
+ let password = pools.map((pool) => pick(pool)).join("");
225
+ while (password.length < length) {
226
+ password += pick(pools.join(""));
227
+ }
228
+ password = shuffle(password.slice(0, length));
229
+ const analysis = analyzePassword(password);
230
+ if (analysis.score >= minScore) {
231
+ return password;
232
+ }
233
+ }
234
+ throw new Error("Could not generate a password that meets the requested strength.");
235
+ }
236
+ function getStrengthColor(label) {
237
+ switch (label) {
238
+ case "pwned":
239
+ case "weak":
240
+ return "#dc2626";
241
+ case "fair":
242
+ return "#ea580c";
243
+ case "good":
244
+ return "#ca8a04";
245
+ case "strong":
246
+ return "#16a34a";
247
+ case "very-strong":
248
+ return "#059669";
249
+ default:
250
+ return "#94a3b8";
251
+ }
252
+ }
253
+ function getStrengthMessage(result) {
254
+ if (result.pwnedCheckPending) {
255
+ return "Checking breach database\u2026";
256
+ }
257
+ switch (result.label) {
258
+ case "empty":
259
+ return "Enter a password to check strength.";
260
+ case "pwned":
261
+ return result.pwnedCount ? `Found in ${result.pwnedCount.toLocaleString()} breaches \u2014 choose another password.` : "Found in a data breach \u2014 choose another password.";
262
+ case "weak":
263
+ return "Weak password.";
264
+ case "fair":
265
+ return "Fair, but could be stronger.";
266
+ case "good":
267
+ return "Good password.";
268
+ case "strong":
269
+ return "Strong password.";
270
+ case "very-strong":
271
+ return "Very strong password.";
272
+ }
273
+ }
274
+ export {
275
+ analyzePassword,
276
+ analyzePasswordAsync,
277
+ checkPwnedPassword,
278
+ generateSecurePassword,
279
+ getPasswordChecks,
280
+ getStrengthColor,
281
+ getStrengthMessage
282
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@pwd-meter/core",
3
+ "version": "2.0.0",
4
+ "description": "Modern password strength analysis with optional HIBP breach checks and secure generation.",
5
+ "license": "MIT",
6
+ "author": "Alen Joy <alenjoy333@gmail.com>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/alenjoy333/Password-Strength-Checker.git",
10
+ "directory": "packages/core"
11
+ },
12
+ "keywords": [
13
+ "password",
14
+ "strength",
15
+ "security",
16
+ "zxcvbn",
17
+ "hibp",
18
+ "pwned-passwords"
19
+ ],
20
+ "type": "module",
21
+ "main": "./dist/index.cjs",
22
+ "module": "./dist/index.js",
23
+ "types": "./dist/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "import": "./dist/index.js",
28
+ "require": "./dist/index.cjs"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "sideEffects": false,
35
+ "scripts": {
36
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
37
+ "clean": "rm -rf dist",
38
+ "test": "vitest run"
39
+ },
40
+ "dependencies": {
41
+ "zxcvbn": "^4.4.2"
42
+ },
43
+ "devDependencies": {
44
+ "@types/zxcvbn": "^4.4.5",
45
+ "tsup": "^8.5.0",
46
+ "typescript": "^5.8.3",
47
+ "vitest": "^3.2.4"
48
+ }
49
+ }