@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 +323 -0
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +282 -0
- package/package.json +49 -0
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
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|