feat: add write support for KeePass entries and groups
This commit is contained in:
+18
-5
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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> =
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user