/**
 * Vault — Transparent encryption layer for Avis private data.
 *
 * All sensitive data (memories, sessions, conversation buffer, graphs,
 * voice profiles, etc.) is encrypted at rest with AES-256-GCM.
 *
 * The master key lives exclusively in macOS Keychain under the service
 * "one.kaio.avis.vault". Only the Avis process can retrieve it at
 * runtime. The key is never written to disk, never logged, never
 * included in any prompt or session file.
 *
 * Design:
 *   - First run: generate 256-bit random key → store in Keychain
 *   - Every read: detect vault header → decrypt transparently
 *   - Every write: encrypt → write with vault header
 *   - Migration: on init, encrypt any existing plaintext files
 *
 * File format (binary):
 *   [magic 4B: "AVIS"] [version 1B: 0x01] [iv 12B] [authTag 16B] [ciphertext ...]
 */
import { execSync } from "node:child_process";
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
// ============================================================================
// Constants
// ============================================================================
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12;
const AUTH_TAG_LENGTH = 16;
const KEY_LENGTH = 32; // 256 bits
/** Magic bytes identifying an encrypted vault file */
const MAGIC = Buffer.from("AVIS");
const VERSION = 0x01;
const HEADER_LENGTH = MAGIC.length + 1 + IV_LENGTH + AUTH_TAG_LENGTH; // 4 + 1 + 12 + 16 = 33
/** Keychain identifiers */
const KEYCHAIN_SERVICE = "one.kaio.avis.vault";
const KEYCHAIN_ACCOUNT = "master-key";
// ============================================================================
// Keychain Operations (macOS only)
// ============================================================================
function keychainStore(key) {
    const hex = key.toString("hex");
    try {
        // Delete existing entry if present (ignore errors)
        execSync(`security delete-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" 2>/dev/null`, {
            stdio: "ignore",
        });
    }
    catch {
        // Entry didn't exist — fine
    }
    // Add to login keychain. The default ACL trusts the `security` CLI tool,
    // which is how Avis retrieves the key at runtime.
    execSync(`security add-generic-password ` +
        `-s "${KEYCHAIN_SERVICE}" ` +
        `-a "${KEYCHAIN_ACCOUNT}" ` +
        `-w "${hex}" ` +
        `-U`, { stdio: "ignore" });
    // Verify the key was stored correctly by reading it back
    const readBack = keychainRetrieve();
    if (!readBack || !readBack.equals(key)) {
        throw new Error("Vault key verification failed: stored key does not match");
    }
}
function keychainRetrieve() {
    try {
        const raw = execSync(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" -w 2>/dev/null`, { encoding: "utf-8" }).trim();
        if (!raw || raw.length !== KEY_LENGTH * 2)
            return null;
        return Buffer.from(raw, "hex");
    }
    catch {
        return null;
    }
}
function keychainExists() {
    try {
        execSync(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -a "${KEYCHAIN_ACCOUNT}" 2>/dev/null`, {
            stdio: "ignore",
        });
        return true;
    }
    catch {
        return false;
    }
}
// ============================================================================
// Low-level Encrypt / Decrypt
// ============================================================================
function encrypt(plaintext, key) {
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, key, iv, {
        authTagLength: AUTH_TAG_LENGTH,
    });
    const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
    const authTag = cipher.getAuthTag();
    // [MAGIC][VERSION][IV][AUTH_TAG][CIPHERTEXT]
    return Buffer.concat([MAGIC, Buffer.from([VERSION]), iv, authTag, encrypted]);
}
function decrypt(data, key) {
    if (!isVaultFile(data)) {
        throw new Error("Not a vault-encrypted file (bad magic)");
    }
    const version = data[MAGIC.length];
    if (version !== VERSION) {
        throw new Error(`Unsupported vault version: ${version}`);
    }
    let offset = MAGIC.length + 1;
    const iv = data.subarray(offset, offset + IV_LENGTH);
    offset += IV_LENGTH;
    const authTag = data.subarray(offset, offset + AUTH_TAG_LENGTH);
    offset += AUTH_TAG_LENGTH;
    const ciphertext = data.subarray(offset);
    const decipher = createDecipheriv(ALGORITHM, key, iv, {
        authTagLength: AUTH_TAG_LENGTH,
    });
    decipher.setAuthTag(authTag);
    return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
}
/** Check if a buffer starts with the vault magic header */
function isVaultFile(data) {
    if (data.length < HEADER_LENGTH)
        return false;
    return data.subarray(0, MAGIC.length).equals(MAGIC);
}
// ============================================================================
// Vault class
// ============================================================================
export class Vault {
    constructor() {
        this._key = null;
        this._ready = false;
    }
    /**
     * Initialize the vault. Must be called once at startup.
     * Generates a new master key on first run, or retrieves the
     * existing one from Keychain.
     */
    init() {
        if (this._ready)
            return;
        let key = keychainRetrieve();
        if (key) {
            // Existing key found — use it
            this._key = key;
            this._ready = true;
            return;
        }
        // First run: generate and store.
        // SAFETY: only generate a new key if no encrypted files exist yet.
        // If encrypted files exist but the key is missing, we have a problem
        // (keychain was wiped) and must NOT overwrite with a new key.
        const agentDir = this._findAgentDir();
        if (agentDir && this._hasEncryptedFiles(agentDir)) {
            throw new Error("Vault key not found in Keychain but encrypted files exist. " +
                "The Keychain entry may have been deleted. Cannot generate a new key " +
                "without losing access to encrypted data.");
        }
        key = randomBytes(KEY_LENGTH);
        keychainStore(key);
        this._key = key;
        this._ready = true;
    }
    /** Best-effort detection of the agent directory */
    _findAgentDir() {
        try {
            const root = join(process.env.HOME ?? "", ".avis");
            const memoryDir = join(root, "self", "memory");
            if (existsSync(memoryDir))
                return memoryDir;
            const agentDir = join(root, "agent");
            return existsSync(agentDir) ? agentDir : null;
        }
        catch {
            return null;
        }
    }
    /** Check if any encrypted (AVIS-magic) files exist in the agent dir */
    _hasEncryptedFiles(agentDir) {
        try {
            const candidates = [
                join(agentDir, "memory.jsonl"),
                join(agentDir, "conversation-buffer.json"),
                join(agentDir, "semantic-trie.json"),
                join(dirname(agentDir), "self", "memory", "memory.jsonl"),
                join(dirname(agentDir), "self", "memory", "conversation-buffer.json"),
                join(dirname(agentDir), "self", "memory", "semantic-trie.json"),
            ];
            for (const file of candidates) {
                if (existsSync(file)) {
                    const raw = readFileSync(file);
                    if (raw.length >= HEADER_LENGTH && raw.subarray(0, 4).equals(MAGIC)) {
                        return true;
                    }
                }
            }
            return false;
        }
        catch {
            return false;
        }
    }
    /** Whether the vault has been initialized with a key */
    get ready() {
        return this._ready;
    }
    assertReady() {
        if (!this._key) {
            throw new Error("Vault not initialized — call vault.init() first");
        }
        return this._key;
    }
    // ──────────────── Core Operations ────────────────
    /** Encrypt a UTF-8 string and return the vault buffer */
    encryptString(plaintext) {
        const key = this.assertReady();
        return encrypt(Buffer.from(plaintext, "utf-8"), key);
    }
    /** Decrypt a vault buffer to a UTF-8 string */
    decryptToString(data) {
        const key = this.assertReady();
        return decrypt(data, key).toString("utf-8");
    }
    /** Encrypt arbitrary binary data */
    encryptBuffer(plaintext) {
        const key = this.assertReady();
        return encrypt(plaintext, key);
    }
    /** Decrypt to raw binary buffer */
    decryptBuffer(data) {
        const key = this.assertReady();
        return decrypt(data, key);
    }
    /** Check if a buffer or file content is vault-encrypted */
    isEncrypted(data) {
        return isVaultFile(data);
    }
    // ──────────────── File System Wrappers ────────────────
    /**
     * Read a file, decrypting transparently if it's a vault file.
     * Returns the plaintext content as a UTF-8 string.
     */
    readFileSync(filePath) {
        const raw = readFileSync(filePath);
        if (isVaultFile(raw)) {
            return this.decryptToString(raw);
        }
        // Plaintext file (pre-migration or non-sensitive)
        return raw.toString("utf-8");
    }
    /**
     * Read a file as raw buffer, decrypting if vault-encrypted.
     */
    readFileBinarySync(filePath) {
        const raw = readFileSync(filePath);
        if (isVaultFile(raw)) {
            return this.decryptBuffer(raw);
        }
        return raw;
    }
    /**
     * Write encrypted content to a file.
     * Creates parent directories and sets restrictive permissions.
     */
    writeFileSync(filePath, content) {
        const dir = dirname(filePath);
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true, mode: 0o700 });
        }
        const encrypted = this.encryptString(content);
        writeFileSync(filePath, encrypted);
        chmodSync(filePath, 0o600);
    }
    /**
     * Write encrypted binary content to a file.
     */
    writeFileBinarySync(filePath, content) {
        const dir = dirname(filePath);
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true, mode: 0o700 });
        }
        const encrypted = this.encryptBuffer(content);
        writeFileSync(filePath, encrypted);
        chmodSync(filePath, 0o600);
    }
    /**
     * Append a line to a vault-encrypted file (e.g., memory.jsonl, session .jsonl).
     *
     * Since we can't append to AES-GCM ciphertext, this:
     *   1. Reads + decrypts the existing content
     *   2. Appends the new line
     *   3. Re-encrypts the whole file
     *
     * This is safe because session/memory files are small (<1MB typically)
     * and writes are infrequent relative to reads.
     */
    appendFileSync(filePath, line) {
        let existing = "";
        if (existsSync(filePath)) {
            existing = this.readFileSync(filePath);
        }
        this.writeFileSync(filePath, existing + line);
    }
    /**
     * Async version of readFileSync for vault files.
     */
    async readFileAsync(filePath) {
        const raw = await readFile(filePath);
        if (isVaultFile(raw)) {
            return this.decryptToString(raw);
        }
        return raw.toString("utf-8");
    }
    /**
     * Async version of writeFileSync for vault files.
     */
    async writeFileAsync(filePath, content) {
        const dir = dirname(filePath);
        if (!existsSync(dir)) {
            mkdirSync(dir, { recursive: true, mode: 0o700 });
        }
        const encrypted = this.encryptString(content);
        await writeFile(filePath, encrypted);
        chmodSync(filePath, 0o600);
    }
    // ──────────────── Migration ────────────────
    /**
     * Encrypt a single plaintext file in-place.
     * Skips files that are already encrypted.
     */
    migrateFile(filePath) {
        if (!existsSync(filePath))
            return false;
        const raw = readFileSync(filePath);
        if (isVaultFile(raw))
            return false; // already encrypted
        const encrypted = encrypt(raw, this.assertReady());
        writeFileSync(filePath, encrypted);
        chmodSync(filePath, 0o600);
        return true;
    }
    /**
     * Recursively encrypt all files in a directory.
     * Returns the number of files migrated.
     */
    migrateDirectory(dirPath, opts) {
        if (!existsSync(dirPath))
            return 0;
        let count = 0;
        const entries = readdirSync(dirPath, { withFileTypes: true });
        for (const entry of entries) {
            const fullPath = join(dirPath, entry.name);
            if (entry.isDirectory()) {
                count += this.migrateDirectory(fullPath, opts);
            }
            else if (entry.isFile()) {
                if (opts?.extensions) {
                    const hasExt = opts.extensions.some((ext) => entry.name.endsWith(ext));
                    if (!hasExt)
                        continue;
                }
                if (this.migrateFile(fullPath)) {
                    count++;
                }
            }
        }
        return count;
    }
    /**
     * Migrate all known Avis data directories.
     * Called once at startup to encrypt any plaintext remnants.
     */
    migrateAll(agentDir) {
        const key = this.assertReady();
        let migrated = 0;
        const errors = [];
        const rootDir = dirname(agentDir);
        const memoryDir = join(rootDir, "self", "memory");
        const sensitiveFiles = [
            join(agentDir, "auth.json"),
            join(memoryDir, "memory.jsonl"),
            join(memoryDir, "conversation-buffer.json"),
            join(memoryDir, "interactions.jsonl"),
            join(memoryDir, "semantic-trie.json"),
            join(memoryDir, "graph", "graph.json"),
            join(memoryDir, "graph", "extraction.json"),
            join(memoryDir, "graph", "memories.md"),
            join(rootDir, "voice", "voice_profile.npy"),
            join(rootDir, "voice.json"),
        ];
        // Encrypt individual known files
        for (const file of sensitiveFiles) {
            try {
                if (this.migrateFile(file))
                    migrated++;
            }
            catch (e) {
                errors.push(`${file}: ${e instanceof Error ? e.message : String(e)}`);
            }
        }
        // Encrypt all session files
        const sessionsDir = join(memoryDir, "sessions");
        if (existsSync(sessionsDir)) {
            try {
                migrated += this.migrateDirectory(sessionsDir, { extensions: [".jsonl"] });
            }
            catch (e) {
                errors.push(`sessions: ${e instanceof Error ? e.message : String(e)}`);
            }
        }
        return { migrated, errors };
    }
}
// ============================================================================
// Singleton
// ============================================================================
/** Global vault instance — initialized once at Avis boot */
export const vault = new Vault();
//# sourceMappingURL=vault.js.map