agent-vault-cli 0.1.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.
Files changed (87) hide show
  1. package/.cursor/skills/npm-publish/SKILL.md +58 -0
  2. package/.github/workflows/ci.yml +67 -0
  3. package/README.md +164 -0
  4. package/ROADMAP.md +986 -0
  5. package/dist/commands/config.d.ts +8 -0
  6. package/dist/commands/config.d.ts.map +1 -0
  7. package/dist/commands/config.js +67 -0
  8. package/dist/commands/config.js.map +1 -0
  9. package/dist/commands/delete.d.ts +7 -0
  10. package/dist/commands/delete.d.ts.map +1 -0
  11. package/dist/commands/delete.js +30 -0
  12. package/dist/commands/delete.js.map +1 -0
  13. package/dist/commands/login.d.ts +7 -0
  14. package/dist/commands/login.d.ts.map +1 -0
  15. package/dist/commands/login.js +37 -0
  16. package/dist/commands/login.js.map +1 -0
  17. package/dist/commands/register.d.ts +13 -0
  18. package/dist/commands/register.d.ts.map +1 -0
  19. package/dist/commands/register.js +160 -0
  20. package/dist/commands/register.js.map +1 -0
  21. package/dist/core/audit.d.ts +15 -0
  22. package/dist/core/audit.d.ts.map +1 -0
  23. package/dist/core/audit.js +36 -0
  24. package/dist/core/audit.js.map +1 -0
  25. package/dist/core/browser.d.ts +7 -0
  26. package/dist/core/browser.d.ts.map +1 -0
  27. package/dist/core/browser.js +104 -0
  28. package/dist/core/browser.js.map +1 -0
  29. package/dist/core/config.d.ts +9 -0
  30. package/dist/core/config.d.ts.map +1 -0
  31. package/dist/core/config.js +80 -0
  32. package/dist/core/config.js.map +1 -0
  33. package/dist/core/crypto.d.ts +17 -0
  34. package/dist/core/crypto.d.ts.map +1 -0
  35. package/dist/core/crypto.js +90 -0
  36. package/dist/core/crypto.js.map +1 -0
  37. package/dist/core/fields.d.ts +5 -0
  38. package/dist/core/fields.d.ts.map +1 -0
  39. package/dist/core/fields.js +54 -0
  40. package/dist/core/fields.js.map +1 -0
  41. package/dist/core/keychain.d.ts +5 -0
  42. package/dist/core/keychain.d.ts.map +1 -0
  43. package/dist/core/keychain.js +97 -0
  44. package/dist/core/keychain.js.map +1 -0
  45. package/dist/core/origin.d.ts +25 -0
  46. package/dist/core/origin.d.ts.map +1 -0
  47. package/dist/core/origin.js +73 -0
  48. package/dist/core/origin.js.map +1 -0
  49. package/dist/core/ratelimit.d.ts +10 -0
  50. package/dist/core/ratelimit.d.ts.map +1 -0
  51. package/dist/core/ratelimit.js +70 -0
  52. package/dist/core/ratelimit.js.map +1 -0
  53. package/dist/core/secure-memory.d.ts +39 -0
  54. package/dist/core/secure-memory.d.ts.map +1 -0
  55. package/dist/core/secure-memory.js +68 -0
  56. package/dist/core/secure-memory.js.map +1 -0
  57. package/dist/index.d.ts +3 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +129 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/types/index.d.ts +27 -0
  62. package/dist/types/index.d.ts.map +1 -0
  63. package/dist/types/index.js +2 -0
  64. package/dist/types/index.js.map +1 -0
  65. package/package.json +58 -0
  66. package/src/commands/config.ts +84 -0
  67. package/src/commands/delete.ts +39 -0
  68. package/src/commands/login.ts +49 -0
  69. package/src/commands/register.ts +188 -0
  70. package/src/core/audit.ts +59 -0
  71. package/src/core/browser.ts +131 -0
  72. package/src/core/config.ts +91 -0
  73. package/src/core/crypto.ts +106 -0
  74. package/src/core/fields.ts +59 -0
  75. package/src/core/keychain.ts +110 -0
  76. package/src/core/origin.ts +90 -0
  77. package/src/core/ratelimit.ts +89 -0
  78. package/src/core/secure-memory.ts +78 -0
  79. package/src/index.ts +133 -0
  80. package/src/types/index.ts +31 -0
  81. package/tests/browser-password-manager.test.ts +1023 -0
  82. package/tests/crypto.test.ts +140 -0
  83. package/tests/e2e.test.ts +565 -0
  84. package/tests/fixtures/server.ts +59 -0
  85. package/tests/security.test.ts +113 -0
  86. package/tsconfig.json +20 -0
  87. package/vitest.config.ts +17 -0
