@lifestreamdynamics/vault-cli 1.0.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/LICENSE +21 -0
- package/README.md +759 -0
- package/dist/client.d.ts +12 -0
- package/dist/client.js +79 -0
- package/dist/commands/admin.d.ts +2 -0
- package/dist/commands/admin.js +263 -0
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.js +119 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +256 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +130 -0
- package/dist/commands/connectors.d.ts +2 -0
- package/dist/commands/connectors.js +224 -0
- package/dist/commands/docs.d.ts +2 -0
- package/dist/commands/docs.js +194 -0
- package/dist/commands/hooks.d.ts +2 -0
- package/dist/commands/hooks.js +159 -0
- package/dist/commands/keys.d.ts +2 -0
- package/dist/commands/keys.js +165 -0
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +138 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +61 -0
- package/dist/commands/shares.d.ts +2 -0
- package/dist/commands/shares.js +121 -0
- package/dist/commands/subscription.d.ts +2 -0
- package/dist/commands/subscription.js +166 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +565 -0
- package/dist/commands/teams.d.ts +2 -0
- package/dist/commands/teams.js +322 -0
- package/dist/commands/user.d.ts +2 -0
- package/dist/commands/user.js +48 -0
- package/dist/commands/vaults.d.ts +2 -0
- package/dist/commands/vaults.js +157 -0
- package/dist/commands/versions.d.ts +2 -0
- package/dist/commands/versions.js +219 -0
- package/dist/commands/webhooks.d.ts +2 -0
- package/dist/commands/webhooks.js +181 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +63 -0
- package/dist/lib/credential-manager.d.ts +48 -0
- package/dist/lib/credential-manager.js +101 -0
- package/dist/lib/encrypted-config.d.ts +20 -0
- package/dist/lib/encrypted-config.js +102 -0
- package/dist/lib/keychain.d.ts +8 -0
- package/dist/lib/keychain.js +82 -0
- package/dist/lib/migration.d.ts +31 -0
- package/dist/lib/migration.js +92 -0
- package/dist/lib/profiles.d.ts +43 -0
- package/dist/lib/profiles.js +104 -0
- package/dist/sync/config.d.ts +32 -0
- package/dist/sync/config.js +100 -0
- package/dist/sync/conflict.d.ts +30 -0
- package/dist/sync/conflict.js +60 -0
- package/dist/sync/daemon-worker.d.ts +1 -0
- package/dist/sync/daemon-worker.js +128 -0
- package/dist/sync/daemon.d.ts +44 -0
- package/dist/sync/daemon.js +174 -0
- package/dist/sync/diff.d.ts +43 -0
- package/dist/sync/diff.js +166 -0
- package/dist/sync/engine.d.ts +41 -0
- package/dist/sync/engine.js +233 -0
- package/dist/sync/ignore.d.ts +16 -0
- package/dist/sync/ignore.js +72 -0
- package/dist/sync/remote-poller.d.ts +23 -0
- package/dist/sync/remote-poller.js +145 -0
- package/dist/sync/state.d.ts +32 -0
- package/dist/sync/state.js +98 -0
- package/dist/sync/types.d.ts +68 -0
- package/dist/sync/types.js +4 -0
- package/dist/sync/watcher.d.ts +23 -0
- package/dist/sync/watcher.js +207 -0
- package/dist/utils/flags.d.ts +18 -0
- package/dist/utils/flags.js +31 -0
- package/dist/utils/format.d.ts +2 -0
- package/dist/utils/format.js +22 -0
- package/dist/utils/output.d.ts +87 -0
- package/dist/utils/output.js +229 -0
- package/package.json +62 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createKeychainBackend } from './keychain.js';
|
|
2
|
+
import { createEncryptedConfigBackend } from './encrypted-config.js';
|
|
3
|
+
// Default passphrase for encrypted config when no interactive prompt is available.
|
|
4
|
+
// This provides basic obfuscation — the real security benefit is file permissions (0600)
|
|
5
|
+
// and the fact that credentials aren't in a JSON file that's easy to grep for API keys.
|
|
6
|
+
const DEFAULT_PASSPHRASE = 'lsvault-cli-local-encryption-key';
|
|
7
|
+
export function createCredentialManager(options = {}) {
|
|
8
|
+
const keychain = options.keychain ?? createKeychainBackend();
|
|
9
|
+
const encryptedConfig = options.encryptedConfig ?? createEncryptedConfigBackend();
|
|
10
|
+
const passphrase = options.passphrase ?? DEFAULT_PASSPHRASE;
|
|
11
|
+
return {
|
|
12
|
+
async getCredentials() {
|
|
13
|
+
const result = {};
|
|
14
|
+
// 1. Environment variables (highest priority)
|
|
15
|
+
if (process.env.LSVAULT_API_KEY) {
|
|
16
|
+
result.apiKey = process.env.LSVAULT_API_KEY;
|
|
17
|
+
}
|
|
18
|
+
if (process.env.LSVAULT_API_URL) {
|
|
19
|
+
result.apiUrl = process.env.LSVAULT_API_URL;
|
|
20
|
+
}
|
|
21
|
+
// If env fully satisfies, return early
|
|
22
|
+
if (result.apiKey)
|
|
23
|
+
return result;
|
|
24
|
+
// 2. OS Keychain
|
|
25
|
+
if (await keychain.isAvailable()) {
|
|
26
|
+
const keychainCreds = await keychain.getCredentials();
|
|
27
|
+
if (keychainCreds.apiKey && !result.apiKey)
|
|
28
|
+
result.apiKey = keychainCreds.apiKey;
|
|
29
|
+
if (keychainCreds.apiUrl && !result.apiUrl)
|
|
30
|
+
result.apiUrl = keychainCreds.apiUrl;
|
|
31
|
+
}
|
|
32
|
+
if (result.apiKey)
|
|
33
|
+
return result;
|
|
34
|
+
// 3. Encrypted config
|
|
35
|
+
const encCreds = encryptedConfig.getCredentials(passphrase);
|
|
36
|
+
if (encCreds) {
|
|
37
|
+
if (encCreds.apiKey && !result.apiKey)
|
|
38
|
+
result.apiKey = encCreds.apiKey;
|
|
39
|
+
if (encCreds.apiUrl && !result.apiUrl)
|
|
40
|
+
result.apiUrl = encCreds.apiUrl;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
},
|
|
44
|
+
async saveCredentials(config) {
|
|
45
|
+
// Prefer keychain if available
|
|
46
|
+
if (await keychain.isAvailable()) {
|
|
47
|
+
await keychain.saveCredentials(config);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Fall back to encrypted config
|
|
51
|
+
encryptedConfig.saveCredentials(config, passphrase);
|
|
52
|
+
},
|
|
53
|
+
async clearCredentials() {
|
|
54
|
+
// Clear from all backends
|
|
55
|
+
await keychain.clearCredentials();
|
|
56
|
+
encryptedConfig.clearCredentials();
|
|
57
|
+
},
|
|
58
|
+
async getStorageMethod() {
|
|
59
|
+
if (process.env.LSVAULT_API_KEY)
|
|
60
|
+
return 'env';
|
|
61
|
+
if (await keychain.isAvailable()) {
|
|
62
|
+
const creds = await keychain.getCredentials();
|
|
63
|
+
if (creds.apiKey)
|
|
64
|
+
return 'keychain';
|
|
65
|
+
}
|
|
66
|
+
if (encryptedConfig.hasCredentials()) {
|
|
67
|
+
const creds = encryptedConfig.getCredentials(passphrase);
|
|
68
|
+
if (creds?.apiKey)
|
|
69
|
+
return 'encrypted-config';
|
|
70
|
+
}
|
|
71
|
+
return 'none';
|
|
72
|
+
},
|
|
73
|
+
async getVaultKey(vaultId) {
|
|
74
|
+
// 1. Environment variable: LSVAULT_VAULT_KEY_<vaultId> (hyphens to underscores, uppercase)
|
|
75
|
+
const envKey = `LSVAULT_VAULT_KEY_${vaultId.replace(/-/g, '_').toUpperCase()}`;
|
|
76
|
+
if (process.env[envKey]) {
|
|
77
|
+
return process.env[envKey];
|
|
78
|
+
}
|
|
79
|
+
// 2. Encrypted config: vaultKeys map
|
|
80
|
+
const creds = encryptedConfig.getCredentials(passphrase);
|
|
81
|
+
const vaultKeys = creds?.vaultKeys;
|
|
82
|
+
if (vaultKeys?.[vaultId]) {
|
|
83
|
+
return vaultKeys[vaultId];
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
},
|
|
87
|
+
async saveVaultKey(vaultId, keyHex) {
|
|
88
|
+
// Read existing config, merge vault key, save
|
|
89
|
+
const existing = encryptedConfig.getCredentials(passphrase) ?? {};
|
|
90
|
+
const vaultKeys = existing.vaultKeys ?? {};
|
|
91
|
+
vaultKeys[vaultId] = keyHex;
|
|
92
|
+
encryptedConfig.saveCredentials({ ...existing, vaultKeys }, passphrase);
|
|
93
|
+
},
|
|
94
|
+
async deleteVaultKey(vaultId) {
|
|
95
|
+
const existing = encryptedConfig.getCredentials(passphrase) ?? {};
|
|
96
|
+
const vaultKeys = existing.vaultKeys ?? {};
|
|
97
|
+
delete vaultKeys[vaultId];
|
|
98
|
+
encryptedConfig.saveCredentials({ ...existing, vaultKeys }, passphrase);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CliConfig } from '../config.js';
|
|
2
|
+
declare const CONFIG_DIR: string;
|
|
3
|
+
declare const ENCRYPTED_FILE: string;
|
|
4
|
+
declare const PBKDF2_ITERATIONS = 600000;
|
|
5
|
+
export interface EncryptedCredentials {
|
|
6
|
+
version: 1;
|
|
7
|
+
salt: string;
|
|
8
|
+
iv: string;
|
|
9
|
+
authTag: string;
|
|
10
|
+
ciphertext: string;
|
|
11
|
+
}
|
|
12
|
+
export interface EncryptedConfigBackend {
|
|
13
|
+
isAvailable(): boolean;
|
|
14
|
+
getCredentials(passphrase: string): Partial<CliConfig> | null;
|
|
15
|
+
saveCredentials(config: Partial<CliConfig>, passphrase: string): void;
|
|
16
|
+
clearCredentials(): void;
|
|
17
|
+
hasCredentials(): boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function createEncryptedConfigBackend(): EncryptedConfigBackend;
|
|
20
|
+
export { ENCRYPTED_FILE, CONFIG_DIR, PBKDF2_ITERATIONS };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.lsvault');
|
|
6
|
+
const ENCRYPTED_FILE = path.join(CONFIG_DIR, 'credentials.enc');
|
|
7
|
+
const PBKDF2_ITERATIONS = 600_000;
|
|
8
|
+
const SALT_LENGTH = 16;
|
|
9
|
+
const IV_LENGTH = 12;
|
|
10
|
+
const KEY_LENGTH = 32; // AES-256
|
|
11
|
+
const AUTH_TAG_LENGTH = 16;
|
|
12
|
+
function deriveKey(passphrase, salt) {
|
|
13
|
+
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
|
|
14
|
+
}
|
|
15
|
+
function encrypt(plaintext, passphrase) {
|
|
16
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
17
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
18
|
+
const key = deriveKey(passphrase, salt);
|
|
19
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
20
|
+
const encrypted = Buffer.concat([
|
|
21
|
+
cipher.update(plaintext, 'utf8'),
|
|
22
|
+
cipher.final(),
|
|
23
|
+
]);
|
|
24
|
+
const authTag = cipher.getAuthTag();
|
|
25
|
+
return {
|
|
26
|
+
version: 1,
|
|
27
|
+
salt: salt.toString('hex'),
|
|
28
|
+
iv: iv.toString('hex'),
|
|
29
|
+
authTag: authTag.toString('hex'),
|
|
30
|
+
ciphertext: encrypted.toString('hex'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function decrypt(data, passphrase) {
|
|
34
|
+
const salt = Buffer.from(data.salt, 'hex');
|
|
35
|
+
const iv = Buffer.from(data.iv, 'hex');
|
|
36
|
+
const authTag = Buffer.from(data.authTag, 'hex');
|
|
37
|
+
const ciphertext = Buffer.from(data.ciphertext, 'hex');
|
|
38
|
+
const key = deriveKey(passphrase, salt);
|
|
39
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
40
|
+
decipher.setAuthTag(authTag);
|
|
41
|
+
const decrypted = Buffer.concat([
|
|
42
|
+
decipher.update(ciphertext),
|
|
43
|
+
decipher.final(),
|
|
44
|
+
]);
|
|
45
|
+
return decrypted.toString('utf8');
|
|
46
|
+
}
|
|
47
|
+
export function createEncryptedConfigBackend() {
|
|
48
|
+
return {
|
|
49
|
+
isAvailable() {
|
|
50
|
+
return true; // Node.js crypto is always available
|
|
51
|
+
},
|
|
52
|
+
getCredentials(passphrase) {
|
|
53
|
+
if (!fs.existsSync(ENCRYPTED_FILE))
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(ENCRYPTED_FILE, 'utf-8');
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
if (data.version !== 1)
|
|
59
|
+
return null;
|
|
60
|
+
const plaintext = decrypt(data, passphrase);
|
|
61
|
+
return JSON.parse(plaintext);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Wrong passphrase or corrupt file
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
saveCredentials(config, passphrase) {
|
|
69
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
70
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
71
|
+
}
|
|
72
|
+
// Merge with existing credentials if possible
|
|
73
|
+
let existing = {};
|
|
74
|
+
if (fs.existsSync(ENCRYPTED_FILE)) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = fs.readFileSync(ENCRYPTED_FILE, 'utf-8');
|
|
77
|
+
const data = JSON.parse(raw);
|
|
78
|
+
const plaintext = decrypt(data, passphrase);
|
|
79
|
+
existing = JSON.parse(plaintext);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Can't decrypt existing — overwrite
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const merged = { ...existing, ...config };
|
|
86
|
+
const encrypted = encrypt(JSON.stringify(merged), passphrase);
|
|
87
|
+
const tmpFile = ENCRYPTED_FILE + '.tmp.' + crypto.randomBytes(4).toString('hex');
|
|
88
|
+
fs.writeFileSync(tmpFile, JSON.stringify(encrypted, null, 2) + '\n', { mode: 0o600 });
|
|
89
|
+
fs.renameSync(tmpFile, ENCRYPTED_FILE);
|
|
90
|
+
},
|
|
91
|
+
clearCredentials() {
|
|
92
|
+
if (fs.existsSync(ENCRYPTED_FILE)) {
|
|
93
|
+
fs.unlinkSync(ENCRYPTED_FILE);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
hasCredentials() {
|
|
97
|
+
return fs.existsSync(ENCRYPTED_FILE);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// Export for testing
|
|
102
|
+
export { ENCRYPTED_FILE, CONFIG_DIR, PBKDF2_ITERATIONS };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CliConfig } from '../config.js';
|
|
2
|
+
export interface KeychainBackend {
|
|
3
|
+
isAvailable(): Promise<boolean>;
|
|
4
|
+
getCredentials(): Promise<Partial<CliConfig>>;
|
|
5
|
+
saveCredentials(config: Partial<CliConfig>): Promise<void>;
|
|
6
|
+
clearCredentials(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createKeychainBackend(): KeychainBackend;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const SERVICE_NAME = 'lifestream-vault-cli';
|
|
2
|
+
const ACCOUNT_API_KEY = 'api-key';
|
|
3
|
+
const ACCOUNT_API_URL = 'api-url';
|
|
4
|
+
/**
|
|
5
|
+
* Dynamically loads keytar. Returns null if unavailable (not installed or
|
|
6
|
+
* native module fails to load, e.g. missing libsecret on Linux).
|
|
7
|
+
*/
|
|
8
|
+
async function loadKeytar() {
|
|
9
|
+
try {
|
|
10
|
+
return await import('keytar');
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createKeychainBackend() {
|
|
17
|
+
let keytarModule;
|
|
18
|
+
async function getKeytar() {
|
|
19
|
+
if (keytarModule === undefined) {
|
|
20
|
+
keytarModule = await loadKeytar();
|
|
21
|
+
}
|
|
22
|
+
return keytarModule;
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
async isAvailable() {
|
|
26
|
+
const kt = await getKeytar();
|
|
27
|
+
if (!kt)
|
|
28
|
+
return false;
|
|
29
|
+
// Verify we can actually use the keychain (libsecret may be missing)
|
|
30
|
+
try {
|
|
31
|
+
await kt.getPassword(SERVICE_NAME, '__probe__');
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
keytarModule = null;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async getCredentials() {
|
|
40
|
+
const kt = await getKeytar();
|
|
41
|
+
if (!kt)
|
|
42
|
+
return {};
|
|
43
|
+
const result = {};
|
|
44
|
+
try {
|
|
45
|
+
const apiKey = await kt.getPassword(SERVICE_NAME, ACCOUNT_API_KEY);
|
|
46
|
+
if (apiKey)
|
|
47
|
+
result.apiKey = apiKey;
|
|
48
|
+
const apiUrl = await kt.getPassword(SERVICE_NAME, ACCOUNT_API_URL);
|
|
49
|
+
if (apiUrl)
|
|
50
|
+
result.apiUrl = apiUrl;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Keychain access failed silently
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
},
|
|
57
|
+
async saveCredentials(config) {
|
|
58
|
+
const kt = await getKeytar();
|
|
59
|
+
if (!kt)
|
|
60
|
+
throw new Error('Keychain is not available');
|
|
61
|
+
if (config.apiKey) {
|
|
62
|
+
await kt.setPassword(SERVICE_NAME, ACCOUNT_API_KEY, config.apiKey);
|
|
63
|
+
}
|
|
64
|
+
if (config.apiUrl) {
|
|
65
|
+
await kt.setPassword(SERVICE_NAME, ACCOUNT_API_URL, config.apiUrl);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
async clearCredentials() {
|
|
69
|
+
const kt = await getKeytar();
|
|
70
|
+
if (!kt)
|
|
71
|
+
return;
|
|
72
|
+
try {
|
|
73
|
+
await kt.deletePassword(SERVICE_NAME, ACCOUNT_API_KEY);
|
|
74
|
+
}
|
|
75
|
+
catch { /* ignore */ }
|
|
76
|
+
try {
|
|
77
|
+
await kt.deletePassword(SERVICE_NAME, ACCOUNT_API_URL);
|
|
78
|
+
}
|
|
79
|
+
catch { /* ignore */ }
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { CliConfig } from '../config.js';
|
|
2
|
+
import type { CredentialManager } from './credential-manager.js';
|
|
3
|
+
declare const CONFIG_DIR: string;
|
|
4
|
+
declare const CONFIG_FILE: string;
|
|
5
|
+
export interface MigrationResult {
|
|
6
|
+
migrated: boolean;
|
|
7
|
+
method: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Checks if plaintext config contains an API key that should be migrated.
|
|
12
|
+
*/
|
|
13
|
+
export declare function hasPlaintextCredentials(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Reads the plaintext config file.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readPlaintextConfig(): Partial<CliConfig>;
|
|
18
|
+
/**
|
|
19
|
+
* Removes the apiKey from the plaintext config, keeping other fields (apiUrl).
|
|
20
|
+
*/
|
|
21
|
+
export declare function removePlaintextApiKey(): void;
|
|
22
|
+
/**
|
|
23
|
+
* Migrates credentials from plaintext config to secure storage.
|
|
24
|
+
*/
|
|
25
|
+
export declare function migrateCredentials(credentialManager: CredentialManager, promptFn?: () => Promise<boolean>): Promise<MigrationResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Prints migration warnings if plaintext credentials are detected.
|
|
28
|
+
* Returns true if migration was performed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkAndPromptMigration(credentialManager: CredentialManager): Promise<boolean>;
|
|
31
|
+
export { CONFIG_FILE, CONFIG_DIR };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.lsvault');
|
|
6
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
|
+
/**
|
|
8
|
+
* Checks if plaintext config contains an API key that should be migrated.
|
|
9
|
+
*/
|
|
10
|
+
export function hasPlaintextCredentials() {
|
|
11
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
15
|
+
const config = JSON.parse(raw);
|
|
16
|
+
return typeof config.apiKey === 'string' && config.apiKey.length > 0;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Reads the plaintext config file.
|
|
24
|
+
*/
|
|
25
|
+
export function readPlaintextConfig() {
|
|
26
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
27
|
+
return {};
|
|
28
|
+
try {
|
|
29
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Removes the apiKey from the plaintext config, keeping other fields (apiUrl).
|
|
38
|
+
*/
|
|
39
|
+
export function removePlaintextApiKey() {
|
|
40
|
+
if (!fs.existsSync(CONFIG_FILE))
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
44
|
+
const config = JSON.parse(raw);
|
|
45
|
+
delete config.apiKey;
|
|
46
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// ignore errors
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Migrates credentials from plaintext config to secure storage.
|
|
54
|
+
*/
|
|
55
|
+
export async function migrateCredentials(credentialManager, promptFn) {
|
|
56
|
+
if (!hasPlaintextCredentials()) {
|
|
57
|
+
return { migrated: false, method: 'none', error: 'No plaintext credentials found' };
|
|
58
|
+
}
|
|
59
|
+
// Ask user for confirmation if prompt function provided
|
|
60
|
+
if (promptFn) {
|
|
61
|
+
const confirmed = await promptFn();
|
|
62
|
+
if (!confirmed) {
|
|
63
|
+
return { migrated: false, method: 'skipped' };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const plaintextConfig = readPlaintextConfig();
|
|
67
|
+
try {
|
|
68
|
+
await credentialManager.saveCredentials({
|
|
69
|
+
apiKey: plaintextConfig.apiKey,
|
|
70
|
+
});
|
|
71
|
+
// Remove apiKey from plaintext config
|
|
72
|
+
removePlaintextApiKey();
|
|
73
|
+
const method = await credentialManager.getStorageMethod();
|
|
74
|
+
return { migrated: true, method };
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
78
|
+
return { migrated: false, method: 'error', error: message };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Prints migration warnings if plaintext credentials are detected.
|
|
83
|
+
* Returns true if migration was performed.
|
|
84
|
+
*/
|
|
85
|
+
export async function checkAndPromptMigration(credentialManager) {
|
|
86
|
+
if (!hasPlaintextCredentials())
|
|
87
|
+
return false;
|
|
88
|
+
console.log(chalk.yellow('\nWarning: API key found in plaintext config (~/.lsvault/config.json)'));
|
|
89
|
+
console.log(chalk.yellow('Run `lsvault auth migrate` to migrate to secure storage.\n'));
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
export { CONFIG_FILE, CONFIG_DIR };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface ProfileConfig {
|
|
2
|
+
apiUrl?: string;
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
[key: string]: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Returns the path to a named profile's config file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getProfilePath(name: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Returns the name of the currently active profile, or 'default' if none set.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getActiveProfile(): string;
|
|
14
|
+
/**
|
|
15
|
+
* Sets the active profile name.
|
|
16
|
+
*/
|
|
17
|
+
export declare function setActiveProfile(name: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Loads a profile's configuration. Returns an empty object if the profile
|
|
20
|
+
* does not exist.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadProfile(name: string): ProfileConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Saves a value to a profile. Creates the profile directory and file if needed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function setProfileValue(name: string, key: string, value: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Gets a single value from a profile.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getProfileValue(name: string, key: string): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Lists all available profile names (derived from filenames in the profiles dir).
|
|
33
|
+
*/
|
|
34
|
+
export declare function listProfiles(): string[];
|
|
35
|
+
/**
|
|
36
|
+
* Deletes a profile.
|
|
37
|
+
*/
|
|
38
|
+
export declare function deleteProfile(name: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Resolves the effective profile name: explicit --profile flag takes precedence,
|
|
41
|
+
* otherwise the active profile is used.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveProfileName(explicitProfile?: string): string;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const CONFIG_DIR = path.join(os.homedir(), '.lsvault');
|
|
5
|
+
const PROFILES_DIR = path.join(CONFIG_DIR, 'profiles');
|
|
6
|
+
const ACTIVE_PROFILE_FILE = path.join(CONFIG_DIR, 'active-profile');
|
|
7
|
+
/**
|
|
8
|
+
* Returns the path to a named profile's config file.
|
|
9
|
+
*/
|
|
10
|
+
export function getProfilePath(name) {
|
|
11
|
+
return path.join(PROFILES_DIR, `${name}.json`);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns the name of the currently active profile, or 'default' if none set.
|
|
15
|
+
*/
|
|
16
|
+
export function getActiveProfile() {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(ACTIVE_PROFILE_FILE)) {
|
|
19
|
+
return fs.readFileSync(ACTIVE_PROFILE_FILE, 'utf-8').trim() || 'default';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Fall through
|
|
24
|
+
}
|
|
25
|
+
return 'default';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sets the active profile name.
|
|
29
|
+
*/
|
|
30
|
+
export function setActiveProfile(name) {
|
|
31
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
32
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
fs.writeFileSync(ACTIVE_PROFILE_FILE, name + '\n');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Loads a profile's configuration. Returns an empty object if the profile
|
|
38
|
+
* does not exist.
|
|
39
|
+
*/
|
|
40
|
+
export function loadProfile(name) {
|
|
41
|
+
const filePath = getProfilePath(name);
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(filePath)) {
|
|
44
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Fall through — treat as empty
|
|
49
|
+
}
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Saves a value to a profile. Creates the profile directory and file if needed.
|
|
54
|
+
*/
|
|
55
|
+
export function setProfileValue(name, key, value) {
|
|
56
|
+
if (!fs.existsSync(PROFILES_DIR)) {
|
|
57
|
+
fs.mkdirSync(PROFILES_DIR, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
const config = loadProfile(name);
|
|
60
|
+
config[key] = value;
|
|
61
|
+
fs.writeFileSync(getProfilePath(name), JSON.stringify(config, null, 2) + '\n');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Gets a single value from a profile.
|
|
65
|
+
*/
|
|
66
|
+
export function getProfileValue(name, key) {
|
|
67
|
+
const config = loadProfile(name);
|
|
68
|
+
return config[key];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Lists all available profile names (derived from filenames in the profiles dir).
|
|
72
|
+
*/
|
|
73
|
+
export function listProfiles() {
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.existsSync(PROFILES_DIR)) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
return fs.readdirSync(PROFILES_DIR)
|
|
79
|
+
.filter(f => f.endsWith('.json'))
|
|
80
|
+
.map(f => f.replace(/\.json$/, ''))
|
|
81
|
+
.sort();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Deletes a profile.
|
|
89
|
+
*/
|
|
90
|
+
export function deleteProfile(name) {
|
|
91
|
+
const filePath = getProfilePath(name);
|
|
92
|
+
if (fs.existsSync(filePath)) {
|
|
93
|
+
fs.unlinkSync(filePath);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolves the effective profile name: explicit --profile flag takes precedence,
|
|
100
|
+
* otherwise the active profile is used.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveProfileName(explicitProfile) {
|
|
103
|
+
return explicitProfile || getActiveProfile();
|
|
104
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SyncConfig, CreateSyncOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Read all sync configurations from disk.
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadSyncConfigs(): SyncConfig[];
|
|
6
|
+
/**
|
|
7
|
+
* Write all sync configurations to disk.
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveSyncConfigs(configs: SyncConfig[]): void;
|
|
10
|
+
/**
|
|
11
|
+
* Find a sync config by its ID.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getSyncConfig(id: string): SyncConfig | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Find a sync config by vault ID.
|
|
16
|
+
* Returns the first match (a vault should typically only have one sync config).
|
|
17
|
+
*/
|
|
18
|
+
export declare function getSyncConfigByVaultId(vaultId: string): SyncConfig | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Create a new sync configuration.
|
|
21
|
+
* Returns the created config with a generated ID.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createSyncConfig(opts: CreateSyncOptions): SyncConfig;
|
|
24
|
+
/**
|
|
25
|
+
* Delete a sync configuration by ID.
|
|
26
|
+
* Returns true if the config was found and deleted.
|
|
27
|
+
*/
|
|
28
|
+
export declare function deleteSyncConfig(id: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Update the lastSyncAt timestamp for a sync config.
|
|
31
|
+
*/
|
|
32
|
+
export declare function updateLastSync(id: string, timestamp?: string): void;
|