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,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;