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,89 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { logAuditEvent } from './audit.js';
|
|
5
|
+
|
|
6
|
+
const RATE_LIMIT_DIR = join(homedir(), '.agent-vault');
|
|
7
|
+
const RATE_LIMIT_FILE = join(RATE_LIMIT_DIR, '.ratelimit');
|
|
8
|
+
|
|
9
|
+
// Rate limit configuration
|
|
10
|
+
const MAX_ATTEMPTS = 5;
|
|
11
|
+
const WINDOW_MS = 60 * 1000; // 1 minute window
|
|
12
|
+
const LOCKOUT_MS = 5 * 60 * 1000; // 5 minute lockout after exceeding
|
|
13
|
+
|
|
14
|
+
interface RateLimitState {
|
|
15
|
+
attempts: number[];
|
|
16
|
+
lockedUntil?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function ensureRateLimitDir(): Promise<void> {
|
|
20
|
+
await mkdir(RATE_LIMIT_DIR, { recursive: true, mode: 0o700 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function loadRateLimitState(): Promise<RateLimitState> {
|
|
24
|
+
try {
|
|
25
|
+
const data = await readFile(RATE_LIMIT_FILE, 'utf-8');
|
|
26
|
+
return JSON.parse(data);
|
|
27
|
+
} catch {
|
|
28
|
+
return { attempts: [] };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function saveRateLimitState(state: RateLimitState): Promise<void> {
|
|
33
|
+
await ensureRateLimitDir();
|
|
34
|
+
await writeFile(RATE_LIMIT_FILE, JSON.stringify(state), { mode: 0o600 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if an operation is rate limited.
|
|
39
|
+
* Returns true if allowed, throws if rate limited.
|
|
40
|
+
*/
|
|
41
|
+
export async function checkRateLimit(operation: string): Promise<void> {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const state = await loadRateLimitState();
|
|
44
|
+
|
|
45
|
+
// Check if currently locked out
|
|
46
|
+
if (state.lockedUntil && now < state.lockedUntil) {
|
|
47
|
+
const remainingSeconds = Math.ceil((state.lockedUntil - now) / 1000);
|
|
48
|
+
await logAuditEvent('rate_limit_exceeded', {
|
|
49
|
+
details: `Operation: ${operation}, locked for ${remainingSeconds}s`,
|
|
50
|
+
success: false,
|
|
51
|
+
});
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Rate limit exceeded. Please wait ${remainingSeconds} seconds before trying again.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Clear lockout if expired
|
|
58
|
+
if (state.lockedUntil && now >= state.lockedUntil) {
|
|
59
|
+
state.lockedUntil = undefined;
|
|
60
|
+
state.attempts = [];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Filter attempts within the window
|
|
64
|
+
state.attempts = state.attempts.filter((ts) => now - ts < WINDOW_MS);
|
|
65
|
+
|
|
66
|
+
// Check if exceeding limit
|
|
67
|
+
if (state.attempts.length >= MAX_ATTEMPTS) {
|
|
68
|
+
state.lockedUntil = now + LOCKOUT_MS;
|
|
69
|
+
await saveRateLimitState(state);
|
|
70
|
+
await logAuditEvent('rate_limit_exceeded', {
|
|
71
|
+
details: `Operation: ${operation}, lockout initiated`,
|
|
72
|
+
success: false,
|
|
73
|
+
});
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Too many attempts. Please wait ${Math.ceil(LOCKOUT_MS / 1000)} seconds before trying again.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Record this attempt
|
|
80
|
+
state.attempts.push(now);
|
|
81
|
+
await saveRateLimitState(state);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reset rate limit state (for testing or admin purposes)
|
|
86
|
+
*/
|
|
87
|
+
export async function resetRateLimit(): Promise<void> {
|
|
88
|
+
await saveRateLimitState({ attempts: [] });
|
|
89
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secure memory utilities for handling sensitive data.
|
|
3
|
+
* Uses Buffer to allow explicit zeroing of memory.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A secure string container that can be explicitly cleared from memory.
|
|
8
|
+
* Uses Buffer internally for memory that can be overwritten.
|
|
9
|
+
*/
|
|
10
|
+
export class SecureString {
|
|
11
|
+
private buffer: Buffer;
|
|
12
|
+
private cleared = false;
|
|
13
|
+
|
|
14
|
+
constructor(value: string) {
|
|
15
|
+
this.buffer = Buffer.from(value, 'utf-8');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the string value. Throws if already cleared.
|
|
20
|
+
*/
|
|
21
|
+
getValue(): string {
|
|
22
|
+
if (this.cleared) {
|
|
23
|
+
throw new Error('SecureString has been cleared');
|
|
24
|
+
}
|
|
25
|
+
return this.buffer.toString('utf-8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Securely clear the buffer by overwriting with zeros.
|
|
30
|
+
*/
|
|
31
|
+
clear(): void {
|
|
32
|
+
if (!this.cleared) {
|
|
33
|
+
this.buffer.fill(0);
|
|
34
|
+
this.cleared = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if the string has been cleared.
|
|
40
|
+
*/
|
|
41
|
+
isCleared(): boolean {
|
|
42
|
+
return this.cleared;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the length of the stored string.
|
|
47
|
+
*/
|
|
48
|
+
get length(): number {
|
|
49
|
+
return this.cleared ? 0 : this.buffer.length;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Execute a function with secure strings, ensuring cleanup on completion.
|
|
55
|
+
* @param values - Object with string values to protect
|
|
56
|
+
* @param fn - Function to execute with SecureString versions
|
|
57
|
+
* @returns Result of the function
|
|
58
|
+
*/
|
|
59
|
+
export async function withSecureStrings<T extends Record<string, string>, R>(
|
|
60
|
+
values: T,
|
|
61
|
+
fn: (secure: { [K in keyof T]: SecureString }) => Promise<R>
|
|
62
|
+
): Promise<R> {
|
|
63
|
+
const secureValues = {} as { [K in keyof T]: SecureString };
|
|
64
|
+
|
|
65
|
+
// Create secure versions
|
|
66
|
+
for (const key of Object.keys(values) as (keyof T)[]) {
|
|
67
|
+
secureValues[key] = new SecureString(values[key]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
return await fn(secureValues);
|
|
72
|
+
} finally {
|
|
73
|
+
// Always clear secure strings
|
|
74
|
+
for (const key of Object.keys(secureValues) as (keyof T)[]) {
|
|
75
|
+
secureValues[key].clear();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { register } from './commands/register.js';
|
|
5
|
+
import { login } from './commands/login.js';
|
|
6
|
+
import { deleteCommand } from './commands/delete.js';
|
|
7
|
+
import { config } from './commands/config.js';
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('vault')
|
|
13
|
+
.description('Secure credential vault CLI for AI agents')
|
|
14
|
+
.version('0.1.0');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('register')
|
|
18
|
+
.description('Register credentials for a new site')
|
|
19
|
+
.requiredOption('--cdp <endpoint>', 'CDP WebSocket endpoint (e.g., ws://localhost:9222)')
|
|
20
|
+
.requiredOption('--username-selector <selector>', 'CSS selector for username/email field')
|
|
21
|
+
.requiredOption('--password-selector <selector>', 'CSS selector for password field')
|
|
22
|
+
.option('--submit-selector <selector>', 'CSS selector for submit button')
|
|
23
|
+
.option('--username <username>', 'Username/email (non-interactive)')
|
|
24
|
+
.option('--password <password>', 'Password (non-interactive)')
|
|
25
|
+
.option('--generate-password', 'Generate a secure password (non-interactive)')
|
|
26
|
+
.option('-f, --force', 'Skip confirmation prompts')
|
|
27
|
+
.option('--allow-http', 'Allow HTTP origins (insecure - not recommended)')
|
|
28
|
+
.action(async (options) => {
|
|
29
|
+
try {
|
|
30
|
+
await register({
|
|
31
|
+
cdp: options.cdp,
|
|
32
|
+
usernameSelector: options.usernameSelector,
|
|
33
|
+
passwordSelector: options.passwordSelector,
|
|
34
|
+
submitSelector: options.submitSelector,
|
|
35
|
+
username: options.username,
|
|
36
|
+
password: options.password,
|
|
37
|
+
generatePassword: options.generatePassword,
|
|
38
|
+
force: options.force,
|
|
39
|
+
allowHttp: options.allowHttp,
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
program
|
|
48
|
+
.command('login')
|
|
49
|
+
.description('Fill credentials for a known site')
|
|
50
|
+
.requiredOption('--cdp <endpoint>', 'CDP WebSocket endpoint (e.g., ws://localhost:9222)')
|
|
51
|
+
.option('--submit', 'Click submit button after filling credentials')
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
try {
|
|
54
|
+
await login({
|
|
55
|
+
cdp: options.cdp,
|
|
56
|
+
submit: options.submit,
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command('delete')
|
|
66
|
+
.description('Delete credentials for a site')
|
|
67
|
+
.requiredOption('--origin <url>', 'Origin to delete (e.g., https://github.com)')
|
|
68
|
+
.option('-f, --force', 'Skip confirmation prompt')
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
try {
|
|
71
|
+
await deleteCommand({
|
|
72
|
+
origin: options.origin,
|
|
73
|
+
force: options.force,
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const configCmd = program
|
|
82
|
+
.command('config')
|
|
83
|
+
.description('Manage vault configuration');
|
|
84
|
+
|
|
85
|
+
configCmd
|
|
86
|
+
.command('list')
|
|
87
|
+
.description('List all configuration values')
|
|
88
|
+
.action(async () => {
|
|
89
|
+
try {
|
|
90
|
+
await config({ action: 'list' });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
configCmd
|
|
98
|
+
.command('get <key>')
|
|
99
|
+
.description('Get a configuration value')
|
|
100
|
+
.action(async (key) => {
|
|
101
|
+
try {
|
|
102
|
+
await config({ action: 'get', key });
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
configCmd
|
|
110
|
+
.command('set <key> <value>')
|
|
111
|
+
.description('Set a configuration value')
|
|
112
|
+
.action(async (key, value) => {
|
|
113
|
+
try {
|
|
114
|
+
await config({ action: 'set', key, value });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
configCmd
|
|
122
|
+
.command('unset <key>')
|
|
123
|
+
.description('Remove a configuration value')
|
|
124
|
+
.action(async (key) => {
|
|
125
|
+
try {
|
|
126
|
+
await config({ action: 'unset', key });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
program.parse();
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface Selectors {
|
|
2
|
+
username: string;
|
|
3
|
+
password: string;
|
|
4
|
+
submit?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface Credentials {
|
|
8
|
+
username: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RPConfig {
|
|
13
|
+
origin: string;
|
|
14
|
+
selectors: Selectors;
|
|
15
|
+
credentials: Credentials;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BrowserConnection {
|
|
19
|
+
browser: import('playwright').Browser;
|
|
20
|
+
page: import('playwright').Page;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VaultConfig {
|
|
24
|
+
defaultUsername?: string;
|
|
25
|
+
/** Allow HTTP origins (insecure - not recommended) */
|
|
26
|
+
allowHttp?: string;
|
|
27
|
+
/** Comma-separated list of allowed CDP hostnames */
|
|
28
|
+
cdpAllowlist?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ConfigKey = keyof VaultConfig;
|