@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
186 lines • 6.71 kB
JavaScript
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