@@ -0,0 +1,1023 @@
1
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
2
+ import { chromium, Browser, BrowserContext } from 'playwright';
3
+ import { spawn, ChildProcess } from 'child_process';
4
+ import { startTestServer, TestServer } from './fixtures/server.js';
5
+ import http from 'http';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { resetRateLimit } from '../src/core/ratelimit.js';
10
+
11
+ /**
12
+ * Tests for Chromium's built-in password manager with --user-data-dir.
13
+ *
14
+ * This explores whether the browser's native credential storage can be used
15
+ * as an alternative to keychain-based storage for credential management.
16
+ */
17
+
18
+ const TEST_PORT = 9601;
19
+ const CDP_PORT = 9444;
20
+
21
+ // Run with HEADFUL=1 npm test to see the browser
22
+ const HEADFUL = process.env.HEADFUL === '1' || process.env.HEADFUL === 'true';
23
+
24
+ async function getCdpEndpoint(port: number): Promise<string> {
25
+ return new Promise((resolve, reject) => {
26
+ const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {
27
+ let data = '';
28
+ res.on('data', (chunk) => (data += chunk));
29
+ res.on('end', () => {
30
+ try {
31
+ const json = JSON.parse(data);
32
+ resolve(json.webSocketDebuggerUrl);
33
+ } catch (e) {
34
+ reject(e);
35
+ }
36
+ });
37
+ });
38
+ req.on('error', reject);
39
+ req.setTimeout(5000, () => {
40
+ req.destroy();
41
+ reject(new Error('Timeout getting CDP endpoint'));
42
+ });
43
+ });
44
+ }
45
+
46
+ async function waitForCdp(port: number, maxRetries = 30): Promise<string> {
47
+ for (let i = 0; i < maxRetries; i++) {
48
+ try {
49
+ return await getCdpEndpoint(port);
50
+ } catch {
51
+ await new Promise((r) => setTimeout(r, 200));
52
+ }
53
+ }
54
+ throw new Error(`Could not connect to CDP on port ${port}`);
55
+ }
56
+
57
+ async function visualDelay(ms: number = 300): Promise<void> {
58
+ if (HEADFUL) {
59
+ await new Promise((r) => setTimeout(r, ms));
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Creates a temporary user data directory for Chrome
65
+ */
66
+ function createTempUserDataDir(): string {
67
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'chrome-profile-'));
68
+ return tmpDir;
69
+ }
70
+
71
+ /**
72
+ * Cleans up the user data directory
73
+ */
74
+ function cleanupUserDataDir(dir: string): void {
75
+ try {
76
+ fs.rmSync(dir, { recursive: true, force: true });
77
+ } catch {
78
+ // Ignore cleanup errors
79
+ }
80
+ }
81
+
82
+ describe('Browser Password Manager with --user-data-dir', () => {
83
+ let server: TestServer;
84
+ let userDataDir: string;
85
+
86
+ beforeAll(async () => {
87
+ // Start test server
88
+ server = await startTestServer(TEST_PORT);
89
+ });
90
+
91
+ afterAll(async () => {
92
+ await server.close();
93
+ });
94
+
95
+ describe('Password persistence across browser sessions', () => {
96
+ let browserProcess: ChildProcess | null = null;
97
+
98
+ afterEach(async () => {
99
+ // Kill browser process
100
+ if (browserProcess) {
101
+ browserProcess.kill('SIGTERM');
102
+ browserProcess = null;
103
+ // Wait for process to fully terminate
104
+ await new Promise((r) => setTimeout(r, 500));
105
+ }
106
+ });
107
+
108
+ it('compares headless vs headful auto-fill with pre-populated Login Data', async () => {
109
+ // This test directly pre-populates Chrome's Login Data SQLite database
110
+ // to see if Chrome will auto-fill from it
111
+ userDataDir = createTempUserDataDir();
112
+
113
+ try {
114
+ const chromiumPath = chromium.executablePath();
115
+ const testOrigin = `http://127.0.0.1:${TEST_PORT}`;
116
+ const { execSync } = await import('child_process');
117
+
118
+ // === STEP 1: Create Chrome profile structure and Login Data ===
119
+ console.log('\nšŸ“ Step 1: Pre-populating Login Data SQLite database...');
120
+
121
+ // Create Default profile directory
122
+ const defaultDir = path.join(userDataDir, 'Default');
123
+ fs.mkdirSync(defaultDir, { recursive: true });
124
+
125
+ // Chrome's Login Data is a SQLite database
126
+ // We need to create the schema and insert a credential
127
+ // Note: With --password-store=basic, passwords are stored in plaintext (for testing)
128
+ const loginDataPath = path.join(defaultDir, 'Login Data');
129
+
130
+ // Create the SQLite database using sqlite3 command
131
+ const createTableSQL = `
132
+ CREATE TABLE logins (
133
+ origin_url TEXT NOT NULL,
134
+ action_url TEXT,
135
+ username_element TEXT,
136
+ username_value TEXT,
137
+ password_element TEXT,
138
+ password_value BLOB,
139
+ submit_element TEXT,
140
+ signon_realm TEXT NOT NULL,
141
+ date_created INTEGER NOT NULL,
142
+ blacklisted_by_user INTEGER NOT NULL,
143
+ scheme INTEGER NOT NULL,
144
+ password_type INTEGER,
145
+ times_used INTEGER,
146
+ form_data BLOB,
147
+ display_name TEXT,
148
+ icon_url TEXT,
149
+ federation_url TEXT,
150
+ skip_zero_click INTEGER,
151
+ generation_upload_status INTEGER,
152
+ possible_username_pairs BLOB,
153
+ id INTEGER PRIMARY KEY,
154
+ date_last_used INTEGER NOT NULL DEFAULT 0,
155
+ moving_blocked_for BLOB,
156
+ date_password_modified INTEGER NOT NULL DEFAULT 0
157
+ );
158
+ CREATE INDEX logins_signon ON logins (signon_realm);
159
+ `;
160
+
161
+ // Insert a test credential
162
+ // For --password-store=basic, the password is stored as plaintext
163
+ const insertSQL = `
164
+ INSERT INTO logins (
165
+ origin_url, action_url, username_element, username_value,
166
+ password_element, password_value, submit_element, signon_realm,
167
+ date_created, blacklisted_by_user, scheme, password_type,
168
+ times_used, date_last_used, date_password_modified
169
+ ) VALUES (
170
+ '${testOrigin}/', '${testOrigin}/', 'email', 'prefilled@example.com',
171
+ 'password', 'PrefilledPass123!', 'submit-btn', '${testOrigin}/',
172
+ ${Date.now() * 1000}, 0, 0, 0,
173
+ 1, ${Date.now() * 1000}, ${Date.now() * 1000}
174
+ );
175
+ `;
176
+
177
+ try {
178
+ // Create database and tables
179
+ execSync(`sqlite3 "${loginDataPath}" "${createTableSQL}"`, { encoding: 'utf-8' });
180
+ execSync(`sqlite3 "${loginDataPath}" "${insertSQL}"`, { encoding: 'utf-8' });
181
+
182
+ // Verify the data was inserted
183
+ const countResult = execSync(`sqlite3 "${loginDataPath}" "SELECT COUNT(*) FROM logins;"`, { encoding: 'utf-8' });
184
+ console.log(` Created Login Data with ${countResult.trim()} credential(s)`);
185
+
186
+ // Show what we inserted
187
+ const selectResult = execSync(
188
+ `sqlite3 "${loginDataPath}" "SELECT username_value, password_value, signon_realm FROM logins;"`,
189
+ { encoding: 'utf-8' }
190
+ );
191
+ console.log(` Stored: ${selectResult.trim()}`);
192
+ } catch (e: any) {
193
+ console.log(' Note: sqlite3 not available, using empty profile');
194
+ console.log(` Error: ${e.message}`);
195
+ }
196
+
197
+ // === STEP 2: Test HEADLESS auto-fill ===
198
+ console.log('\nšŸ¤– Step 2: Testing HEADLESS auto-fill...');
199
+
200
+ const headlessArgs = [
201
+ `--remote-debugging-port=${CDP_PORT}`,
202
+ `--user-data-dir=${userDataDir}`,
203
+ '--no-first-run',
204
+ '--no-default-browser-check',
205
+ '--disable-extensions',
206
+ '--disable-component-update',
207
+ '--enable-features=PasswordManager',
208
+ '--password-store=basic',
209
+ '--headless=new',
210
+ '--disable-gpu',
211
+ 'about:blank',
212
+ ];
213
+
214
+ browserProcess = spawn(chromiumPath, headlessArgs, {
215
+ stdio: 'pipe',
216
+ detached: false,
217
+ });
218
+
219
+ let wsEndpoint = await waitForCdp(CDP_PORT);
220
+ let browser = await chromium.connectOverCDP(wsEndpoint);
221
+ let context = browser.contexts()[0];
222
+ let page = context.pages()[0] || await context.newPage();
223
+
224
+ await page.goto(testOrigin);
225
+ await new Promise((r) => setTimeout(r, 2000)); // Wait for potential auto-fill
226
+
227
+ // Click on email field to potentially trigger auto-fill
228
+ await page.click('#email');
229
+ await new Promise((r) => setTimeout(r, 500));
230
+
231
+ const headlessEmail = await page.inputValue('#email');
232
+ const headlessPassword = await page.inputValue('#password');
233
+
234
+ console.log(' HEADLESS results:');
235
+ console.log(` - Form auto-fill: email="${headlessEmail || '(empty)'}", password=${headlessPassword ? '"****"' : '"(empty)"'}`);
236
+
237
+ await browser.close();
238
+ browserProcess.kill('SIGTERM');
239
+ browserProcess = null;
240
+ await new Promise((r) => setTimeout(r, 1000));
241
+
242
+ // === STEP 3: Test HEADFUL auto-fill ===
243
+ console.log('\nšŸ‘ļø Step 3: Testing HEADFUL auto-fill...');
244
+
245
+ const headfulArgs = [
246
+ `--remote-debugging-port=${CDP_PORT}`,
247
+ `--user-data-dir=${userDataDir}`,
248
+ '--no-first-run',
249
+ '--no-default-browser-check',
250
+ '--disable-extensions',
251
+ '--disable-component-update',
252
+ '--enable-features=PasswordManager',
253
+ '--password-store=basic',
254
+ '--window-size=800,600',
255
+ 'about:blank',
256
+ ];
257
+
258
+ browserProcess = spawn(chromiumPath, headfulArgs, {
259
+ stdio: 'pipe',
260
+ detached: false,
261
+ });
262
+
263
+ wsEndpoint = await waitForCdp(CDP_PORT);
264
+ browser = await chromium.connectOverCDP(wsEndpoint);
265
+ context = browser.contexts()[0];
266
+ page = context.pages()[0] || await context.newPage();
267
+
268
+ await page.goto(testOrigin);
269
+ await new Promise((r) => setTimeout(r, 2000)); // Wait for potential auto-fill
270
+
271
+ // Click on email field to potentially trigger auto-fill
272
+ await page.click('#email');
273
+ await new Promise((r) => setTimeout(r, 500));
274
+
275
+ const headfulEmail = await page.inputValue('#email');
276
+ const headfulPassword = await page.inputValue('#password');
277
+
278
+ console.log(' HEADFUL results:');
279
+ console.log(` - Form auto-fill: email="${headfulEmail || '(empty)'}", password=${headfulPassword ? '"****"' : '"(empty)"'}`);
280
+
281
+ await browser.close();
282
+
283
+ // === Summary ===
284
+ console.log('\nšŸ“Š Comparison Summary:');
285
+ console.log('ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”');
286
+ console.log('│ Mode │ Auto-fill from Login Data │');
287
+ console.log('ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤');
288
+ console.log(`│ HEADLESS │ ${headlessEmail ? 'YES āœ… ' + headlessEmail : 'NO āŒ'} │`);
289
+ console.log(`│ HEADFUL │ ${headfulEmail ? 'YES āœ… ' + headfulEmail : 'NO āŒ'} │`);
290
+ console.log('ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜');
291
+
292
+ // === STEP 4: Check what Chrome sees ===
293
+ console.log('\nšŸ” Step 4: Investigating why auto-fill failed...');
294
+
295
+ // Start browser again to inspect
296
+ browserProcess = spawn(chromiumPath, headfulArgs, {
297
+ stdio: 'pipe',
298
+ detached: false,
299
+ });
300
+
301
+ wsEndpoint = await waitForCdp(CDP_PORT);
302
+ browser = await chromium.connectOverCDP(wsEndpoint);
303
+ context = browser.contexts()[0];
304
+ page = context.pages()[0] || await context.newPage();
305
+
306
+ await page.goto(testOrigin);
307
+
308
+ // Check form field attributes
309
+ const formAnalysis = await page.evaluate(() => {
310
+ const email = document.querySelector('#email') as HTMLInputElement;
311
+ const password = document.querySelector('#password') as HTMLInputElement;
312
+
313
+ return {
314
+ emailField: {
315
+ id: email?.id,
316
+ name: email?.name,
317
+ type: email?.type,
318
+ autocomplete: email?.autocomplete,
319
+ },
320
+ passwordField: {
321
+ id: password?.id,
322
+ name: password?.name,
323
+ type: password?.type,
324
+ autocomplete: password?.autocomplete,
325
+ },
326
+ };
327
+ });
328
+
329
+ console.log(' Form field attributes:');
330
+ console.log(` - Email: ${JSON.stringify(formAnalysis.emailField)}`);
331
+ console.log(` - Password: ${JSON.stringify(formAnalysis.passwordField)}`);
332
+
333
+ // Try to use CDP to get stored passwords
334
+ const cdpClient = await context.newCDPSession(page);
335
+
336
+ try {
337
+ // Enable Autofill domain if available
338
+ await cdpClient.send('Autofill.enable' as any);
339
+ console.log(' CDP Autofill domain enabled');
340
+ } catch (e: any) {
341
+ console.log(` CDP Autofill domain not available: ${e.message}`);
342
+ }
343
+
344
+ // Check Login Data file after Chrome touched it
345
+ try {
346
+ const selectAfter = execSync(
347
+ `sqlite3 "${path.join(userDataDir, 'Default', 'Login Data')}" "SELECT username_value, signon_realm FROM logins;" 2>/dev/null || echo "Database locked or missing"`,
348
+ { encoding: 'utf-8' }
349
+ );
350
+ console.log(` Login Data after Chrome: ${selectAfter.trim() || '(empty or locked)'}`);
351
+ } catch {
352
+ console.log(' Could not read Login Data (might be locked by Chrome)');
353
+ }
354
+
355
+ await browser.close();
356
+
357
+ // Summary
358
+ if (!headlessEmail && !headfulEmail) {
359
+ console.log('\nšŸ’” Conclusion: Chrome password manager does NOT auto-fill because:');
360
+ console.log(' 1. Auto-fill requires user to click on field suggestions dropdown');
361
+ console.log(' 2. Automation cannot trigger the dropdown programmatically');
362
+ console.log(' 3. This is a SECURITY feature, not a bug');
363
+ console.log('\n āž”ļø The CLI keychain approach is the correct solution for automation!');
364
+ }
365
+
366
+ expect(true).toBe(true);
367
+
368
+ } finally {
369
+ cleanupUserDataDir(userDataDir);
370
+ }
371
+ });
372
+
373
+ it('stores and retrieves credentials using Chrome password manager', async () => {
374
+ userDataDir = createTempUserDataDir();
375
+
376
+ try {
377
+ const chromiumPath = chromium.executablePath();
378
+ const testOrigin = `http://127.0.0.1:${TEST_PORT}`;
379
+
380
+ // === SESSION 1: Save credentials ===
381
+ console.log('\nšŸ“ Session 1: Saving credentials with password manager...');
382
+
383
+ const browserArgs1 = [
384
+ `--remote-debugging-port=${CDP_PORT}`,
385
+ `--user-data-dir=${userDataDir}`,
386
+ '--no-first-run',
387
+ '--no-default-browser-check',
388
+ '--disable-extensions',
389
+ '--disable-dev-shm-usage',
390
+ // Enable password manager features
391
+ '--enable-features=PasswordManager',
392
+ '--password-store=basic', // Use basic file-based storage (not OS keychain)
393
+ ];
394
+
395
+ if (!HEADFUL) {
396
+ browserArgs1.push('--headless=new', '--disable-gpu');
397
+ } else {
398
+ browserArgs1.push('--window-size=1280,800');
399
+ }
400
+
401
+ browserArgs1.push('about:blank');
402
+
403
+ browserProcess = spawn(chromiumPath, browserArgs1, {
404
+ stdio: HEADFUL ? 'inherit' : 'pipe',
405
+ detached: false,
406
+ });
407
+
408
+ const wsEndpoint1 = await waitForCdp(CDP_PORT);
409
+ const browser1 = await chromium.connectOverCDP(wsEndpoint1);
410
+ const context1 = browser1.contexts()[0];
411
+ const page1 = context1.pages()[0] || await context1.newPage();
412
+
413
+ // Navigate to login page
414
+ await page1.goto(testOrigin);
415
+ await visualDelay(500);
416
+
417
+ // Fill the form manually (simulating user input)
418
+ await page1.fill('#email', 'browser-test@example.com');
419
+ await page1.fill('#password', 'BrowserPass123!');
420
+ await visualDelay(500);
421
+
422
+ // Try to use the Credential Management API to save credentials
423
+ // This is how modern browsers handle password saving
424
+ const saveResult = await page1.evaluate(async () => {
425
+ try {
426
+ // Check if Credential Management API is available
427
+ if (!('credentials' in navigator)) {
428
+ return { success: false, reason: 'Credential Management API not available' };
429
+ }
430
+
431
+ // Create a PasswordCredential
432
+ const cred = new (window as any).PasswordCredential({
433
+ id: 'browser-test@example.com',
434
+ password: 'BrowserPass123!',
435
+ name: 'Browser Test User',
436
+ });
437
+
438
+ // Store the credential
439
+ await navigator.credentials.store(cred);
440
+ return { success: true };
441
+ } catch (e: any) {
442
+ return { success: false, reason: e.message || 'Unknown error' };
443
+ }
444
+ });
445
+
446
+ console.log('Credential save result:', saveResult);
447
+
448
+ // Submit the form to trigger potential password save prompt
449
+ await page1.click('#submit-btn');
450
+ await visualDelay(1000);
451
+
452
+ // Close the first session
453
+ await browser1.close();
454
+ browserProcess.kill('SIGTERM');
455
+ browserProcess = null;
456
+ await new Promise((r) => setTimeout(r, 1000));
457
+
458
+ // === SESSION 2: Retrieve credentials ===
459
+ console.log('\nšŸ”‘ Session 2: Attempting to retrieve saved credentials...');
460
+
461
+ const browserArgs2 = [
462
+ `--remote-debugging-port=${CDP_PORT}`,
463
+ `--user-data-dir=${userDataDir}`, // Same user data dir!
464
+ '--no-first-run',
465
+ '--no-default-browser-check',
466
+ '--disable-extensions',
467
+ '--disable-dev-shm-usage',
468
+ '--enable-features=PasswordManager',
469
+ '--password-store=basic',
470
+ ];
471
+
472
+ if (!HEADFUL) {
473
+ browserArgs2.push('--headless=new', '--disable-gpu');
474
+ } else {
475
+ browserArgs2.push('--window-size=1280,800');
476
+ }
477
+
478
+ browserArgs2.push('about:blank');
479
+
480
+ browserProcess = spawn(chromiumPath, browserArgs2, {
481
+ stdio: HEADFUL ? 'inherit' : 'pipe',
482
+ detached: false,
483
+ });
484
+
485
+ const wsEndpoint2 = await waitForCdp(CDP_PORT);
486
+ const browser2 = await chromium.connectOverCDP(wsEndpoint2);
487
+ const context2 = browser2.contexts()[0];
488
+ const page2 = context2.pages()[0] || await context2.newPage();
489
+
490
+ // Navigate to login page
491
+ await page2.goto(testOrigin);
492
+ await visualDelay(500);
493
+
494
+ // Check if form is auto-filled or if we can retrieve credentials
495
+ const emailValue = await page2.inputValue('#email');
496
+ const passwordValue = await page2.inputValue('#password');
497
+
498
+ console.log('Auto-fill check - Email:', emailValue || '(empty)');
499
+ console.log('Auto-fill check - Password:', passwordValue ? '****' : '(empty)');
500
+
501
+ // Try to retrieve credentials using Credential Management API
502
+ const retrieveResult = await page2.evaluate(async () => {
503
+ try {
504
+ if (!('credentials' in navigator)) {
505
+ return { success: false, reason: 'Credential Management API not available' };
506
+ }
507
+
508
+ // Try to get stored password credential
509
+ const cred = await navigator.credentials.get({
510
+ password: true,
511
+ mediation: 'silent', // Don't show UI prompt
512
+ } as any);
513
+
514
+ if (cred && (cred as any).password) {
515
+ return {
516
+ success: true,
517
+ id: (cred as any).id,
518
+ hasPassword: true,
519
+ };
520
+ }
521
+
522
+ return { success: false, reason: 'No credential found' };
523
+ } catch (e: any) {
524
+ return { success: false, reason: e.message || 'Unknown error' };
525
+ }
526
+ });
527
+
528
+ console.log('Credential retrieve result:', retrieveResult);
529
+
530
+ await browser2.close();
531
+
532
+ // Report findings
533
+ if (emailValue || retrieveResult.success) {
534
+ console.log('\nāœ… Browser password manager appears to work with --user-data-dir');
535
+ } else {
536
+ console.log('\nāš ļø Browser password manager did NOT auto-fill credentials');
537
+ console.log(' This is expected behavior in headless mode - the browser');
538
+ console.log(' password manager requires user interaction for security.');
539
+ }
540
+
541
+ // The test "passes" but reports findings - this is exploratory
542
+ expect(true).toBe(true);
543
+
544
+ } finally {
545
+ cleanupUserDataDir(userDataDir);
546
+ }
547
+ });
548
+
549
+ it('can use localStorage as fallback credential storage with user-data-dir', async () => {
550
+ userDataDir = createTempUserDataDir();
551
+
552
+ try {
553
+ const chromiumPath = chromium.executablePath();
554
+ const testOrigin = `http://127.0.0.1:${TEST_PORT}`;
555
+
556
+ // === SESSION 1: Save to localStorage ===
557
+ console.log('\nšŸ“ Session 1: Saving credentials to localStorage...');
558
+
559
+ const browserArgs1 = [
560
+ `--remote-debugging-port=${CDP_PORT}`,
561
+ `--user-data-dir=${userDataDir}`,
562
+ '--no-first-run',
563
+ '--no-default-browser-check',
564
+ '--disable-extensions',
565
+ '--disable-dev-shm-usage',
566
+ ];
567
+
568
+ if (!HEADFUL) {
569
+ browserArgs1.push('--headless=new', '--disable-gpu');
570
+ }
571
+
572
+ browserArgs1.push('about:blank');
573
+
574
+ browserProcess = spawn(chromiumPath, browserArgs1, {
575
+ stdio: HEADFUL ? 'inherit' : 'pipe',
576
+ detached: false,
577
+ });
578
+
579
+ const wsEndpoint1 = await waitForCdp(CDP_PORT);
580
+ const browser1 = await chromium.connectOverCDP(wsEndpoint1);
581
+ const context1 = browser1.contexts()[0];
582
+ const page1 = context1.pages()[0] || await context1.newPage();
583
+
584
+ await page1.goto(testOrigin);
585
+ await visualDelay(300);
586
+
587
+ // Save credentials to localStorage (simulating browser extension storage)
588
+ await page1.evaluate(() => {
589
+ const credentials = {
590
+ username: 'localstorage-test@example.com',
591
+ password: 'LocalPass456!',
592
+ savedAt: new Date().toISOString(),
593
+ };
594
+ // Note: In real use, this would be encrypted
595
+ localStorage.setItem('vault_credentials', JSON.stringify(credentials));
596
+ });
597
+
598
+ await browser1.close();
599
+ browserProcess.kill('SIGTERM');
600
+ browserProcess = null;
601
+ await new Promise((r) => setTimeout(r, 1000));
602
+
603
+ // === SESSION 2: Retrieve from localStorage ===
604
+ console.log('\nšŸ”‘ Session 2: Retrieving credentials from localStorage...');
605
+
606
+ const browserArgs2 = [
607
+ `--remote-debugging-port=${CDP_PORT}`,
608
+ `--user-data-dir=${userDataDir}`,
609
+ '--no-first-run',
610
+ '--no-default-browser-check',
611
+ '--disable-extensions',
612
+ '--disable-dev-shm-usage',
613
+ ];
614
+
615
+ if (!HEADFUL) {
616
+ browserArgs2.push('--headless=new', '--disable-gpu');
617
+ }
618
+
619
+ browserArgs2.push('about:blank');
620
+
621
+ browserProcess = spawn(chromiumPath, browserArgs2, {
622
+ stdio: HEADFUL ? 'inherit' : 'pipe',
623
+ detached: false,
624
+ });
625
+
626
+ const wsEndpoint2 = await waitForCdp(CDP_PORT);
627
+ const browser2 = await chromium.connectOverCDP(wsEndpoint2);
628
+ const context2 = browser2.contexts()[0];
629
+ const page2 = context2.pages()[0] || await context2.newPage();
630
+
631
+ await page2.goto(testOrigin);
632
+ await visualDelay(300);
633
+
634
+ // Retrieve credentials from localStorage
635
+ const storedCredentials = await page2.evaluate(() => {
636
+ const raw = localStorage.getItem('vault_credentials');
637
+ if (!raw) return null;
638
+ return JSON.parse(raw);
639
+ });
640
+
641
+ console.log('Retrieved credentials:', storedCredentials ? 'Found' : 'Not found');
642
+
643
+ expect(storedCredentials).not.toBeNull();
644
+ expect(storedCredentials.username).toBe('localstorage-test@example.com');
645
+ expect(storedCredentials.password).toBe('LocalPass456!');
646
+
647
+ // Fill the form using stored credentials
648
+ if (storedCredentials) {
649
+ await page2.fill('#email', storedCredentials.username);
650
+ await page2.fill('#password', storedCredentials.password);
651
+ await visualDelay(500);
652
+
653
+ const emailValue = await page2.inputValue('#email');
654
+ const passwordValue = await page2.inputValue('#password');
655
+
656
+ expect(emailValue).toBe('localstorage-test@example.com');
657
+ expect(passwordValue).toBe('LocalPass456!');
658
+ console.log('\nāœ… localStorage persistence works with --user-data-dir');
659
+ }
660
+
661
+ await browser2.close();
662
+
663
+ } finally {
664
+ cleanupUserDataDir(userDataDir);
665
+ }
666
+ });
667
+
668
+ it('IndexedDB persists across browser sessions with user-data-dir', async () => {
669
+ userDataDir = createTempUserDataDir();
670
+
671
+ try {
672
+ const chromiumPath = chromium.executablePath();
673
+ const testOrigin = `http://127.0.0.1:${TEST_PORT}`;
674
+
675
+ // === SESSION 1: Save to IndexedDB ===
676
+ console.log('\nšŸ“ Session 1: Saving credentials to IndexedDB...');
677
+
678
+ const browserArgs1 = [
679
+ `--remote-debugging-port=${CDP_PORT}`,
680
+ `--user-data-dir=${userDataDir}`,
681
+ '--no-first-run',
682
+ '--no-default-browser-check',
683
+ '--disable-extensions',
684
+ ];
685
+
686
+ if (!HEADFUL) {
687
+ browserArgs1.push('--headless=new', '--disable-gpu');
688
+ }
689
+
690
+ browserArgs1.push('about:blank');
691
+
692
+ browserProcess = spawn(chromiumPath, browserArgs1, {
693
+ stdio: HEADFUL ? 'inherit' : 'pipe',
694
+ detached: false,
695
+ });
696
+
697
+ const wsEndpoint1 = await waitForCdp(CDP_PORT);
698
+ const browser1 = await chromium.connectOverCDP(wsEndpoint1);
699
+ const context1 = browser1.contexts()[0];
700
+ const page1 = context1.pages()[0] || await context1.newPage();
701
+
702
+ await page1.goto(testOrigin);
703
+ await visualDelay(300);
704
+
705
+ // Save credentials to IndexedDB
706
+ const saveSuccess = await page1.evaluate(async () => {
707
+ return new Promise<boolean>((resolve, reject) => {
708
+ const request = indexedDB.open('VaultCredentials', 1);
709
+
710
+ request.onerror = () => reject(request.error);
711
+
712
+ request.onupgradeneeded = (event) => {
713
+ const db = (event.target as IDBOpenDBRequest).result;
714
+ if (!db.objectStoreNames.contains('credentials')) {
715
+ db.createObjectStore('credentials', { keyPath: 'origin' });
716
+ }
717
+ };
718
+
719
+ request.onsuccess = (event) => {
720
+ const db = (event.target as IDBOpenDBRequest).result;
721
+ const tx = db.transaction('credentials', 'readwrite');
722
+ const store = tx.objectStore('credentials');
723
+
724
+ const credential = {
725
+ origin: window.location.origin,
726
+ username: 'indexeddb-test@example.com',
727
+ password: 'IndexedDBPass789!',
728
+ selectors: {
729
+ username: '#email',
730
+ password: '#password',
731
+ },
732
+ savedAt: new Date().toISOString(),
733
+ };
734
+
735
+ const putRequest = store.put(credential);
736
+ putRequest.onsuccess = () => {
737
+ db.close();
738
+ resolve(true);
739
+ };
740
+ putRequest.onerror = () => {
741
+ db.close();
742
+ reject(putRequest.error);
743
+ };
744
+ };
745
+ });
746
+ });
747
+
748
+ expect(saveSuccess).toBe(true);
749
+ console.log('Saved to IndexedDB successfully');
750
+
751
+ await browser1.close();
752
+ browserProcess.kill('SIGTERM');
753
+ browserProcess = null;
754
+ await new Promise((r) => setTimeout(r, 1000));
755
+
756
+ // === SESSION 2: Retrieve from IndexedDB ===
757
+ console.log('\nšŸ”‘ Session 2: Retrieving credentials from IndexedDB...');
758
+
759
+ const browserArgs2 = [
760
+ `--remote-debugging-port=${CDP_PORT}`,
761
+ `--user-data-dir=${userDataDir}`,
762
+ '--no-first-run',
763
+ '--no-default-browser-check',
764
+ '--disable-extensions',
765
+ ];
766
+
767
+ if (!HEADFUL) {
768
+ browserArgs2.push('--headless=new', '--disable-gpu');
769
+ }
770
+
771
+ browserArgs2.push('about:blank');
772
+
773
+ browserProcess = spawn(chromiumPath, browserArgs2, {
774
+ stdio: HEADFUL ? 'inherit' : 'pipe',
775
+ detached: false,
776
+ });
777
+
778
+ const wsEndpoint2 = await waitForCdp(CDP_PORT);
779
+ const browser2 = await chromium.connectOverCDP(wsEndpoint2);
780
+ const context2 = browser2.contexts()[0];
781
+ const page2 = context2.pages()[0] || await context2.newPage();
782
+
783
+ await page2.goto(testOrigin);
784
+ await visualDelay(300);
785
+
786
+ // Retrieve credentials from IndexedDB and auto-fill
787
+ const storedCredential = await page2.evaluate(async () => {
788
+ return new Promise<any>((resolve, reject) => {
789
+ const request = indexedDB.open('VaultCredentials', 1);
790
+
791
+ request.onerror = () => reject(request.error);
792
+
793
+ request.onsuccess = (event) => {
794
+ const db = (event.target as IDBOpenDBRequest).result;
795
+ const tx = db.transaction('credentials', 'readonly');
796
+ const store = tx.objectStore('credentials');
797
+
798
+ const getRequest = store.get(window.location.origin);
799
+ getRequest.onsuccess = () => {
800
+ db.close();
801
+ resolve(getRequest.result || null);
802
+ };
803
+ getRequest.onerror = () => {
804
+ db.close();
805
+ reject(getRequest.error);
806
+ };
807
+ };
808
+ });
809
+ });
810
+
811
+ expect(storedCredential).not.toBeNull();
812
+ expect(storedCredential.username).toBe('indexeddb-test@example.com');
813
+ expect(storedCredential.password).toBe('IndexedDBPass789!');
814
+
815
+ console.log('Retrieved from IndexedDB:', storedCredential ? 'Found' : 'Not found');
816
+
817
+ // Fill the form using stored credentials
818
+ await page2.fill(storedCredential.selectors.username, storedCredential.username);
819
+ await page2.fill(storedCredential.selectors.password, storedCredential.password);
820
+ await visualDelay(500);
821
+
822
+ const emailValue = await page2.inputValue('#email');
823
+ const passwordValue = await page2.inputValue('#password');
824
+
825
+ expect(emailValue).toBe('indexeddb-test@example.com');
826
+ expect(passwordValue).toBe('IndexedDBPass789!');
827
+
828
+ console.log('\nāœ… IndexedDB persistence works with --user-data-dir');
829
+
830
+ await browser2.close();
831
+
832
+ } finally {
833
+ cleanupUserDataDir(userDataDir);
834
+ }
835
+ });
836
+ });
837
+
838
+ describe('Comparison: CLI keychain vs browser storage', () => {
839
+ let browserProcess: ChildProcess | null = null;
840
+
841
+ afterEach(async () => {
842
+ if (browserProcess) {
843
+ browserProcess.kill('SIGTERM');
844
+ browserProcess = null;
845
+ await new Promise((r) => setTimeout(r, 500));
846
+ }
847
+ });
848
+
849
+ it('CLI works with localhost using user-data-dir profile', async () => {
850
+ // This test demonstrates that the CLI works with any origin including localhost.
851
+ // The real security comes from OS keychain encryption, not origin blocking.
852
+ userDataDir = createTempUserDataDir();
853
+ await resetRateLimit();
854
+
855
+ try {
856
+ const chromiumPath = chromium.executablePath();
857
+ const testOrigin = `http://127.0.0.1:${TEST_PORT}`;
858
+ const { execSync } = await import('child_process');
859
+
860
+ // Start browser with fresh profile
861
+ const browserArgs = [
862
+ `--remote-debugging-port=${CDP_PORT}`,
863
+ `--user-data-dir=${userDataDir}`,
864
+ '--no-first-run',
865
+ '--no-default-browser-check',
866
+ '--disable-extensions',
867
+ ];
868
+
869
+ if (!HEADFUL) {
870
+ browserArgs.push('--headless=new', '--disable-gpu');
871
+ }
872
+
873
+ browserArgs.push('about:blank');
874
+
875
+ browserProcess = spawn(chromiumPath, browserArgs, {
876
+ stdio: HEADFUL ? 'inherit' : 'pipe',
877
+ detached: false,
878
+ });
879
+
880
+ const wsEndpoint = await waitForCdp(CDP_PORT);
881
+ const browser = await chromium.connectOverCDP(wsEndpoint);
882
+ const context = browser.contexts()[0];
883
+ const page = context.pages()[0] || await context.newPage();
884
+
885
+ await page.goto(testOrigin);
886
+ await visualDelay(300);
887
+
888
+ // Register credentials via CLI
889
+ const output = execSync(
890
+ `node ./dist/index.js register ` +
891
+ `--cdp "${wsEndpoint}" ` +
892
+ `--username-selector "#email" ` +
893
+ `--password-selector "#password" ` +
894
+ `--username "cli-localhost-test@example.com" ` +
895
+ `--password "LocalhostPass123!" ` +
896
+ `--allow-http ` +
897
+ `--force`,
898
+ { encoding: 'utf-8', cwd: process.cwd() }
899
+ );
900
+
901
+ expect(output).toContain('Credentials registered successfully');
902
+
903
+ console.log('\nāœ… CLI works with localhost');
904
+ console.log(' Security is provided by:');
905
+ console.log(' - OS keychain encryption (credentials never in browser)');
906
+ console.log(' - CDP connection validation');
907
+ console.log(' - Rate limiting');
908
+
909
+ await browser.close();
910
+
911
+ // Clean up
912
+ const { deleteRP } = await import('../src/core/keychain.js');
913
+ await deleteRP(testOrigin);
914
+
915
+ } finally {
916
+ cleanupUserDataDir(userDataDir);
917
+ }
918
+ });
919
+
920
+ it('demonstrates keychain-based credential flow (using direct APIs)', async () => {
921
+ // Reset rate limit before this test
922
+ await resetRateLimit();
923
+
924
+ // Demonstrate the keychain approach by directly using the keychain APIs
925
+ const testOrigin = 'https://example.com';
926
+
927
+ const { storeRP, getRP, deleteRP } = await import('../src/core/keychain.js');
928
+
929
+ // Store credentials in OS keychain
930
+ await storeRP({
931
+ origin: testOrigin,
932
+ selectors: {
933
+ username: '#email',
934
+ password: '#password',
935
+ },
936
+ credentials: {
937
+ username: 'keychain-demo@example.com',
938
+ password: 'KeychainDemo123!',
939
+ },
940
+ });
941
+
942
+ console.log('\nšŸ“ Stored credentials in OS keychain for', testOrigin);
943
+
944
+ // Retrieve from keychain
945
+ const stored = await getRP(testOrigin);
946
+
947
+ expect(stored).not.toBeNull();
948
+ expect(stored?.credentials.username).toBe('keychain-demo@example.com');
949
+ expect(stored?.credentials.password).toBe('KeychainDemo123!');
950
+
951
+ console.log('āœ… Retrieved credentials from OS keychain');
952
+ console.log(' Key advantages of keychain approach:');
953
+ console.log(' - Credentials encrypted at OS level');
954
+ console.log(' - Never exposed to browser JavaScript');
955
+ console.log(' - Persists independently of browser profile');
956
+ console.log(' - Works with any browser via CDP');
957
+
958
+ // Clean up
959
+ await deleteRP(testOrigin);
960
+ });
961
+
962
+ it('documents the key differences between approaches', () => {
963
+ const comparison = {
964
+ cliKeychain: {
965
+ pros: [
966
+ 'OS-level security (macOS Keychain, Windows Credential Manager)',
967
+ 'Credentials never exposed to browser context',
968
+ 'Works across all browser sessions',
969
+ 'Independent of browser state',
970
+ ],
971
+ cons: [
972
+ 'Requires keytar native module',
973
+ 'Platform-specific implementation',
974
+ 'Separate storage from browser',
975
+ ],
976
+ },
977
+ browserPasswordManager: {
978
+ pros: [
979
+ 'Built into browser',
980
+ 'Familiar UX for users',
981
+ 'Syncs with Chrome account (if enabled)',
982
+ ],
983
+ cons: [
984
+ 'Limited API access in headless mode',
985
+ 'Requires user interaction for prompts',
986
+ 'Credentials accessible to page JavaScript',
987
+ 'Depends on browser profile integrity',
988
+ ],
989
+ },
990
+ browserLocalStorage: {
991
+ pros: [
992
+ 'Simple API',
993
+ 'Persists with --user-data-dir',
994
+ 'Easy to implement',
995
+ ],
996
+ cons: [
997
+ 'NOT secure - accessible to any JS on origin',
998
+ 'No encryption by default',
999
+ 'Lost if profile cleared',
1000
+ ],
1001
+ },
1002
+ browserIndexedDB: {
1003
+ pros: [
1004
+ 'Structured data storage',
1005
+ 'Persists with --user-data-dir',
1006
+ 'Good for complex data',
1007
+ ],
1008
+ cons: [
1009
+ 'Still accessible to page JS',
1010
+ 'No built-in encryption',
1011
+ 'More complex API',
1012
+ ],
1013
+ },
1014
+ };
1015
+
1016
+ console.log('\nšŸ“Š Storage Comparison:\n');
1017
+ console.log(JSON.stringify(comparison, null, 2));
1018
+
1019
+ // This test just documents findings
1020
+ expect(comparison).toBeDefined();
1021
+ });
1022
+ });
1023
+ });