@fentz26/envcp 1.2.0-exp.1 → 1.2.0-exp.92feaad
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/README.md +8 -1
- package/VERSION +1 -1
- package/dist/adapters/base.d.ts +42 -4
- package/dist/adapters/base.d.ts.map +1 -1
- package/dist/adapters/base.js +153 -26
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/gemini.d.ts +1 -4
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +22 -111
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -4
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +26 -125
- package/dist/adapters/openai.js.map +1 -1
- package/dist/adapters/rest.d.ts +1 -1
- package/dist/adapters/rest.d.ts.map +1 -1
- package/dist/adapters/rest.js +36 -16
- package/dist/adapters/rest.js.map +1 -1
- package/dist/cli/index.js +278 -107
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config-guard.d.ts.map +1 -1
- package/dist/config/config-guard.js +9 -4
- package/dist/config/config-guard.js.map +1 -1
- package/dist/config/config-hmac.js +2 -2
- package/dist/config/config-hmac.js.map +1 -1
- package/dist/config/manager.d.ts +6 -2
- package/dist/config/manager.d.ts.map +1 -1
- package/dist/config/manager.js +53 -33
- package/dist/config/manager.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +7 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/server/unified.d.ts.map +1 -1
- package/dist/server/unified.js +10 -10
- package/dist/server/unified.js.map +1 -1
- package/dist/service/config.d.ts.map +1 -1
- package/dist/service/config.js +5 -4
- package/dist/service/config.js.map +1 -1
- package/dist/service/generators.d.ts.map +1 -1
- package/dist/service/generators.js +6 -8
- package/dist/service/generators.js.map +1 -1
- package/dist/service/index.js +4 -3
- package/dist/service/index.js.map +1 -1
- package/dist/service/platform.js +2 -2
- package/dist/service/platform.js.map +1 -1
- package/dist/storage/index.d.ts +14 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +67 -27
- package/dist/storage/index.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/crypto.js +2 -2
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/fs.d.ts +1 -0
- package/dist/utils/fs.d.ts.map +1 -1
- package/dist/utils/fs.js +17 -5
- package/dist/utils/fs.js.map +1 -1
- package/dist/utils/hsm.d.ts +8 -8
- package/dist/utils/hsm.d.ts.map +1 -1
- package/dist/utils/hsm.js +7 -6
- package/dist/utils/hsm.js.map +1 -1
- package/dist/utils/http.d.ts +1 -1
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +1 -1
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/keychain.js +4 -4
- package/dist/utils/keychain.js.map +1 -1
- package/dist/utils/lock.d.ts.map +1 -1
- package/dist/utils/lock.js +2 -1
- package/dist/utils/lock.js.map +1 -1
- package/dist/utils/lockout.d.ts +2 -2
- package/dist/utils/lockout.d.ts.map +1 -1
- package/dist/utils/lockout.js +3 -3
- package/dist/utils/lockout.js.map +1 -1
- package/dist/utils/prompt.js +3 -3
- package/dist/utils/prompt.js.map +1 -1
- package/dist/utils/secure-memory.d.ts.map +1 -1
- package/dist/utils/secure-memory.js +6 -3
- package/dist/utils/secure-memory.js.map +1 -1
- package/dist/utils/session.js +3 -3
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/update-checker.d.ts +3 -0
- package/dist/utils/update-checker.d.ts.map +1 -1
- package/dist/utils/update-checker.js +51 -5
- package/dist/utils/update-checker.js.map +1 -1
- package/dist/vault/index.d.ts +2 -0
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +17 -5
- package/dist/vault/index.js.map +1 -1
- package/dist/version.js +3 -3
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/scripts/sync-version.js +65 -23
package/dist/cli/index.js
CHANGED
|
@@ -7,20 +7,67 @@ import { promptPassword, promptInput, promptConfirm, promptList } from '../utils
|
|
|
7
7
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
8
8
|
import { loadConfig, initConfig, saveConfig, parseEnvFile, registerMcpConfig, isBlacklisted, canAccess } from '../config/manager.js';
|
|
9
9
|
import { ConfigGuard } from '../config/config-guard.js';
|
|
10
|
-
import { StorageManager, LogManager } from '../storage/index.js';
|
|
10
|
+
import { StorageManager, LogManager, resolveLogPath } from '../storage/index.js';
|
|
11
11
|
import { VERSION } from '../version.js';
|
|
12
12
|
import { SessionManager } from '../utils/session.js';
|
|
13
13
|
import { maskValue, validatePassword, encrypt, decrypt, generateRecoveryKey, createRecoveryData, recoverPassword } from '../utils/crypto.js';
|
|
14
14
|
import { KeychainManager } from '../utils/keychain.js';
|
|
15
15
|
import { HsmManager } from '../utils/hsm.js';
|
|
16
|
-
import { checkForUpdate, formatUpdateMessage, logUpdateCheck } from '../utils/update-checker.js';
|
|
16
|
+
import { checkForUpdate, formatUpdateMessage, logUpdateCheck, fetchReleases, filterByChannel } from '../utils/update-checker.js';
|
|
17
|
+
import { spawnSync } from 'child_process';
|
|
17
18
|
import { LockoutManager } from '../utils/lockout.js';
|
|
18
19
|
import { initMemoryProtection } from '../utils/secure-memory.js';
|
|
19
|
-
import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
|
|
20
|
+
import { getGlobalVaultPath, getProjectVaultPath, resolveVaultPath, resolveSessionPath, setActiveVault, listVaults, initNamedVault, } from '../vault/index.js';
|
|
21
|
+
import { findProjectRoot } from '../utils/fs.js';
|
|
20
22
|
initMemoryProtection();
|
|
21
|
-
async function
|
|
23
|
+
async function resolveCliContext(vaultOverride) {
|
|
24
|
+
if (vaultOverride === 'global') {
|
|
25
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
26
|
+
const config = await loadConfig(home);
|
|
27
|
+
config.vault = { ...config.vault, mode: 'global' };
|
|
28
|
+
return { projectPath: home, config };
|
|
29
|
+
}
|
|
22
30
|
const projectPath = process.cwd();
|
|
23
31
|
const config = await loadConfig(projectPath);
|
|
32
|
+
if (vaultOverride === 'project') {
|
|
33
|
+
config.vault = { ...config.vault, mode: 'project' };
|
|
34
|
+
}
|
|
35
|
+
return { projectPath, config };
|
|
36
|
+
}
|
|
37
|
+
function buildSecuritySettings(securityChoice) {
|
|
38
|
+
const bruteForceProtection = {
|
|
39
|
+
enabled: true,
|
|
40
|
+
max_attempts: 5,
|
|
41
|
+
lockout_duration: 300,
|
|
42
|
+
progressive_delay: true,
|
|
43
|
+
max_delay: 60,
|
|
44
|
+
permanent_lockout_threshold: 50,
|
|
45
|
+
permanent_lockout_action: 'require_recovery_key',
|
|
46
|
+
notifications: {},
|
|
47
|
+
};
|
|
48
|
+
if (securityChoice === 'none') {
|
|
49
|
+
return {
|
|
50
|
+
encryption: { enabled: false },
|
|
51
|
+
storageEncrypted: false,
|
|
52
|
+
security: {
|
|
53
|
+
mode: 'recoverable',
|
|
54
|
+
recovery_file: '.envcp/.recovery',
|
|
55
|
+
brute_force_protection: bruteForceProtection,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
encryption: { enabled: true },
|
|
61
|
+
storageEncrypted: true,
|
|
62
|
+
security: {
|
|
63
|
+
mode: securityChoice,
|
|
64
|
+
recovery_file: '.envcp/.recovery',
|
|
65
|
+
brute_force_protection: bruteForceProtection,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function withSession(fn, vaultOverride) {
|
|
70
|
+
const { projectPath, config } = await resolveCliContext(vaultOverride);
|
|
24
71
|
const vaultPath = vaultOverride
|
|
25
72
|
? vaultOverride === 'global'
|
|
26
73
|
? getGlobalVaultPath(config)
|
|
@@ -28,15 +75,15 @@ async function withSession(fn, vaultOverride) {
|
|
|
28
75
|
: await resolveVaultPath(projectPath, config);
|
|
29
76
|
if (config.encryption?.enabled === false) {
|
|
30
77
|
const storage = new StorageManager(vaultPath, false);
|
|
31
|
-
const logManager = new LogManager(
|
|
78
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
32
79
|
await logManager.init();
|
|
33
80
|
await fn(storage, '', config, projectPath, logManager);
|
|
34
81
|
return;
|
|
35
82
|
}
|
|
36
|
-
const sessionManager = new SessionManager(
|
|
83
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
37
84
|
await sessionManager.init();
|
|
38
85
|
// Initialize audit logging
|
|
39
|
-
const logManager = new LogManager(
|
|
86
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
40
87
|
await logManager.init();
|
|
41
88
|
let session = await sessionManager.load();
|
|
42
89
|
let password = '';
|
|
@@ -136,12 +183,32 @@ program
|
|
|
136
183
|
.option('--hsm-type <type>', 'HSM type for --auth-method hsm|multi: yubikey | gpg | pkcs11')
|
|
137
184
|
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label for HSM auth')
|
|
138
185
|
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library for --hsm-type pkcs11')
|
|
186
|
+
.option('--global', 'Initialize a global vault at ~/.envcp/ (config + store live in $HOME)')
|
|
187
|
+
.option('--force', 'Overwrite existing envcp.yaml (DESTRUCTIVE: existing vault may become inaccessible)')
|
|
139
188
|
.action(async (options) => {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
189
|
+
const useGlobal = !!options.global;
|
|
190
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
191
|
+
const projectPath = useGlobal ? home : process.cwd();
|
|
192
|
+
const projectName = options.project || (useGlobal ? 'global' : path.basename(projectPath));
|
|
193
|
+
const configPath = path.join(projectPath, 'envcp.yaml');
|
|
194
|
+
if (await pathExists(configPath) && !options.force) {
|
|
195
|
+
const vaultRelPath = path.join('.envcp', 'store.enc');
|
|
196
|
+
console.log(chalk.yellow('EnvCP is already initialized here.'));
|
|
197
|
+
console.log(chalk.gray(` Config: ${configPath}`));
|
|
198
|
+
if (await pathExists(path.join(projectPath, vaultRelPath))) {
|
|
199
|
+
console.log(chalk.gray(` Vault: ${path.join(projectPath, vaultRelPath)}`));
|
|
200
|
+
}
|
|
201
|
+
console.log('');
|
|
202
|
+
console.log(chalk.gray('Running init again would overwrite your config and may make'));
|
|
203
|
+
console.log(chalk.gray('existing encrypted variables permanently inaccessible.'));
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(chalk.gray('To reconfigure: edit envcp.yaml directly'));
|
|
206
|
+
console.log(chalk.gray('To force re-init (DESTRUCTIVE): envcp init --force'));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
console.log(chalk.blue(useGlobal ? 'Initializing EnvCP (global vault)...' : 'Initializing EnvCP...'));
|
|
143
210
|
console.log('');
|
|
144
|
-
const config = await initConfig(projectPath, projectName);
|
|
211
|
+
const config = await initConfig(projectPath, projectName, { global: useGlobal });
|
|
145
212
|
// Single security question (or skip if --no-encrypt)
|
|
146
213
|
let securityChoice;
|
|
147
214
|
if (options.encrypt === false) {
|
|
@@ -154,43 +221,10 @@ program
|
|
|
154
221
|
{ name: 'Encrypted hard-lock (max security, no recovery)', value: 'hard-lock' },
|
|
155
222
|
], 'recoverable');
|
|
156
223
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
config.security = {
|
|
162
|
-
mode: 'recoverable',
|
|
163
|
-
recovery_file: '.envcp/.recovery',
|
|
164
|
-
brute_force_protection: {
|
|
165
|
-
enabled: true,
|
|
166
|
-
max_attempts: 5,
|
|
167
|
-
lockout_duration: 300,
|
|
168
|
-
progressive_delay: true,
|
|
169
|
-
max_delay: 60,
|
|
170
|
-
permanent_lockout_threshold: 50,
|
|
171
|
-
permanent_lockout_action: 'require_recovery_key',
|
|
172
|
-
notifications: {}
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
else {
|
|
177
|
-
config.encryption = { enabled: true };
|
|
178
|
-
config.storage.encrypted = true;
|
|
179
|
-
config.security = {
|
|
180
|
-
mode: securityChoice,
|
|
181
|
-
recovery_file: '.envcp/.recovery',
|
|
182
|
-
brute_force_protection: {
|
|
183
|
-
enabled: true,
|
|
184
|
-
max_attempts: 5,
|
|
185
|
-
lockout_duration: 300,
|
|
186
|
-
progressive_delay: true,
|
|
187
|
-
max_delay: 60,
|
|
188
|
-
permanent_lockout_threshold: 50,
|
|
189
|
-
permanent_lockout_action: 'require_recovery_key',
|
|
190
|
-
notifications: {},
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
}
|
|
224
|
+
const securitySettings = buildSecuritySettings(securityChoice);
|
|
225
|
+
config.encryption = securitySettings.encryption;
|
|
226
|
+
config.storage.encrypted = securitySettings.storageEncrypted;
|
|
227
|
+
config.security = securitySettings.security;
|
|
194
228
|
// For encrypted modes: get password now
|
|
195
229
|
let pwd = '';
|
|
196
230
|
if (securityChoice !== 'none') {
|
|
@@ -203,7 +237,7 @@ program
|
|
|
203
237
|
}
|
|
204
238
|
pwd = password;
|
|
205
239
|
}
|
|
206
|
-
await saveConfig(config, projectPath);
|
|
240
|
+
await saveConfig(config, projectPath, { global: useGlobal });
|
|
207
241
|
const modeLabel = securityChoice === 'none' ? 'no encryption' : securityChoice;
|
|
208
242
|
console.log(chalk.green('EnvCP initialized!'));
|
|
209
243
|
console.log(chalk.gray(` Project: ${config.project}`));
|
|
@@ -236,7 +270,7 @@ program
|
|
|
236
270
|
await storage.save(existing);
|
|
237
271
|
// Create session for encrypted mode
|
|
238
272
|
if (pwd) {
|
|
239
|
-
const sessionManager = new SessionManager(
|
|
273
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
240
274
|
await sessionManager.init();
|
|
241
275
|
await sessionManager.create(pwd);
|
|
242
276
|
}
|
|
@@ -303,7 +337,7 @@ program
|
|
|
303
337
|
if (await hsm.isAvailable()) {
|
|
304
338
|
try {
|
|
305
339
|
await hsm.protectVaultPassword(pwd);
|
|
306
|
-
await saveConfig(config, projectPath);
|
|
340
|
+
await saveConfig(config, projectPath, { global: useGlobal });
|
|
307
341
|
console.log('');
|
|
308
342
|
console.log(chalk.green(` Vault password protected by ${hsm.backendName}`));
|
|
309
343
|
console.log(chalk.gray(` Future sessions will authenticate via hardware`));
|
|
@@ -317,7 +351,7 @@ program
|
|
|
317
351
|
console.log(chalk.yellow(` HSM device (${hsm.backendName}) not available at init time.`));
|
|
318
352
|
console.log(chalk.gray(' Run "envcp unlock --setup-hsm" later to enable hardware authentication.'));
|
|
319
353
|
config.auth = { method: authMethod, multi_factors: ['password', 'hsm'], fallback: 'password' };
|
|
320
|
-
await saveConfig(config, projectPath);
|
|
354
|
+
await saveConfig(config, projectPath, { global: useGlobal });
|
|
321
355
|
}
|
|
322
356
|
}
|
|
323
357
|
console.log('');
|
|
@@ -326,20 +360,16 @@ program
|
|
|
326
360
|
program
|
|
327
361
|
.command('unlock')
|
|
328
362
|
.description('Unlock EnvCP session with password')
|
|
329
|
-
.option('-p, --password <password>', 'Password (will prompt if not provided)')
|
|
330
363
|
.option('--recovery-key <key>', 'Recovery key to clear permanent lockout')
|
|
331
364
|
.option('--save-to-keychain', 'Save password to OS keychain for auto-unlock')
|
|
332
365
|
.option('--setup-hsm', 'Protect vault password with hardware security module')
|
|
333
366
|
.option('--hsm-type <type>', 'HSM type: yubikey | gpg | pkcs11 (default: yubikey)')
|
|
334
367
|
.option('--key-id <id>', 'GPG key ID or PKCS#11 key label')
|
|
335
368
|
.option('--pkcs11-lib <path>', 'Path to PKCS#11 shared library (.so / .dll)')
|
|
369
|
+
.option('--global', 'Unlock the global vault at ~/.envcp')
|
|
336
370
|
.action(async (options) => {
|
|
337
|
-
const projectPath =
|
|
338
|
-
const
|
|
339
|
-
let password = options.password;
|
|
340
|
-
if (!password) {
|
|
341
|
-
password = await promptPassword('Enter password:');
|
|
342
|
-
}
|
|
371
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
372
|
+
const password = await promptPassword('Enter password:');
|
|
343
373
|
const { valid: passwordValid, warning: passwordWarning } = validatePassword(password, config.password || {});
|
|
344
374
|
if (!passwordValid) {
|
|
345
375
|
// nosem: no tainted data flows to log
|
|
@@ -349,7 +379,7 @@ program
|
|
|
349
379
|
if (passwordValid && passwordWarning) {
|
|
350
380
|
console.log(chalk.yellow('⚠ Weak password detected'));
|
|
351
381
|
}
|
|
352
|
-
const sessionDir = path.
|
|
382
|
+
const sessionDir = path.dirname(resolveSessionPath(projectPath, config));
|
|
353
383
|
let lockoutManager = new LockoutManager(path.join(sessionDir, '.lockout'));
|
|
354
384
|
// Handle recovery key if provided
|
|
355
385
|
if (options.recoveryKey) {
|
|
@@ -365,7 +395,7 @@ program
|
|
|
365
395
|
await lockoutManager.clearPermanentLockout();
|
|
366
396
|
console.log(chalk.green('Permanent lockout cleared.'));
|
|
367
397
|
// Log recovery event
|
|
368
|
-
const logManager = new LogManager(
|
|
398
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
369
399
|
await logManager.init();
|
|
370
400
|
await logManager.log({
|
|
371
401
|
timestamp: new Date().toISOString(),
|
|
@@ -394,12 +424,13 @@ program
|
|
|
394
424
|
const progressiveDelay = bfpConfig?.progressive_delay ?? true;
|
|
395
425
|
const maxDelay = bfpConfig?.max_delay ?? 60;
|
|
396
426
|
const permanentThreshold = bfpConfig?.permanent_lockout_threshold ?? 0;
|
|
397
|
-
const sessionManager = new SessionManager(
|
|
427
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
398
428
|
await sessionManager.init();
|
|
399
429
|
// Initialize audit logging
|
|
400
|
-
const logManager = new LogManager(
|
|
430
|
+
const logManager = new LogManager(resolveLogPath(config.audit, projectPath), config.audit);
|
|
401
431
|
await logManager.init();
|
|
402
|
-
const
|
|
432
|
+
const vaultPathForUnlock = await resolveVaultPath(projectPath, config);
|
|
433
|
+
const storage = new StorageManager(vaultPathForUnlock, config.storage.encrypted);
|
|
403
434
|
storage.setPassword(password);
|
|
404
435
|
const storeExists = await storage.exists();
|
|
405
436
|
// Check lockout before attempting password verification on existing stores
|
|
@@ -618,10 +649,10 @@ program
|
|
|
618
649
|
program
|
|
619
650
|
.command('lock')
|
|
620
651
|
.description('Lock EnvCP session')
|
|
621
|
-
.
|
|
622
|
-
|
|
623
|
-
const config = await
|
|
624
|
-
const sessionManager = new SessionManager(
|
|
652
|
+
.option('--global', 'Lock the global vault session at ~/.envcp/.session')
|
|
653
|
+
.action(async (options) => {
|
|
654
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
655
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
625
656
|
await sessionManager.init();
|
|
626
657
|
await sessionManager.destroy();
|
|
627
658
|
console.log(chalk.green('Session locked'));
|
|
@@ -629,10 +660,10 @@ program
|
|
|
629
660
|
program
|
|
630
661
|
.command('status')
|
|
631
662
|
.description('Check session status')
|
|
632
|
-
.
|
|
633
|
-
|
|
634
|
-
const config = await
|
|
635
|
-
const sessionManager = new SessionManager(
|
|
663
|
+
.option('--global', 'Check status of the global vault session at ~/.envcp/.session')
|
|
664
|
+
.action(async (options) => {
|
|
665
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
666
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
636
667
|
await sessionManager.init();
|
|
637
668
|
const password = await promptPassword('Enter password:');
|
|
638
669
|
const session = await sessionManager.load(password);
|
|
@@ -669,10 +700,10 @@ program
|
|
|
669
700
|
program
|
|
670
701
|
.command('extend')
|
|
671
702
|
.description('Extend session timeout')
|
|
672
|
-
.
|
|
673
|
-
|
|
674
|
-
const config = await
|
|
675
|
-
const sessionManager = new SessionManager(
|
|
703
|
+
.option('--global', 'Extend the global vault session at ~/.envcp/.session')
|
|
704
|
+
.action(async (options) => {
|
|
705
|
+
const { projectPath, config } = await resolveCliContext(options.global ? 'global' : undefined);
|
|
706
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
676
707
|
await sessionManager.init();
|
|
677
708
|
const password = await promptPassword('Enter password:');
|
|
678
709
|
const loaded = await sessionManager.load(password);
|
|
@@ -749,7 +780,7 @@ program
|
|
|
749
780
|
console.log(chalk.yellow.bold(` ${newRecoveryKey}`));
|
|
750
781
|
console.log(chalk.gray('Your old recovery key no longer works.'));
|
|
751
782
|
// Create a session with new password
|
|
752
|
-
const sessionManager = new SessionManager(
|
|
783
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
753
784
|
await sessionManager.init();
|
|
754
785
|
await sessionManager.create(newPassword);
|
|
755
786
|
console.log(chalk.green('Session unlocked with new password.'));
|
|
@@ -786,15 +817,70 @@ program
|
|
|
786
817
|
program
|
|
787
818
|
.command('add <name>')
|
|
788
819
|
.description('Add a new environment variable')
|
|
789
|
-
.option('-v, --value <value>', 'Variable value')
|
|
820
|
+
.option('-v, --value <value>', 'Variable value (WARNING: leaks in shell history; prefer --from-env/--from-file/--stdin)')
|
|
821
|
+
.option('--from-env <envVar>', 'Read value from the named environment variable')
|
|
822
|
+
.option('--from-file <path>', 'Read value from a file (trailing newline trimmed)')
|
|
823
|
+
.option('--stdin', 'Read value from piped stdin')
|
|
790
824
|
.option('-t, --tags <tags>', 'Tags (comma-separated)')
|
|
791
825
|
.option('-d, --description <desc>', 'Description')
|
|
792
826
|
.action(async (name, options) => {
|
|
827
|
+
const sourceFlags = [
|
|
828
|
+
options.value !== undefined ? '--value' : null,
|
|
829
|
+
options.fromEnv !== undefined ? '--from-env' : null,
|
|
830
|
+
options.fromFile !== undefined ? '--from-file' : null,
|
|
831
|
+
options.stdin ? '--stdin' : null,
|
|
832
|
+
].filter(Boolean);
|
|
833
|
+
if (sourceFlags.length > 1) {
|
|
834
|
+
console.error(chalk.red(`Error: ${sourceFlags.join(', ')} are mutually exclusive — pick one`));
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
837
|
+
let value;
|
|
838
|
+
let sourced = false;
|
|
839
|
+
if (options.value !== undefined) {
|
|
840
|
+
value = options.value;
|
|
841
|
+
sourced = true;
|
|
842
|
+
if (process.stdout.isTTY) {
|
|
843
|
+
console.warn(chalk.yellow('⚠ --value is visible in shell history and process list. Use --from-env, --from-file, or --stdin for secrets.'));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
else if (options.fromEnv) {
|
|
847
|
+
const envValue = process.env[options.fromEnv];
|
|
848
|
+
if (envValue === undefined) {
|
|
849
|
+
console.error(chalk.red(`Error: environment variable '${options.fromEnv}' is not set`));
|
|
850
|
+
process.exit(1);
|
|
851
|
+
}
|
|
852
|
+
value = envValue;
|
|
853
|
+
sourced = true;
|
|
854
|
+
}
|
|
855
|
+
else if (options.fromFile) {
|
|
856
|
+
try {
|
|
857
|
+
const raw = await fs.readFile(options.fromFile, 'utf-8');
|
|
858
|
+
value = raw.replace(/\r?\n$/, '');
|
|
859
|
+
sourced = true;
|
|
860
|
+
}
|
|
861
|
+
catch (err) {
|
|
862
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
863
|
+
console.error(chalk.red(`Error: cannot read '${options.fromFile}': ${msg}`));
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
else if (options.stdin) {
|
|
868
|
+
if (process.stdin.isTTY) {
|
|
869
|
+
console.error(chalk.red('Error: --stdin requires piped input (e.g., `echo "$SECRET" | envcp add NAME --stdin`)'));
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
value = await new Promise((resolve, reject) => {
|
|
873
|
+
const chunks = [];
|
|
874
|
+
process.stdin.on('data', (c) => chunks.push(typeof c === 'string' ? Buffer.from(c) : c));
|
|
875
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8').replace(/\r?\n$/, '')));
|
|
876
|
+
process.stdin.on('error', reject);
|
|
877
|
+
});
|
|
878
|
+
sourced = true;
|
|
879
|
+
}
|
|
793
880
|
await withSession(async (storage, _password, config) => {
|
|
794
|
-
let value = options.value;
|
|
795
881
|
let tags = [];
|
|
796
882
|
let description = options.description;
|
|
797
|
-
if (!
|
|
883
|
+
if (!sourced) {
|
|
798
884
|
value = await promptPassword('Enter value:');
|
|
799
885
|
const tagsInput = await promptInput('Tags (comma-separated):');
|
|
800
886
|
tags = tagsInput.split(',').map((t) => t.trim()).filter(Boolean);
|
|
@@ -803,6 +889,10 @@ program
|
|
|
803
889
|
else if (options.tags) {
|
|
804
890
|
tags = options.tags.split(',').map((t) => t.trim()).filter(Boolean);
|
|
805
891
|
}
|
|
892
|
+
if (value === undefined) {
|
|
893
|
+
console.error(chalk.red('Error: no value provided'));
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
806
896
|
const now = new Date().toISOString();
|
|
807
897
|
const variable = {
|
|
808
898
|
name,
|
|
@@ -909,13 +999,13 @@ program
|
|
|
909
999
|
continue;
|
|
910
1000
|
const excluded = config.sync.exclude?.some((pattern) => {
|
|
911
1001
|
// eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
|
|
912
|
-
const regex = new RegExp('^' + pattern.
|
|
1002
|
+
const regex = new RegExp('^' + pattern.replaceAll(/[.+?^${}()|[\]\\]/g, '\\$&').replaceAll('*', '.*') + '$');
|
|
913
1003
|
return regex.test(name);
|
|
914
1004
|
});
|
|
915
1005
|
if (excluded)
|
|
916
1006
|
continue;
|
|
917
1007
|
const needsQuoting = /[\s#"'\\]/.test(variable.value);
|
|
918
|
-
const val = needsQuoting ? `"${variable.value.
|
|
1008
|
+
const val = needsQuoting ? `"${variable.value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"` : variable.value;
|
|
919
1009
|
lines.push(`${name}=${val}`);
|
|
920
1010
|
}
|
|
921
1011
|
if (options.dryRun) {
|
|
@@ -935,7 +1025,7 @@ program
|
|
|
935
1025
|
continue;
|
|
936
1026
|
const excluded = config.sync.exclude?.some((pattern) => {
|
|
937
1027
|
// eslint-disable-next-line security/detect-non-literal-regexp -- glob pattern from config; metacharacters escaped above
|
|
938
|
-
const regex = new RegExp('^' + pattern.
|
|
1028
|
+
const regex = new RegExp('^' + pattern.replaceAll(/[.+?^${}()|[\]\\]/g, '\\$&').replaceAll('*', '.*') + '$');
|
|
939
1029
|
return regex.test(name);
|
|
940
1030
|
});
|
|
941
1031
|
if (excluded)
|
|
@@ -979,38 +1069,67 @@ program
|
|
|
979
1069
|
program
|
|
980
1070
|
.command('serve')
|
|
981
1071
|
.description('Start EnvCP server')
|
|
982
|
-
.option('-p, --password <password>', 'Encryption password')
|
|
983
1072
|
.option('-m, --mode <mode>', 'Server mode: mcp, rest, openai, gemini, all, auto', 'auto')
|
|
984
1073
|
.option('--port <port>', 'HTTP port (for non-MCP modes)', '3456')
|
|
985
1074
|
.option('--host <host>', 'HTTP host', '127.0.0.1')
|
|
986
1075
|
.option('-k, --api-key <key>', 'API key for HTTP authentication')
|
|
1076
|
+
.option('--global', 'Force the global vault at ~/.envcp (skip project lookup)')
|
|
987
1077
|
.action(async (options) => {
|
|
988
|
-
const
|
|
1078
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
1079
|
+
const globalConfigPath = path.join(home, '.envcp', 'config.yaml');
|
|
1080
|
+
// Resolve where the vault lives: --global forces home; otherwise walk up
|
|
1081
|
+
// from cwd looking for envcp.yaml; otherwise fall back to a global install.
|
|
1082
|
+
let projectPath;
|
|
1083
|
+
let forceGlobalMode = false;
|
|
1084
|
+
if (options.global) {
|
|
1085
|
+
projectPath = home;
|
|
1086
|
+
forceGlobalMode = true;
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
const found = await findProjectRoot(process.cwd());
|
|
1090
|
+
if (found) {
|
|
1091
|
+
projectPath = found;
|
|
1092
|
+
}
|
|
1093
|
+
else if (await pathExists(globalConfigPath)) {
|
|
1094
|
+
projectPath = home;
|
|
1095
|
+
forceGlobalMode = true;
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
process.stderr.write('Error: No envcp.yaml found in cwd or any ancestor, and no global config at ~/.envcp/config.yaml.\n' +
|
|
1099
|
+
'Run `envcp init` (project) or `envcp init --global` first.\n');
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
989
1103
|
const configGuard = new ConfigGuard(projectPath);
|
|
990
1104
|
const config = await configGuard.loadAndLock();
|
|
1105
|
+
if (forceGlobalMode) {
|
|
1106
|
+
config.vault = { ...config.vault, mode: 'global' };
|
|
1107
|
+
}
|
|
1108
|
+
const vaultPath = await resolveVaultPath(projectPath, config);
|
|
1109
|
+
const sessionPath = resolveSessionPath(projectPath, config);
|
|
991
1110
|
const mode = options.mode;
|
|
992
1111
|
const port = parseInt(options.port, 10);
|
|
993
1112
|
const host = options.host;
|
|
994
1113
|
const apiKey = options.apiKey;
|
|
995
|
-
let password =
|
|
1114
|
+
let password = '';
|
|
996
1115
|
// Passwordless mode: skip all session/password logic
|
|
997
1116
|
if (config.encryption?.enabled === false) {
|
|
998
1117
|
if (mode === 'mcp') {
|
|
999
1118
|
const { EnvCPServer } = await import('../mcp/server.js');
|
|
1000
|
-
const server = new EnvCPServer(config, projectPath);
|
|
1119
|
+
const server = new EnvCPServer(config, projectPath, undefined, vaultPath, sessionPath);
|
|
1001
1120
|
await server.start();
|
|
1002
1121
|
return;
|
|
1003
1122
|
}
|
|
1004
1123
|
}
|
|
1005
1124
|
else {
|
|
1006
1125
|
// Encrypted mode: need password
|
|
1007
|
-
const sessionManager = new SessionManager(
|
|
1126
|
+
const sessionManager = new SessionManager(sessionPath, config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
1008
1127
|
await sessionManager.init();
|
|
1009
1128
|
let session = await sessionManager.load();
|
|
1010
1129
|
if (!session && !password) {
|
|
1011
1130
|
// MCP mode uses stdio — can't prompt interactively
|
|
1012
1131
|
if (mode === 'mcp') {
|
|
1013
|
-
process.stderr.write(
|
|
1132
|
+
process.stderr.write(`Error: No active session at ${sessionPath}. Run \`envcp unlock${forceGlobalMode ? ' --global' : ''}\` first.\n`);
|
|
1014
1133
|
process.exit(1);
|
|
1015
1134
|
}
|
|
1016
1135
|
password = await promptPassword('Enter password:');
|
|
@@ -1029,7 +1148,7 @@ program
|
|
|
1029
1148
|
// MCP mode uses stdio
|
|
1030
1149
|
if (mode === 'mcp') {
|
|
1031
1150
|
const { EnvCPServer } = await import('../mcp/server.js');
|
|
1032
|
-
const server = new EnvCPServer(config, projectPath, password);
|
|
1151
|
+
const server = new EnvCPServer(config, projectPath, password, vaultPath, sessionPath);
|
|
1033
1152
|
await server.start();
|
|
1034
1153
|
return;
|
|
1035
1154
|
}
|
|
@@ -1134,7 +1253,7 @@ program
|
|
|
1134
1253
|
default: {
|
|
1135
1254
|
const lines = Object.entries(variables).map(([k, v]) => {
|
|
1136
1255
|
const needsQuoting = /[\s#"'\\]/.test(v.value);
|
|
1137
|
-
const val = needsQuoting ? `"${v.value.
|
|
1256
|
+
const val = needsQuoting ? `"${v.value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"` : v.value;
|
|
1138
1257
|
return `${k}=${val}`;
|
|
1139
1258
|
});
|
|
1140
1259
|
output = lines.join('\n');
|
|
@@ -1243,7 +1362,7 @@ program
|
|
|
1243
1362
|
console.log(chalk.yellow('No variables to backup'));
|
|
1244
1363
|
return;
|
|
1245
1364
|
}
|
|
1246
|
-
const timestamp = new Date().toISOString().
|
|
1365
|
+
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
|
|
1247
1366
|
const defaultPath = path.join(projectPath, `.envcp/backup-${timestamp}.enc`);
|
|
1248
1367
|
const outputPath = options.output || defaultPath;
|
|
1249
1368
|
const backupData = JSON.stringify({
|
|
@@ -1340,7 +1459,7 @@ program
|
|
|
1340
1459
|
}
|
|
1341
1460
|
// 5. Session status
|
|
1342
1461
|
if (encrypted) {
|
|
1343
|
-
const sessionManager = new SessionManager(
|
|
1462
|
+
const sessionManager = new SessionManager(resolveSessionPath(projectPath, config), config.session?.timeout_minutes || 30, config.session?.max_extensions || 5);
|
|
1344
1463
|
await sessionManager.init();
|
|
1345
1464
|
const session = await sessionManager.load();
|
|
1346
1465
|
if (session) {
|
|
@@ -1423,32 +1542,84 @@ program
|
|
|
1423
1542
|
});
|
|
1424
1543
|
program
|
|
1425
1544
|
.command('update')
|
|
1426
|
-
.description('Check for EnvCP updates')
|
|
1427
|
-
.option('--
|
|
1545
|
+
.description('Check for or install EnvCP updates')
|
|
1546
|
+
.option('-l, --latest', 'List and install latest stable versions')
|
|
1547
|
+
.option('-e, --experimental', 'List and install experimental versions')
|
|
1548
|
+
.option('-c, --canary', 'List and install canary versions')
|
|
1549
|
+
.option('--backup', 'Backup vault before installing')
|
|
1428
1550
|
.action(async (options) => {
|
|
1429
1551
|
const projectPath = process.cwd();
|
|
1430
|
-
|
|
1552
|
+
const home = os.homedir();
|
|
1553
|
+
const channel = options.latest ? 'latest' :
|
|
1554
|
+
options.experimental ? 'experimental' :
|
|
1555
|
+
options.canary ? 'canary' : null;
|
|
1556
|
+
if (!channel) {
|
|
1557
|
+
// Original behaviour: just check and notify
|
|
1431
1558
|
console.log(chalk.blue('Checking for updates...'));
|
|
1432
1559
|
try {
|
|
1433
1560
|
const info = await checkForUpdate(projectPath);
|
|
1434
1561
|
const message = formatUpdateMessage(info);
|
|
1435
1562
|
await logUpdateCheck(projectPath, info);
|
|
1436
1563
|
if (info.updateAvailable) {
|
|
1437
|
-
|
|
1438
|
-
console.log(chalk.red.bold(message));
|
|
1439
|
-
}
|
|
1440
|
-
else {
|
|
1441
|
-
console.log(chalk.yellow(message));
|
|
1442
|
-
}
|
|
1564
|
+
console.log(info.critical ? chalk.red.bold(message) : chalk.yellow(message));
|
|
1443
1565
|
}
|
|
1444
1566
|
else {
|
|
1445
1567
|
console.log(chalk.green(message));
|
|
1446
1568
|
}
|
|
1447
1569
|
}
|
|
1448
|
-
catch
|
|
1570
|
+
catch {
|
|
1449
1571
|
console.log(chalk.yellow('Could not check for updates (offline or rate-limited)'));
|
|
1450
|
-
console.log(chalk.gray(` Current version: v${require('../../package.json').version}`));
|
|
1451
1572
|
}
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
console.log(chalk.blue(`Fetching ${channel} releases...`));
|
|
1576
|
+
let releases;
|
|
1577
|
+
try {
|
|
1578
|
+
const all = await fetchReleases();
|
|
1579
|
+
releases = filterByChannel(all, channel).slice(0, 3);
|
|
1580
|
+
}
|
|
1581
|
+
catch {
|
|
1582
|
+
console.log(chalk.red('Could not fetch releases (offline or rate-limited)'));
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (releases.length === 0) {
|
|
1586
|
+
console.log(chalk.yellow(`No ${channel} releases found.`));
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
console.log(chalk.bold(`\n Available ${channel} versions:\n`));
|
|
1590
|
+
releases.forEach((r, i) => {
|
|
1591
|
+
console.log(chalk.cyan(` [${i + 1}] v${r.tag}`));
|
|
1592
|
+
});
|
|
1593
|
+
console.log(chalk.gray('\n [0] Cancel\n'));
|
|
1594
|
+
const answer = await promptInput('Pick a version (0 to cancel):');
|
|
1595
|
+
const picked = parseInt(answer.trim(), 10);
|
|
1596
|
+
if (!picked || picked < 1 || picked > releases.length) {
|
|
1597
|
+
console.log(chalk.gray('Cancelled.'));
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const chosen = releases[picked - 1];
|
|
1601
|
+
if (options.backup) {
|
|
1602
|
+
const backupPath = path.join(home, `.envcp.bak.${Date.now()}`);
|
|
1603
|
+
const vaultDir = path.join(home, '.envcp');
|
|
1604
|
+
const confirmed = await promptConfirm(`Backup ~/.envcp → ${backupPath}?`);
|
|
1605
|
+
if (confirmed) {
|
|
1606
|
+
try {
|
|
1607
|
+
await fs.cp(vaultDir, backupPath, { recursive: true });
|
|
1608
|
+
console.log(chalk.green(` ✓ Backed up to ${backupPath}`));
|
|
1609
|
+
}
|
|
1610
|
+
catch (e) {
|
|
1611
|
+
console.log(chalk.red(` Backup failed: ${e.message}`));
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
console.log(chalk.blue(`\nInstalling @fentz26/envcp@${chosen.tag}...`));
|
|
1617
|
+
const result = spawnSync('npm', ['install', '-g', `@fentz26/envcp@${chosen.tag}`], { stdio: 'inherit' });
|
|
1618
|
+
if (result.status === 0) {
|
|
1619
|
+
console.log(chalk.green(`\n ✓ Installed v${chosen.tag}`));
|
|
1620
|
+
}
|
|
1621
|
+
else {
|
|
1622
|
+
console.log(chalk.red('\n Installation failed.'));
|
|
1452
1623
|
}
|
|
1453
1624
|
});
|
|
1454
1625
|
program
|
|
@@ -1727,7 +1898,7 @@ program
|
|
|
1727
1898
|
.action(async (options) => {
|
|
1728
1899
|
const projectPath = process.cwd();
|
|
1729
1900
|
const config = await loadConfig(projectPath);
|
|
1730
|
-
const logDir =
|
|
1901
|
+
const logDir = resolveLogPath(config.audit, projectPath);
|
|
1731
1902
|
const logs = new LogManager(logDir, config.audit);
|
|
1732
1903
|
await logs.init();
|
|
1733
1904
|
if (options.dates) {
|
|
@@ -1794,7 +1965,7 @@ program
|
|
|
1794
1965
|
.action(async (options) => {
|
|
1795
1966
|
const projectPath = process.cwd();
|
|
1796
1967
|
const config = await loadConfig(projectPath);
|
|
1797
|
-
const logDir =
|
|
1968
|
+
const logDir = resolveLogPath(config.audit, projectPath);
|
|
1798
1969
|
const logs = new LogManager(logDir, config.audit);
|
|
1799
1970
|
await logs.init();
|
|
1800
1971
|
if (!config.audit.hmac_chain) {
|
|
@@ -1817,7 +1988,7 @@ program
|
|
|
1817
1988
|
.action(async () => {
|
|
1818
1989
|
const projectPath = process.cwd();
|
|
1819
1990
|
const config = await loadConfig(projectPath);
|
|
1820
|
-
const logDir =
|
|
1991
|
+
const logDir = resolveLogPath(config.audit, projectPath);
|
|
1821
1992
|
const logs = new LogManager(logDir, config.audit);
|
|
1822
1993
|
await logs.init();
|
|
1823
1994
|
if (config.audit.protection === 'none') {
|