feat: add write support for KeePass entries and groups

This commit is contained in:
2026-05-10 00:19:42 +02:00
parent 89ba04d61a
commit da0b396bf8
7 changed files with 221 additions and 39 deletions
+18 -5
View File
@@ -3,18 +3,20 @@ import { fileURLToPath } from "node:url";
import type {
KeePassCommand,
KeePassEntry,
KeePassEntryInput,
KeePassFindQuery,
KeePassGroup,
KeePassGroupInput,
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)
private path: string,
private options: KeePassOpenOptions,
private pythonPath = process.env.PYTHON_PATH ?? ".venv/bin/python3",
private bridgePath = new URL("./python/bridge.py", import.meta.url)
) {}
async listEntries(): Promise<KeePassEntry[]> {
@@ -32,8 +34,19 @@ export class KeePassDatabase {
return response;
}
async createEntry(entry: KeePassEntryInput): Promise<KeePassEntry> {
return this.run<KeePassEntry>({ command: "create-entry", entry });
}
async createGroup(group: KeePassGroupInput): Promise<KeePassGroup> {
return this.run<KeePassGroup>({ command: "create-group", group });
}
async save(): Promise<void> {
await this.run<void>({ command: "save" });
}
async close(): Promise<void> {
// No persistent process is kept alive yet.
return;
}
+70
View File
@@ -60,6 +60,40 @@ def load_db(db_path, password, key_file=None):
return PyKeePass(db_path, password=password)
def save_db(db):
db.save()
def normalize_path(value):
if value is None:
return ""
return str(value).strip()
def resolve_group_by_path(db, group_path):
normalized = normalize_path(group_path)
if not normalized:
return db.root_group
candidate_paths = [normalized]
if normalized.startswith("/"):
candidate_paths.append(normalized.lstrip("/"))
else:
candidate_paths.append(f"/{normalized}")
for candidate in candidate_paths:
matching_groups = db.find_groups(path=candidate)
if matching_groups:
return matching_groups[0]
matching_groups = db.find_groups(name=normalized.split("/")[-1])
for group in matching_groups:
if path_to_string(group.path).endswith(normalized):
return group
raise ValueError(f"Group not found: {normalized}")
def read_payload():
if len(sys.argv) > 1 and sys.argv[1].strip():
return json.loads(sys.argv[1])
@@ -123,6 +157,42 @@ def main():
emit({"ok": True, "data": groups})
return 0
if command == "create-entry":
entry_input = payload.get("entry", {})
title = normalize_path(entry_input.get("title"))
if not title:
emit({"ok": False, "error": "Missing entry title"})
return 1
group = resolve_group_by_path(db, entry_input.get("groupPath"))
created = db.add_entry(
group,
title,
normalize_path(entry_input.get("username")),
normalize_path(entry_input.get("password")),
normalize_path(entry_input.get("url")),
normalize_path(entry_input.get("notes")),
)
save_db(db)
emit({"ok": True, "data": entry_to_dict(created)})
return 0
if command == "create-group":
group_input = payload.get("group", {})
name = normalize_path(group_input.get("name"))
if not name:
emit({"ok": False, "error": "Missing group name"})
return 1
parent = resolve_group_by_path(db, group_input.get("path"))
created_group = db.add_group(parent, name)
save_db(db)
emit({"ok": True, "data": group_to_dict(created_group)})
return 0
if command == "save":
save_db(db)
emit({"ok": True, "data": None})
return 0
emit({"ok": False, "error": f"Unknown command: {command}"})
return 1
+18 -1
View File
@@ -25,10 +25,27 @@ export type KeePassFindQuery = {
groupPath?: string;
};
export type KeePassEntryInput = {
title: string;
username?: string;
password?: string;
url?: string;
notes?: string;
groupPath?: string;
};
export type KeePassGroupInput = {
name: string;
path?: string;
};
export type KeePassCommand =
| { command: "list-entries" }
| { command: "find-entries"; query: KeePassFindQuery }
| { command: "list-groups" };
| { command: "list-groups" }
| { command: "create-entry"; entry: KeePassEntryInput }
| { command: "create-group"; group: KeePassGroupInput }
| { command: "save" };
export type KeePassResponse<T> =
| {