Initial commit
This commit is contained in:
+110
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user