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,140 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generatePassword, generatePasswordNanoid } from '../src/core/crypto.js';
|
|
3
|
+
|
|
4
|
+
describe('generatePassword', () => {
|
|
5
|
+
it('generates password of default length (24)', () => {
|
|
6
|
+
const password = generatePassword();
|
|
7
|
+
expect(password).toHaveLength(24);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('generates password of specified length', () => {
|
|
11
|
+
const password = generatePassword(32);
|
|
12
|
+
expect(password).toHaveLength(32);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('throws error for length below minimum (12)', () => {
|
|
16
|
+
expect(() => generatePassword(8)).toThrow('Password length must be at least 12 characters');
|
|
17
|
+
expect(() => generatePassword(11)).toThrow('Password length must be at least 12 characters');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('accepts minimum length of 12', () => {
|
|
21
|
+
const password = generatePassword(12);
|
|
22
|
+
expect(password).toHaveLength(12);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('contains at least one lowercase letter', () => {
|
|
26
|
+
for (let i = 0; i < 10; i++) {
|
|
27
|
+
const password = generatePassword();
|
|
28
|
+
expect(password).toMatch(/[a-z]/);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('contains at least one uppercase letter', () => {
|
|
33
|
+
for (let i = 0; i < 10; i++) {
|
|
34
|
+
const password = generatePassword();
|
|
35
|
+
expect(password).toMatch(/[A-Z]/);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('contains at least one digit', () => {
|
|
40
|
+
for (let i = 0; i < 10; i++) {
|
|
41
|
+
const password = generatePassword();
|
|
42
|
+
expect(password).toMatch(/[0-9]/);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('contains at least one special character', () => {
|
|
47
|
+
for (let i = 0; i < 10; i++) {
|
|
48
|
+
const password = generatePassword();
|
|
49
|
+
expect(password).toMatch(/[!@#$%^&*]/);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('only contains allowed characters', () => {
|
|
54
|
+
const allowedChars = /^[a-zA-Z0-9!@#$%^&*]+$/;
|
|
55
|
+
for (let i = 0; i < 10; i++) {
|
|
56
|
+
const password = generatePassword();
|
|
57
|
+
expect(password).toMatch(allowedChars);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('generates unique passwords', () => {
|
|
62
|
+
const passwords = new Set<string>();
|
|
63
|
+
for (let i = 0; i < 100; i++) {
|
|
64
|
+
passwords.add(generatePassword());
|
|
65
|
+
}
|
|
66
|
+
// All 100 passwords should be unique
|
|
67
|
+
expect(passwords.size).toBe(100);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('generatePasswordNanoid', () => {
|
|
72
|
+
it('generates password of default length (24)', () => {
|
|
73
|
+
const password = generatePasswordNanoid();
|
|
74
|
+
expect(password).toHaveLength(24);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('generates password of specified length', () => {
|
|
78
|
+
const password = generatePasswordNanoid(32);
|
|
79
|
+
expect(password).toHaveLength(32);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('throws error for length below minimum (12)', () => {
|
|
83
|
+
expect(() => generatePasswordNanoid(8)).toThrow(
|
|
84
|
+
'Password length must be at least 12 characters'
|
|
85
|
+
);
|
|
86
|
+
expect(() => generatePasswordNanoid(11)).toThrow(
|
|
87
|
+
'Password length must be at least 12 characters'
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('accepts minimum length of 12', () => {
|
|
92
|
+
const password = generatePasswordNanoid(12);
|
|
93
|
+
expect(password).toHaveLength(12);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('contains at least one lowercase letter', () => {
|
|
97
|
+
for (let i = 0; i < 10; i++) {
|
|
98
|
+
const password = generatePasswordNanoid();
|
|
99
|
+
expect(password).toMatch(/[a-z]/);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('contains at least one uppercase letter', () => {
|
|
104
|
+
for (let i = 0; i < 10; i++) {
|
|
105
|
+
const password = generatePasswordNanoid();
|
|
106
|
+
expect(password).toMatch(/[A-Z]/);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('contains at least one digit', () => {
|
|
111
|
+
for (let i = 0; i < 10; i++) {
|
|
112
|
+
const password = generatePasswordNanoid();
|
|
113
|
+
expect(password).toMatch(/[0-9]/);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('contains at least one special character', () => {
|
|
118
|
+
for (let i = 0; i < 10; i++) {
|
|
119
|
+
const password = generatePasswordNanoid();
|
|
120
|
+
expect(password).toMatch(/[!@#$%^&*]/);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('only contains allowed characters', () => {
|
|
125
|
+
const allowedChars = /^[a-zA-Z0-9!@#$%^&*]+$/;
|
|
126
|
+
for (let i = 0; i < 10; i++) {
|
|
127
|
+
const password = generatePasswordNanoid();
|
|
128
|
+
expect(password).toMatch(allowedChars);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('generates unique passwords', () => {
|
|
133
|
+
const passwords = new Set<string>();
|
|
134
|
+
for (let i = 0; i < 100; i++) {
|
|
135
|
+
passwords.add(generatePasswordNanoid());
|
|
136
|
+
}
|
|
137
|
+
// All 100 passwords should be unique
|
|
138
|
+
expect(passwords.size).toBe(100);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
2
|
+
import { chromium, Browser } from 'playwright';
|
|
3
|
+
import { execSync, spawn, ChildProcess } from 'child_process';
|
|
4
|
+
import { startTestServer, TestServer } from './fixtures/server.js';
|
|
5
|
+
import { deleteRP, getRP } from '../src/core/keychain.js';
|
|
6
|
+
import { loadConfig, saveConfig } from '../src/core/config.js';
|
|
7
|
+
import { resetRateLimit } from '../src/core/ratelimit.js';
|
|
8
|
+
import http from 'http';
|
|
9
|
+
|
|
10
|
+
const CLI_PATH = './dist/index.js';
|
|
11
|
+
|
|
12
|
+
// Ports for different "origins"
|
|
13
|
+
const TEST_PORTS = [9501, 9502, 9503];
|
|
14
|
+
const CDP_PORT = 9333;
|
|
15
|
+
|
|
16
|
+
// Run with HEADFUL=1 npm test to see the browser
|
|
17
|
+
const HEADFUL = process.env.HEADFUL === '1' || process.env.HEADFUL === 'true';
|
|
18
|
+
const SLOW_MO = HEADFUL ? 500 : 0; // Slow down actions in headful mode
|
|
19
|
+
|
|
20
|
+
// Helper to add delay in headful mode so you can see what's happening
|
|
21
|
+
async function visualDelay(ms: number = 300): Promise<void> {
|
|
22
|
+
if (HEADFUL) {
|
|
23
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getCdpEndpoint(port: number): Promise<string> {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const req = http.get(`http://127.0.0.1:${port}/json/version`, (res) => {
|
|
30
|
+
let data = '';
|
|
31
|
+
res.on('data', (chunk) => (data += chunk));
|
|
32
|
+
res.on('end', () => {
|
|
33
|
+
try {
|
|
34
|
+
const json = JSON.parse(data);
|
|
35
|
+
resolve(json.webSocketDebuggerUrl);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
reject(e);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
req.setTimeout(5000, () => {
|
|
43
|
+
req.destroy();
|
|
44
|
+
reject(new Error('Timeout getting CDP endpoint'));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function waitForCdp(port: number, maxRetries = 30): Promise<string> {
|
|
50
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
51
|
+
try {
|
|
52
|
+
return await getCdpEndpoint(port);
|
|
53
|
+
} catch {
|
|
54
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Could not connect to CDP on port ${port}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('E2E: Vault CLI', () => {
|
|
61
|
+
let servers: TestServer[] = [];
|
|
62
|
+
let browser: Browser;
|
|
63
|
+
let browserProcess: ChildProcess;
|
|
64
|
+
let wsEndpoint: string;
|
|
65
|
+
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
// Reset rate limiter before tests
|
|
68
|
+
await resetRateLimit();
|
|
69
|
+
|
|
70
|
+
// Start test servers on multiple ports
|
|
71
|
+
for (const port of TEST_PORTS) {
|
|
72
|
+
const server = await startTestServer(port);
|
|
73
|
+
servers.push(server);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Get the Chromium executable path from Playwright
|
|
77
|
+
const chromiumPath = chromium.executablePath();
|
|
78
|
+
|
|
79
|
+
// Build browser args
|
|
80
|
+
const browserArgs = [
|
|
81
|
+
`--remote-debugging-port=${CDP_PORT}`,
|
|
82
|
+
'--no-first-run',
|
|
83
|
+
'--no-default-browser-check',
|
|
84
|
+
'--disable-extensions',
|
|
85
|
+
'--disable-dev-shm-usage',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Add headless flag only if not in headful mode
|
|
89
|
+
if (!HEADFUL) {
|
|
90
|
+
browserArgs.push('--headless=new', '--disable-gpu');
|
|
91
|
+
} else {
|
|
92
|
+
// In headful mode, set a reasonable window size
|
|
93
|
+
browserArgs.push('--window-size=1280,800', '--window-position=100,100');
|
|
94
|
+
console.log('\n🖥️ Running in HEADFUL mode - watch the browser!\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
browserArgs.push('about:blank');
|
|
98
|
+
|
|
99
|
+
// Launch Chromium with remote debugging enabled
|
|
100
|
+
browserProcess = spawn(chromiumPath, browserArgs, {
|
|
101
|
+
stdio: HEADFUL ? 'inherit' : 'pipe',
|
|
102
|
+
detached: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Wait for CDP to be available and get the endpoint
|
|
106
|
+
wsEndpoint = await waitForCdp(CDP_PORT);
|
|
107
|
+
|
|
108
|
+
// Connect to the browser using CDP (same as CLI does)
|
|
109
|
+
browser = await chromium.connectOverCDP(wsEndpoint);
|
|
110
|
+
}, 60000);
|
|
111
|
+
|
|
112
|
+
afterAll(async () => {
|
|
113
|
+
// Close browser
|
|
114
|
+
if (browser) {
|
|
115
|
+
try {
|
|
116
|
+
await browser.close();
|
|
117
|
+
} catch {
|
|
118
|
+
// Ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Kill browser process
|
|
123
|
+
if (browserProcess) {
|
|
124
|
+
browserProcess.kill('SIGTERM');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Stop all test servers
|
|
128
|
+
for (const server of servers) {
|
|
129
|
+
await server.close();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Clean up any test credentials
|
|
133
|
+
for (const port of TEST_PORTS) {
|
|
134
|
+
try {
|
|
135
|
+
await deleteRP(`http://127.0.0.1:${port}`);
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore errors during cleanup
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('register command', () => {
|
|
143
|
+
const testPort = TEST_PORTS[0];
|
|
144
|
+
const testOrigin = `http://127.0.0.1:${testPort}`;
|
|
145
|
+
|
|
146
|
+
beforeEach(async () => {
|
|
147
|
+
// Reset rate limiter before each test
|
|
148
|
+
await resetRateLimit();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
afterEach(async () => {
|
|
152
|
+
// Clean up credentials after each test
|
|
153
|
+
try {
|
|
154
|
+
await deleteRP(testOrigin);
|
|
155
|
+
} catch {
|
|
156
|
+
// Ignore
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('registers credentials for a site', async () => {
|
|
161
|
+
const context = browser.contexts()[0];
|
|
162
|
+
const page = context.pages()[0] || await context.newPage();
|
|
163
|
+
await page.goto(testOrigin);
|
|
164
|
+
await visualDelay(500);
|
|
165
|
+
|
|
166
|
+
// Run the CLI register command
|
|
167
|
+
const output = execSync(
|
|
168
|
+
`node ${CLI_PATH} register ` +
|
|
169
|
+
`--cdp "${wsEndpoint}" ` +
|
|
170
|
+
`--username-selector "#email" ` +
|
|
171
|
+
`--password-selector "#password" ` +
|
|
172
|
+
`--username "test@example.com" ` +
|
|
173
|
+
`--password "TestPassword123!" ` +
|
|
174
|
+
`--allow-http ` +
|
|
175
|
+
`--force`,
|
|
176
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await visualDelay(800); // Show the filled form
|
|
180
|
+
|
|
181
|
+
expect(output).toContain('Credentials registered successfully');
|
|
182
|
+
|
|
183
|
+
// Verify credentials were stored
|
|
184
|
+
const stored = await getRP(testOrigin);
|
|
185
|
+
expect(stored).not.toBeNull();
|
|
186
|
+
expect(stored?.credentials.username).toBe('test@example.com');
|
|
187
|
+
expect(stored?.credentials.password).toBe('TestPassword123!');
|
|
188
|
+
expect(stored?.selectors.username).toBe('#email');
|
|
189
|
+
expect(stored?.selectors.password).toBe('#password');
|
|
190
|
+
|
|
191
|
+
// Verify form was filled
|
|
192
|
+
const emailValue = await page.inputValue('#email');
|
|
193
|
+
const passwordValue = await page.inputValue('#password');
|
|
194
|
+
expect(emailValue).toBe('test@example.com');
|
|
195
|
+
expect(passwordValue).toBe('TestPassword123!');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('generates a secure password when --generate-password is used', async () => {
|
|
199
|
+
const context = browser.contexts()[0];
|
|
200
|
+
const page = context.pages()[0] || await context.newPage();
|
|
201
|
+
await page.goto(testOrigin);
|
|
202
|
+
await visualDelay(500);
|
|
203
|
+
|
|
204
|
+
const output = execSync(
|
|
205
|
+
`node ${CLI_PATH} register ` +
|
|
206
|
+
`--cdp "${wsEndpoint}" ` +
|
|
207
|
+
`--username-selector "#email" ` +
|
|
208
|
+
`--password-selector "#password" ` +
|
|
209
|
+
`--username "generated@example.com" ` +
|
|
210
|
+
`--generate-password ` +
|
|
211
|
+
`--allow-http ` +
|
|
212
|
+
`--force`,
|
|
213
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await visualDelay(800);
|
|
217
|
+
|
|
218
|
+
expect(output).toContain('Secure password generated');
|
|
219
|
+
expect(output).toContain('Credentials registered successfully');
|
|
220
|
+
|
|
221
|
+
// Verify credentials were stored with a generated password
|
|
222
|
+
const stored = await getRP(testOrigin);
|
|
223
|
+
expect(stored).not.toBeNull();
|
|
224
|
+
expect(stored?.credentials.username).toBe('generated@example.com');
|
|
225
|
+
expect(stored?.credentials.password.length).toBeGreaterThanOrEqual(16);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('fails with invalid selector', async () => {
|
|
229
|
+
const context = browser.contexts()[0];
|
|
230
|
+
const page = context.pages()[0] || await context.newPage();
|
|
231
|
+
await page.goto(testOrigin);
|
|
232
|
+
|
|
233
|
+
expect(() => {
|
|
234
|
+
execSync(
|
|
235
|
+
`node ${CLI_PATH} register ` +
|
|
236
|
+
`--cdp "${wsEndpoint}" ` +
|
|
237
|
+
`--username-selector "#nonexistent" ` +
|
|
238
|
+
`--password-selector "#password" ` +
|
|
239
|
+
`--username "test@example.com" ` +
|
|
240
|
+
`--password "TestPassword123!" ` +
|
|
241
|
+
`--allow-http ` +
|
|
242
|
+
`--force`,
|
|
243
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
244
|
+
);
|
|
245
|
+
}).toThrow(/Username.*not found/);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('login command', () => {
|
|
250
|
+
const testPort = TEST_PORTS[1];
|
|
251
|
+
const testOrigin = `http://127.0.0.1:${testPort}`;
|
|
252
|
+
|
|
253
|
+
beforeEach(async () => {
|
|
254
|
+
await resetRateLimit();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
afterEach(async () => {
|
|
258
|
+
try {
|
|
259
|
+
await deleteRP(testOrigin);
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('fills credentials for a registered site', async () => {
|
|
266
|
+
// First, register credentials
|
|
267
|
+
const context = browser.contexts()[0];
|
|
268
|
+
const page = context.pages()[0] || await context.newPage();
|
|
269
|
+
await page.goto(testOrigin);
|
|
270
|
+
await visualDelay(500);
|
|
271
|
+
|
|
272
|
+
execSync(
|
|
273
|
+
`node ${CLI_PATH} register ` +
|
|
274
|
+
`--cdp "${wsEndpoint}" ` +
|
|
275
|
+
`--username-selector "#email" ` +
|
|
276
|
+
`--password-selector "#password" ` +
|
|
277
|
+
`--submit-selector "#submit-btn" ` +
|
|
278
|
+
`--username "login@example.com" ` +
|
|
279
|
+
`--password "LoginPass456!" ` +
|
|
280
|
+
`--allow-http ` +
|
|
281
|
+
`--force`,
|
|
282
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
await visualDelay(600);
|
|
286
|
+
|
|
287
|
+
// Navigate to the page again to clear the form
|
|
288
|
+
await page.goto(testOrigin);
|
|
289
|
+
await visualDelay(500);
|
|
290
|
+
|
|
291
|
+
// Verify form is empty
|
|
292
|
+
expect(await page.inputValue('#email')).toBe('');
|
|
293
|
+
expect(await page.inputValue('#password')).toBe('');
|
|
294
|
+
|
|
295
|
+
// Run login command
|
|
296
|
+
const output = execSync(
|
|
297
|
+
`node ${CLI_PATH} login --cdp "${wsEndpoint}"`,
|
|
298
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
await visualDelay(800); // Show the auto-filled form
|
|
302
|
+
|
|
303
|
+
expect(output).toContain('Login filled successfully');
|
|
304
|
+
|
|
305
|
+
// Verify form was filled with stored credentials
|
|
306
|
+
const emailValue = await page.inputValue('#email');
|
|
307
|
+
const passwordValue = await page.inputValue('#password');
|
|
308
|
+
expect(emailValue).toBe('login@example.com');
|
|
309
|
+
expect(passwordValue).toBe('LoginPass456!');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('fails for unknown origin', async () => {
|
|
313
|
+
// Use a different port that has no registered credentials
|
|
314
|
+
const unknownPort = TEST_PORTS[2];
|
|
315
|
+
const context = browser.contexts()[0];
|
|
316
|
+
const page = context.pages()[0] || await context.newPage();
|
|
317
|
+
await page.goto(`http://127.0.0.1:${unknownPort}`);
|
|
318
|
+
|
|
319
|
+
// Clean up any existing credentials for this origin
|
|
320
|
+
try {
|
|
321
|
+
await deleteRP(`http://127.0.0.1:${unknownPort}`);
|
|
322
|
+
} catch {
|
|
323
|
+
// Ignore
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
expect(() => {
|
|
327
|
+
execSync(
|
|
328
|
+
`node ${CLI_PATH} login --cdp "${wsEndpoint}"`,
|
|
329
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
330
|
+
);
|
|
331
|
+
}).toThrow(/No credentials found/);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('delete command', () => {
|
|
336
|
+
const testPort = TEST_PORTS[2];
|
|
337
|
+
const testOrigin = `http://127.0.0.1:${testPort}`;
|
|
338
|
+
|
|
339
|
+
beforeEach(async () => {
|
|
340
|
+
await resetRateLimit();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('deletes credentials for a site', async () => {
|
|
344
|
+
// First, register credentials
|
|
345
|
+
const context = browser.contexts()[0];
|
|
346
|
+
const page = context.pages()[0] || await context.newPage();
|
|
347
|
+
await page.goto(testOrigin);
|
|
348
|
+
|
|
349
|
+
execSync(
|
|
350
|
+
`node ${CLI_PATH} register ` +
|
|
351
|
+
`--cdp "${wsEndpoint}" ` +
|
|
352
|
+
`--username-selector "#email" ` +
|
|
353
|
+
`--password-selector "#password" ` +
|
|
354
|
+
`--username "delete@example.com" ` +
|
|
355
|
+
`--password "DeletePass789!" ` +
|
|
356
|
+
`--allow-http ` +
|
|
357
|
+
`--force`,
|
|
358
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Verify credentials exist
|
|
362
|
+
let stored = await getRP(testOrigin);
|
|
363
|
+
expect(stored).not.toBeNull();
|
|
364
|
+
|
|
365
|
+
// Run delete command
|
|
366
|
+
const output = execSync(
|
|
367
|
+
`node ${CLI_PATH} delete --origin "${testOrigin}" --force`,
|
|
368
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
expect(output).toContain('Deleted credentials for');
|
|
372
|
+
|
|
373
|
+
// Verify credentials were deleted
|
|
374
|
+
stored = await getRP(testOrigin);
|
|
375
|
+
expect(stored).toBeNull();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('fails for non-existent origin', async () => {
|
|
379
|
+
expect(() => {
|
|
380
|
+
execSync(
|
|
381
|
+
`node ${CLI_PATH} delete --origin "http://nonexistent.example.com" --force`,
|
|
382
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
383
|
+
);
|
|
384
|
+
}).toThrow(/No credentials found/);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('multiple origins', () => {
|
|
389
|
+
beforeEach(async () => {
|
|
390
|
+
await resetRateLimit();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
afterEach(async () => {
|
|
394
|
+
// Clean up all test credentials
|
|
395
|
+
for (const port of TEST_PORTS) {
|
|
396
|
+
try {
|
|
397
|
+
await deleteRP(`http://127.0.0.1:${port}`);
|
|
398
|
+
} catch {
|
|
399
|
+
// Ignore
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('handles credentials for different origins independently', async () => {
|
|
405
|
+
const credentials = [
|
|
406
|
+
{ port: TEST_PORTS[0], username: 'user1@site1.com', password: 'Pass1!' },
|
|
407
|
+
{ port: TEST_PORTS[1], username: 'user2@site2.com', password: 'Pass2!' },
|
|
408
|
+
];
|
|
409
|
+
|
|
410
|
+
const context = browser.contexts()[0];
|
|
411
|
+
const page = context.pages()[0] || await context.newPage();
|
|
412
|
+
|
|
413
|
+
// Register credentials for multiple sites
|
|
414
|
+
for (const cred of credentials) {
|
|
415
|
+
await resetRateLimit(); // Reset rate limit for each registration
|
|
416
|
+
await page.goto(`http://127.0.0.1:${cred.port}`);
|
|
417
|
+
await visualDelay(400);
|
|
418
|
+
|
|
419
|
+
execSync(
|
|
420
|
+
`node ${CLI_PATH} register ` +
|
|
421
|
+
`--cdp "${wsEndpoint}" ` +
|
|
422
|
+
`--username-selector "#email" ` +
|
|
423
|
+
`--password-selector "#password" ` +
|
|
424
|
+
`--username "${cred.username}" ` +
|
|
425
|
+
`--password "${cred.password}" ` +
|
|
426
|
+
`--allow-http ` +
|
|
427
|
+
`--force`,
|
|
428
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
await visualDelay(600);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Verify each origin has correct credentials
|
|
435
|
+
for (const cred of credentials) {
|
|
436
|
+
const origin = `http://127.0.0.1:${cred.port}`;
|
|
437
|
+
const stored = await getRP(origin);
|
|
438
|
+
expect(stored).not.toBeNull();
|
|
439
|
+
expect(stored?.credentials.username).toBe(cred.username);
|
|
440
|
+
expect(stored?.credentials.password).toBe(cred.password);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Test that login fills the correct credentials for each origin
|
|
444
|
+
for (const cred of credentials) {
|
|
445
|
+
await resetRateLimit(); // Reset rate limit for each login
|
|
446
|
+
await page.goto(`http://127.0.0.1:${cred.port}`);
|
|
447
|
+
await visualDelay(400);
|
|
448
|
+
|
|
449
|
+
// Verify form is empty
|
|
450
|
+
expect(await page.inputValue('#email')).toBe('');
|
|
451
|
+
|
|
452
|
+
execSync(
|
|
453
|
+
`node ${CLI_PATH} login --cdp "${wsEndpoint}"`,
|
|
454
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
await visualDelay(800); // Watch the credentials get filled
|
|
458
|
+
|
|
459
|
+
// Verify correct credentials were filled
|
|
460
|
+
expect(await page.inputValue('#email')).toBe(cred.username);
|
|
461
|
+
expect(await page.inputValue('#password')).toBe(cred.password);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('config command', () => {
|
|
467
|
+
let originalConfig: Awaited<ReturnType<typeof loadConfig>>;
|
|
468
|
+
|
|
469
|
+
beforeAll(async () => {
|
|
470
|
+
// Save original config to restore after tests
|
|
471
|
+
originalConfig = await loadConfig();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
afterAll(async () => {
|
|
475
|
+
// Restore original config
|
|
476
|
+
await saveConfig(originalConfig);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
afterEach(async () => {
|
|
480
|
+
// Reset config between tests
|
|
481
|
+
await saveConfig({});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('sets and gets a config value', () => {
|
|
485
|
+
// Set a value
|
|
486
|
+
const setOutput = execSync(
|
|
487
|
+
`node ${CLI_PATH} config set defaultUsername config-test@example.com`,
|
|
488
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
489
|
+
);
|
|
490
|
+
expect(setOutput).toContain('Set defaultUsername=config-test@example.com');
|
|
491
|
+
|
|
492
|
+
// Get the value
|
|
493
|
+
const getOutput = execSync(
|
|
494
|
+
`node ${CLI_PATH} config get defaultUsername`,
|
|
495
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
496
|
+
);
|
|
497
|
+
expect(getOutput.trim()).toBe('config-test@example.com');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('lists config values', async () => {
|
|
501
|
+
// Set a value first
|
|
502
|
+
execSync(
|
|
503
|
+
`node ${CLI_PATH} config set defaultUsername list-test@example.com`,
|
|
504
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// List config
|
|
508
|
+
const output = execSync(
|
|
509
|
+
`node ${CLI_PATH} config list`,
|
|
510
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
511
|
+
);
|
|
512
|
+
expect(output).toContain('defaultUsername=list-test@example.com');
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('shows empty message when no config is set', async () => {
|
|
516
|
+
// Ensure config is empty
|
|
517
|
+
await saveConfig({});
|
|
518
|
+
|
|
519
|
+
const output = execSync(
|
|
520
|
+
`node ${CLI_PATH} config list`,
|
|
521
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
522
|
+
);
|
|
523
|
+
expect(output).toContain('No configuration values set');
|
|
524
|
+
expect(output).toContain('Available keys: defaultUsername');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('unsets a config value', () => {
|
|
528
|
+
// Set a value first
|
|
529
|
+
execSync(
|
|
530
|
+
`node ${CLI_PATH} config set defaultUsername unset-test@example.com`,
|
|
531
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
// Unset it
|
|
535
|
+
const unsetOutput = execSync(
|
|
536
|
+
`node ${CLI_PATH} config unset defaultUsername`,
|
|
537
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
538
|
+
);
|
|
539
|
+
expect(unsetOutput).toContain('Unset defaultUsername');
|
|
540
|
+
|
|
541
|
+
// Verify it's gone
|
|
542
|
+
const getOutput = execSync(
|
|
543
|
+
`node ${CLI_PATH} config get defaultUsername`,
|
|
544
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
545
|
+
);
|
|
546
|
+
expect(getOutput.trim()).toBe('(not set)');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('rejects invalid config keys', () => {
|
|
550
|
+
expect(() => {
|
|
551
|
+
execSync(
|
|
552
|
+
`node ${CLI_PATH} config set invalidKey someValue`,
|
|
553
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
554
|
+
);
|
|
555
|
+
}).toThrow(/Invalid config key/);
|
|
556
|
+
|
|
557
|
+
expect(() => {
|
|
558
|
+
execSync(
|
|
559
|
+
`node ${CLI_PATH} config get invalidKey`,
|
|
560
|
+
{ encoding: 'utf-8', cwd: process.cwd() }
|
|
561
|
+
);
|
|
562
|
+
}).toThrow(/Invalid config key/);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
});
|