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.
- package/.cursor/skills/npm-publish/SKILL.md +58 -0
- package/.github/workflows/ci.yml +67 -0
- package/README.md +164 -0
- package/ROADMAP.md +986 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +67 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/delete.d.ts +7 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +30 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +37 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/register.d.ts +13 -0
- package/dist/commands/register.d.ts.map +1 -0
- package/dist/commands/register.js +160 -0
- package/dist/commands/register.js.map +1 -0
- package/dist/core/audit.d.ts +15 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +36 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/browser.d.ts +7 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +104 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +80 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/crypto.d.ts +17 -0
- package/dist/core/crypto.d.ts.map +1 -0
- package/dist/core/crypto.js +90 -0
- package/dist/core/crypto.js.map +1 -0
- package/dist/core/fields.d.ts +5 -0
- package/dist/core/fields.d.ts.map +1 -0
- package/dist/core/fields.js +54 -0
- package/dist/core/fields.js.map +1 -0
- package/dist/core/keychain.d.ts +5 -0
- package/dist/core/keychain.d.ts.map +1 -0
- package/dist/core/keychain.js +97 -0
- package/dist/core/keychain.js.map +1 -0
- package/dist/core/origin.d.ts +25 -0
- package/dist/core/origin.d.ts.map +1 -0
- package/dist/core/origin.js +73 -0
- package/dist/core/origin.js.map +1 -0
- package/dist/core/ratelimit.d.ts +10 -0
- package/dist/core/ratelimit.d.ts.map +1 -0
- package/dist/core/ratelimit.js +70 -0
- package/dist/core/ratelimit.js.map +1 -0
- package/dist/core/secure-memory.d.ts +39 -0
- package/dist/core/secure-memory.d.ts.map +1 -0
- package/dist/core/secure-memory.js +68 -0
- package/dist/core/secure-memory.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +58 -0
- package/src/commands/config.ts +84 -0
- package/src/commands/delete.ts +39 -0
- package/src/commands/login.ts +49 -0
- package/src/commands/register.ts +188 -0
- package/src/core/audit.ts +59 -0
- package/src/core/browser.ts +131 -0
- package/src/core/config.ts +91 -0
- package/src/core/crypto.ts +106 -0
- package/src/core/fields.ts +59 -0
- package/src/core/keychain.ts +110 -0
- package/src/core/origin.ts +90 -0
- package/src/core/ratelimit.ts +89 -0
- package/src/core/secure-memory.ts +78 -0
- package/src/index.ts +133 -0
- package/src/types/index.ts +31 -0
- package/tests/browser-password-manager.test.ts +1023 -0
- package/tests/crypto.test.ts +140 -0
- package/tests/e2e.test.ts +565 -0
- package/tests/fixtures/server.ts +59 -0
- package/tests/security.test.ts +113 -0
- package/tsconfig.json +20 -0
- 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
|
+
});
|