UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

186 lines 6.71 kB
import * as os from 'os'; import * as path from 'path'; import { existsSync } from 'fs'; import * as fs from 'fs/promises'; import { getCachedMachineId } from '../machine-id.js'; import { encode, decode, encrypt, decrypt, hashKey, createFingerprint } from '../crypto.js'; import { SecretError } from '../types.js'; export class LocalSecretProvider { constructor(config) { this.initialized = false; const baseDir = config?.['storageDir'] || path.join(os.homedir(), '.xec', 'secrets'); this.storageDir = path.resolve(baseDir); this.passphrase = config?.['passphrase']; } async initialize() { if (this.initialized) return; await fs.mkdir(this.storageDir, { recursive: true, mode: 0o700 }); const indexPath = this.getIndexPath(); if (!existsSync(indexPath)) { await this.writeIndex({}); } try { await fs.access(this.storageDir, fs.constants.R_OK | fs.constants.W_OK); } catch (error) { throw new SecretError(`Cannot access secret storage directory: ${this.storageDir}`, 'STORAGE_ACCESS_ERROR'); } this.initialized = true; } async get(key) { await this.ensureInitialized(); try { const secretPath = this.getSecretPath(key); if (!existsSync(secretPath)) { return null; } const data = await fs.readFile(secretPath, 'utf8'); const encryptedSecret = JSON.parse(data); const machineId = await getCachedMachineId(); const dataWithSalt = JSON.parse(data); const decrypted = await decrypt(decode(encryptedSecret.encrypted), decode(dataWithSalt.salt), decode(encryptedSecret.iv), decode(encryptedSecret.authTag), machineId, this.passphrase); return decrypted; } catch (error) { if (error.code === 'ENOENT') { return null; } throw new SecretError(`Failed to get secret: ${error instanceof Error ? error.message : 'Unknown error'}`, 'GET_ERROR', key); } } async set(key, value) { await this.ensureInitialized(); try { const machineId = await getCachedMachineId(); const { encrypted, salt, iv, authTag } = await encrypt(value, machineId, this.passphrase); const encryptedSecret = { version: 1, encrypted: encode(encrypted), iv: encode(iv), authTag: encode(authTag), algorithm: 'aes-256-gcm', metadata: { key, createdAt: new Date(), updatedAt: new Date() } }; const dataWithSalt = { ...encryptedSecret, salt: encode(salt) }; const secretPath = this.getSecretPath(key); await fs.writeFile(secretPath, JSON.stringify(dataWithSalt, null, 2), { mode: 0o600 }); await this.updateIndex(key, { hashedKey: hashKey(key), createdAt: encryptedSecret.metadata.createdAt, updatedAt: encryptedSecret.metadata.updatedAt, fingerprint: createFingerprint(encrypted) }); } catch (error) { throw new SecretError(`Failed to set secret: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SET_ERROR', key); } } async delete(key) { await this.ensureInitialized(); try { const secretPath = this.getSecretPath(key); await fs.unlink(secretPath); await this.removeFromIndex(key); } catch (error) { if (error.code === 'ENOENT') { return; } throw new SecretError(`Failed to delete secret: ${error instanceof Error ? error.message : 'Unknown error'}`, 'DELETE_ERROR', key); } } async list() { await this.ensureInitialized(); try { const index = await this.readIndex(); return Object.keys(index); } catch (error) { throw new SecretError(`Failed to list secrets: ${error instanceof Error ? error.message : 'Unknown error'}`, 'LIST_ERROR'); } } async has(key) { await this.ensureInitialized(); const secretPath = this.getSecretPath(key); return existsSync(secretPath); } async changePassphrase(oldPassphrase, newPassphrase) { await this.ensureInitialized(); const keys = await this.list(); const tempProvider = new LocalSecretProvider({ storageDir: this.storageDir, passphrase: oldPassphrase }); for (const key of keys) { const value = await tempProvider.get(key); if (value !== null) { this.passphrase = newPassphrase; await this.set(key, value); } } } async export() { await this.ensureInitialized(); const keys = await this.list(); const secrets = {}; for (const key of keys) { const value = await this.get(key); if (value !== null) { secrets[key] = value; } } return secrets; } async import(secrets) { await this.ensureInitialized(); for (const [key, value] of Object.entries(secrets)) { await this.set(key, value); } } async ensureInitialized() { if (!this.initialized) { await this.initialize(); } } getSecretPath(key) { const hashedKey = hashKey(key); return path.join(this.storageDir, `${hashedKey}.secret`); } getIndexPath() { return path.join(this.storageDir, '.index.json'); } async readIndex() { try { const data = await fs.readFile(this.getIndexPath(), 'utf8'); return JSON.parse(data); } catch (error) { if (error.code === 'ENOENT') { return {}; } throw error; } } async writeIndex(index) { await fs.writeFile(this.getIndexPath(), JSON.stringify(index, null, 2), { mode: 0o600 }); } async updateIndex(key, metadata) { const index = await this.readIndex(); index[key] = metadata; await this.writeIndex(index); } async removeFromIndex(key) { const index = await this.readIndex(); delete index[key]; await this.writeIndex(index); } } //# sourceMappingURL=local.js.map