Initial commit

This commit is contained in:
2026-05-09 23:50:24 +02:00
commit 0d25e52ebc
21 changed files with 867 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
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 = "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);
}