@co0ontty/wand 2.1.1 → 2.2.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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "commit": "4f3dd3d0b6f424bdc142ec4bdf83d82f7ce77524",
3
- "builtAt": "2026-06-28T08:09:07.444Z",
4
- "version": "2.1.1",
2
+ "commit": "edf467aea7d57201c21ba6047091251383386c2e",
3
+ "builtAt": "2026-06-29T01:08:34.738Z",
4
+ "version": "2.2.0",
5
5
  "channel": "stable"
6
6
  }
@@ -0,0 +1,82 @@
1
+ export declare const DEFAULT_BROWSER_EXTENSION_BASE_URL = "https://home.huniu.fun:8183";
2
+ export declare const DEFAULT_PASSWORD_VAULT_ID = "personal";
3
+ export declare const DEFAULT_PASSWORD_VAULT_NAME = "Personal";
4
+ export type PasswordVaultItemType = "login" | "credit_card" | "identity" | "secure_note" | "passkey";
5
+ export interface PasswordVault {
6
+ id: string;
7
+ name: string;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ }
11
+ export interface PasswordVaultItem {
12
+ id: string;
13
+ vaultId: string;
14
+ type: PasswordVaultItemType;
15
+ title: string;
16
+ username?: string;
17
+ password?: string;
18
+ urls: string[];
19
+ notes?: string;
20
+ fields: Record<string, string>;
21
+ tags: string[];
22
+ favorite: boolean;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ lastUsedAt?: string;
26
+ passwordUpdatedAt?: string;
27
+ }
28
+ export interface PasswordVaultItemInput {
29
+ vaultId?: string;
30
+ type?: PasswordVaultItemType;
31
+ title?: string;
32
+ username?: string;
33
+ password?: string;
34
+ urls?: string[];
35
+ notes?: string;
36
+ fields?: Record<string, unknown>;
37
+ tags?: string[];
38
+ favorite?: boolean;
39
+ }
40
+ export interface PasswordVaultItemFilter {
41
+ q?: string;
42
+ url?: string;
43
+ type?: PasswordVaultItemType;
44
+ vaultId?: string;
45
+ includeArchived?: boolean;
46
+ limit?: number;
47
+ }
48
+ export interface PasswordIssue {
49
+ itemId: string;
50
+ title: string;
51
+ kind: "weak_password" | "reused_password" | "missing_url" | "old_password" | "passkey_available";
52
+ severity: "low" | "medium" | "high";
53
+ message: string;
54
+ }
55
+ export interface PasswordSecurityReport {
56
+ totalItems: number;
57
+ loginItems: number;
58
+ weakPasswords: number;
59
+ reusedPasswords: number;
60
+ missingUrls: number;
61
+ oldPasswords: number;
62
+ passkeyItems: number;
63
+ issues: PasswordIssue[];
64
+ }
65
+ export declare function nowIso(): string;
66
+ export declare function normalizePasswordItemType(value: unknown): PasswordVaultItemType;
67
+ export declare function normalizeVaultName(value: unknown): string;
68
+ export declare function normalizePasswordItemInput(input: PasswordVaultItemInput): Required<Pick<PasswordVaultItemInput, "type" | "title" | "urls" | "fields" | "tags" | "favorite">> & PasswordVaultItemInput;
69
+ export declare function normalizeUrls(urls: unknown): string[];
70
+ export declare function normalizeStoredUrl(value: string): string | null;
71
+ export declare function urlsMatch(storedUrl: string, pageUrl: string): boolean;
72
+ export declare function itemMatchesFilter(item: PasswordVaultItem, filter: PasswordVaultItemFilter): boolean;
73
+ export declare function scorePasswordStrength(password: string | undefined): number;
74
+ export declare function buildPasswordSecurityReport(items: PasswordVaultItem[], now?: number): PasswordSecurityReport;
75
+ export interface PasswordGeneratorOptions {
76
+ length?: number;
77
+ digits?: boolean;
78
+ symbols?: boolean;
79
+ }
80
+ export declare function generatePassword(options?: PasswordGeneratorOptions): string;
81
+ export declare function generateTotpCode(secret: string, timeMs?: number, digits?: number, period?: number): string;
82
+ export declare function decodeTotpSecret(secret: string): Buffer;
@@ -0,0 +1,349 @@
1
+ import crypto from "node:crypto";
2
+ export const DEFAULT_BROWSER_EXTENSION_BASE_URL = "https://home.huniu.fun:8183";
3
+ export const DEFAULT_PASSWORD_VAULT_ID = "personal";
4
+ export const DEFAULT_PASSWORD_VAULT_NAME = "Personal";
5
+ const ITEM_TYPES = new Set([
6
+ "login",
7
+ "credit_card",
8
+ "identity",
9
+ "secure_note",
10
+ "passkey",
11
+ ]);
12
+ const COMMON_WEAK_PASSWORDS = new Set([
13
+ "123456",
14
+ "123456789",
15
+ "qwerty",
16
+ "password",
17
+ "111111",
18
+ "abc123",
19
+ "password1",
20
+ "iloveyou",
21
+ ]);
22
+ export function nowIso() {
23
+ return new Date().toISOString();
24
+ }
25
+ export function normalizePasswordItemType(value) {
26
+ return typeof value === "string" && ITEM_TYPES.has(value)
27
+ ? value
28
+ : "login";
29
+ }
30
+ export function normalizeVaultName(value) {
31
+ const name = typeof value === "string" ? value.trim() : "";
32
+ if (!name) {
33
+ throw new Error("Vault name is required.");
34
+ }
35
+ if (name.length > 80) {
36
+ throw new Error("Vault name must be 80 characters or fewer.");
37
+ }
38
+ return name;
39
+ }
40
+ export function normalizePasswordItemInput(input) {
41
+ const type = normalizePasswordItemType(input.type);
42
+ const title = typeof input.title === "string" ? input.title.trim() : "";
43
+ if (!title) {
44
+ throw new Error("Item title is required.");
45
+ }
46
+ if (title.length > 160) {
47
+ throw new Error("Item title must be 160 characters or fewer.");
48
+ }
49
+ return {
50
+ ...input,
51
+ type,
52
+ title,
53
+ username: cleanOptionalString(input.username, 320),
54
+ password: cleanOptionalString(input.password, 4096),
55
+ urls: normalizeUrls(input.urls),
56
+ notes: cleanOptionalString(input.notes, 10000),
57
+ fields: normalizeFields(input.fields),
58
+ tags: normalizeTags(input.tags),
59
+ favorite: input.favorite === true,
60
+ };
61
+ }
62
+ export function normalizeUrls(urls) {
63
+ if (!Array.isArray(urls))
64
+ return [];
65
+ const out = [];
66
+ for (const value of urls) {
67
+ if (typeof value !== "string")
68
+ continue;
69
+ const normalized = normalizeStoredUrl(value);
70
+ if (normalized && !out.includes(normalized))
71
+ out.push(normalized);
72
+ }
73
+ return out.slice(0, 20);
74
+ }
75
+ export function normalizeStoredUrl(value) {
76
+ const raw = value.trim();
77
+ if (!raw)
78
+ return null;
79
+ try {
80
+ const parsed = new URL(raw.includes("://") ? raw : `https://${raw}`);
81
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
82
+ return null;
83
+ parsed.username = "";
84
+ parsed.password = "";
85
+ parsed.hash = "";
86
+ return parsed.origin + normalizePathname(parsed.pathname);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ }
92
+ export function urlsMatch(storedUrl, pageUrl) {
93
+ try {
94
+ const stored = new URL(normalizeStoredUrl(storedUrl) ?? storedUrl);
95
+ const page = new URL(pageUrl);
96
+ if (stored.protocol !== "http:" && stored.protocol !== "https:")
97
+ return false;
98
+ if (page.protocol !== "http:" && page.protocol !== "https:")
99
+ return false;
100
+ const storedHost = stored.hostname.toLowerCase();
101
+ const pageHost = page.hostname.toLowerCase();
102
+ const hostMatch = pageHost === storedHost || pageHost.endsWith(`.${storedHost}`);
103
+ if (!hostMatch)
104
+ return false;
105
+ const storedPath = normalizePathname(stored.pathname);
106
+ return storedPath === "/" || normalizePathname(page.pathname).startsWith(storedPath);
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ export function itemMatchesFilter(item, filter) {
113
+ if (filter.type && item.type !== filter.type)
114
+ return false;
115
+ if (filter.vaultId && item.vaultId !== filter.vaultId)
116
+ return false;
117
+ if (filter.url && !item.urls.some((url) => urlsMatch(url, filter.url)))
118
+ return false;
119
+ const q = filter.q?.trim().toLowerCase();
120
+ if (q) {
121
+ const haystack = [
122
+ item.title,
123
+ item.username,
124
+ ...item.urls,
125
+ ...item.tags,
126
+ ...Object.values(item.fields),
127
+ ].filter(Boolean).join("\n").toLowerCase();
128
+ if (!haystack.includes(q))
129
+ return false;
130
+ }
131
+ return true;
132
+ }
133
+ export function scorePasswordStrength(password) {
134
+ if (!password)
135
+ return 0;
136
+ const lower = password.toLowerCase();
137
+ if (COMMON_WEAK_PASSWORDS.has(lower))
138
+ return 0;
139
+ let score = Math.min(40, password.length * 3);
140
+ if (/[a-z]/.test(password))
141
+ score += 10;
142
+ if (/[A-Z]/.test(password))
143
+ score += 10;
144
+ if (/\d/.test(password))
145
+ score += 10;
146
+ if (/[^A-Za-z0-9]/.test(password))
147
+ score += 15;
148
+ if (password.length >= 20)
149
+ score += 15;
150
+ if (/(.)\1{2,}/.test(password))
151
+ score -= 20;
152
+ if (/^(?:[a-z]+|\d+)$/.test(password))
153
+ score -= 20;
154
+ return Math.max(0, Math.min(100, score));
155
+ }
156
+ export function buildPasswordSecurityReport(items, now = Date.now()) {
157
+ const issues = [];
158
+ const loginItems = items.filter((item) => item.type === "login");
159
+ const passwordGroups = new Map();
160
+ let weakPasswords = 0;
161
+ let missingUrls = 0;
162
+ let oldPasswords = 0;
163
+ let passkeyItems = 0;
164
+ for (const item of items) {
165
+ if (item.type === "passkey")
166
+ passkeyItems += 1;
167
+ if (item.type !== "login")
168
+ continue;
169
+ if (!item.urls.length) {
170
+ missingUrls += 1;
171
+ issues.push({
172
+ itemId: item.id,
173
+ title: item.title,
174
+ kind: "missing_url",
175
+ severity: "medium",
176
+ message: "Login item has no website URL, so phishing checks and autofill matching are limited.",
177
+ });
178
+ }
179
+ if (item.password) {
180
+ const group = passwordGroups.get(item.password) ?? [];
181
+ group.push(item);
182
+ passwordGroups.set(item.password, group);
183
+ if (scorePasswordStrength(item.password) < 50) {
184
+ weakPasswords += 1;
185
+ issues.push({
186
+ itemId: item.id,
187
+ title: item.title,
188
+ kind: "weak_password",
189
+ severity: "high",
190
+ message: "Password is short, common, or lacks character variety.",
191
+ });
192
+ }
193
+ }
194
+ if (item.passwordUpdatedAt) {
195
+ const ageMs = now - Date.parse(item.passwordUpdatedAt);
196
+ if (Number.isFinite(ageMs) && ageMs > 365 * 24 * 60 * 60 * 1000) {
197
+ oldPasswords += 1;
198
+ issues.push({
199
+ itemId: item.id,
200
+ title: item.title,
201
+ kind: "old_password",
202
+ severity: "low",
203
+ message: "Password has not been updated in over a year.",
204
+ });
205
+ }
206
+ }
207
+ if (item.fields.passkeyAvailable === "true") {
208
+ issues.push({
209
+ itemId: item.id,
210
+ title: item.title,
211
+ kind: "passkey_available",
212
+ severity: "low",
213
+ message: "This site can be upgraded to a passkey when browser WebAuthn integration is enabled.",
214
+ });
215
+ }
216
+ }
217
+ let reusedPasswords = 0;
218
+ for (const group of passwordGroups.values()) {
219
+ if (group.length < 2)
220
+ continue;
221
+ reusedPasswords += group.length;
222
+ for (const item of group) {
223
+ issues.push({
224
+ itemId: item.id,
225
+ title: item.title,
226
+ kind: "reused_password",
227
+ severity: "high",
228
+ message: "This password is reused by another login item.",
229
+ });
230
+ }
231
+ }
232
+ return {
233
+ totalItems: items.length,
234
+ loginItems: loginItems.length,
235
+ weakPasswords,
236
+ reusedPasswords,
237
+ missingUrls,
238
+ oldPasswords,
239
+ passkeyItems,
240
+ issues: issues.sort((a, b) => issueRank(b.severity) - issueRank(a.severity)),
241
+ };
242
+ }
243
+ export function generatePassword(options = {}) {
244
+ const length = clampInteger(options.length ?? 20, 8, 80);
245
+ const lower = "abcdefghijkmnopqrstuvwxyz";
246
+ const upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
247
+ const digits = "23456789";
248
+ const symbols = "!@#$%^&*-_=+?";
249
+ const pools = [lower, upper];
250
+ if (options.digits !== false)
251
+ pools.push(digits);
252
+ if (options.symbols !== false)
253
+ pools.push(symbols);
254
+ const alphabet = pools.join("");
255
+ const chars = pools.map((pool) => randomChar(pool));
256
+ while (chars.length < length)
257
+ chars.push(randomChar(alphabet));
258
+ return shuffle(chars).join("");
259
+ }
260
+ export function generateTotpCode(secret, timeMs = Date.now(), digits = 6, period = 30) {
261
+ const key = decodeTotpSecret(secret);
262
+ const counter = Math.floor(timeMs / 1000 / period);
263
+ const buf = Buffer.alloc(8);
264
+ buf.writeBigUInt64BE(BigInt(counter));
265
+ const digest = crypto.createHmac("sha1", key).update(buf).digest();
266
+ const offset = digest[digest.length - 1] & 0x0f;
267
+ const code = (((digest[offset] & 0x7f) << 24)
268
+ | ((digest[offset + 1] & 0xff) << 16)
269
+ | ((digest[offset + 2] & 0xff) << 8)
270
+ | (digest[offset + 3] & 0xff)) % (10 ** digits);
271
+ return String(code).padStart(digits, "0");
272
+ }
273
+ export function decodeTotpSecret(secret) {
274
+ const cleaned = secret.replace(/\s+/g, "").replace(/=+$/g, "").toUpperCase();
275
+ if (!cleaned)
276
+ throw new Error("TOTP secret is required.");
277
+ if (/^[0-9a-f]+$/i.test(cleaned) && cleaned.length % 2 === 0) {
278
+ return Buffer.from(cleaned, "hex");
279
+ }
280
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
281
+ let bits = "";
282
+ for (const char of cleaned) {
283
+ const value = alphabet.indexOf(char);
284
+ if (value < 0)
285
+ throw new Error("Invalid TOTP secret.");
286
+ bits += value.toString(2).padStart(5, "0");
287
+ }
288
+ const bytes = [];
289
+ for (let i = 0; i + 8 <= bits.length; i += 8) {
290
+ bytes.push(parseInt(bits.slice(i, i + 8), 2));
291
+ }
292
+ return Buffer.from(bytes);
293
+ }
294
+ function cleanOptionalString(value, maxLength) {
295
+ if (typeof value !== "string")
296
+ return undefined;
297
+ const trimmed = value.trim();
298
+ if (!trimmed)
299
+ return undefined;
300
+ return trimmed.slice(0, maxLength);
301
+ }
302
+ function normalizeFields(fields) {
303
+ if (!fields || typeof fields !== "object" || Array.isArray(fields))
304
+ return {};
305
+ const out = {};
306
+ for (const [key, value] of Object.entries(fields)) {
307
+ const normalizedKey = key.trim().slice(0, 80);
308
+ if (!normalizedKey || value === undefined || value === null)
309
+ continue;
310
+ out[normalizedKey] = String(value).trim().slice(0, 4096);
311
+ }
312
+ return out;
313
+ }
314
+ function normalizeTags(tags) {
315
+ if (!Array.isArray(tags))
316
+ return [];
317
+ const out = [];
318
+ for (const tag of tags) {
319
+ if (typeof tag !== "string")
320
+ continue;
321
+ const value = tag.trim().slice(0, 60);
322
+ if (value && !out.includes(value))
323
+ out.push(value);
324
+ }
325
+ return out.slice(0, 40);
326
+ }
327
+ function normalizePathname(pathname) {
328
+ if (!pathname || pathname === "/")
329
+ return "/";
330
+ return pathname.endsWith("/") ? pathname : `${pathname}/`;
331
+ }
332
+ function issueRank(severity) {
333
+ return severity === "high" ? 3 : severity === "medium" ? 2 : 1;
334
+ }
335
+ function clampInteger(value, min, max) {
336
+ if (!Number.isFinite(value))
337
+ return min;
338
+ return Math.max(min, Math.min(max, Math.floor(value)));
339
+ }
340
+ function randomChar(alphabet) {
341
+ return alphabet[crypto.randomInt(0, alphabet.length)];
342
+ }
343
+ function shuffle(values) {
344
+ for (let i = values.length - 1; i > 0; i -= 1) {
345
+ const j = crypto.randomInt(0, i + 1);
346
+ [values[i], values[j]] = [values[j], values[i]];
347
+ }
348
+ return values;
349
+ }