import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import type { KeePassCommand, KeePassEntry, KeePassFindQuery, KeePassGroup, KeePassOpenOptions, KeePassResponse, } from "./types"; export class KeePassDatabase { constructor( private readonly path: string, private readonly options: KeePassOpenOptions, private readonly pythonPath = process.env.PYTHON_PATH ?? ".venv/bin/python3", private readonly bridgePath = new URL("./python/bridge.py", import.meta.url) ) {} async listEntries(): Promise { const response = await this.run({ command: "list-entries" }); return response; } async findEntries(query: KeePassFindQuery): Promise { const response = await this.run({ command: "find-entries", query }); return response; } async listGroups(): Promise { const response = await this.run({ command: "list-groups" }); return response; } async close(): Promise { // No persistent process is kept alive yet. return; } private async run(command: KeePassCommand): Promise { const payload = JSON.stringify({ ...command, path: this.path, password: this.options.password, keyFile: this.options.keyFile, }); const bridgeFile = fileURLToPath(this.bridgePath); const result = await new Promise<{ stdout: string; stderr: string; code: number }>((resolve, reject) => { const child = spawn(this.pythonPath, [bridgeFile], { stdio: ["pipe", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; let settled = false; try { child.stdin.write(payload); child.stdin.end(); } catch (error) { reject(error); } child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); child.on("error", (error) => { if (!settled) { settled = true; reject(error); } }); child.on("close", (code) => { if (!settled) { settled = true; resolve({ stdout, stderr, code: code ?? 1 }); } }); }); const output = result.stdout.trim(); if (!output) { throw new Error(result.stderr || `Empty response from Python bridge (exit code ${result.code})`); } let parsed: KeePassResponse; try { parsed = JSON.parse(output) as KeePassResponse; } catch (error) { throw new Error(`Invalid JSON from Python bridge: ${error instanceof Error ? error.message : String(error)}`); } if (!parsed.ok) { throw new Error(parsed.error || result.stderr || `KeePass bridge error (exit code ${result.code})`); } return parsed.data; } } export function openKeePassDatabase(path: string, options: KeePassOpenOptions) { return new KeePassDatabase(path, options); }