Files
ts-pykeepass-wrapper/src/keepass.ts
T
matmoul 4cb568c326 fix: default bridge and tests to project venv Python
Use .venv/bin/python3 by default, with PYTHON_PATH as an override, and update the setup script and docs to match the new virtualenv-based workflow.
2026-05-10 00:00:56 +02:00

110 lines
2.8 KiB
TypeScript

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<KeePassEntry[]> {
const response = await this.run<KeePassEntry[]>({ command: "list-entries" });
return response;
}
async findEntries(query: KeePassFindQuery): Promise<KeePassEntry[]> {
const response = await this.run<KeePassEntry[]>({ command: "find-entries", query });
return response;
}
async listGroups(): Promise<KeePassGroup[]> {
const response = await this.run<KeePassGroup[]>({ command: "list-groups" });
return response;
}
async close(): Promise<void> {
// No persistent process is kept alive yet.
return;
}
private async run<T>(command: KeePassCommand): Promise<T> {
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<T>;
try {
parsed = JSON.parse(output) as KeePassResponse<T>;
} 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);
}