Compare commits
9 Commits
0d25e52ebc
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 210f7b414b | |||
| 5fa30414d7 | |||
| ee0e2c85f4 | |||
| 8e990cb1b4 | |||
| 2d444e9a8b | |||
| da0b396bf8 | |||
| 89ba04d61a | |||
| 9496c07049 | |||
| 4cb568c326 |
+3
-1
@@ -30,4 +30,6 @@ __pycache__/
|
|||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
# Local project memory/state
|
# Local project memory/state
|
||||||
.memory/
|
# Keep memory files tracked
|
||||||
|
!.memory/
|
||||||
|
!.memory/**
|
||||||
|
|||||||
+43
-49
@@ -1,58 +1,52 @@
|
|||||||
# kdbx-lib
|
# Project
|
||||||
|
|
||||||
TypeScript wrapper around `pykeepass` for read-only access to KeePass `.kdbx` files.
|
## Goal
|
||||||
|
Provide a TypeScript wrapper around KeePass `.kdbx` databases using a Python bridge powered by `pykeepass`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
- Public API is TypeScript.
|
||||||
|
- `src/python/bridge.py` is the runtime backend and uses `pykeepass`.
|
||||||
|
- TypeScript spawns a Python process per request; there is no persistent worker yet.
|
||||||
|
- JSON is exchanged over stdin/stdout.
|
||||||
|
- Bridge errors, empty output, invalid JSON, missing files, and backend exceptions are surfaced as TypeScript errors.
|
||||||
|
- Bridge error reporting now distinguishes invalid KeePass requests from backend errors.
|
||||||
|
- Coverage now includes `keyFile` payload propagation, nested group payload shaping, and core API smoke checks.
|
||||||
|
|
||||||
- Public API: TypeScript
|
## Public API
|
||||||
- Runtime backend: Python 3
|
- `openKeePassDatabase(path, options)`
|
||||||
- Bridge: `src/python/bridge.py`
|
- `KeePassDatabase.listEntries()`
|
||||||
- Transport: JSON over stdin/stdout
|
- `KeePassDatabase.findEntries(query)`
|
||||||
- Backend library: `pykeepass`
|
- `KeePassDatabase.listGroups()`
|
||||||
|
- `KeePassDatabase.createEntry(entry)`
|
||||||
|
- `KeePassDatabase.createGroup(group)`
|
||||||
|
- `KeePassDatabase.save()`
|
||||||
|
- `KeePassDatabase.close()` is a no-op.
|
||||||
|
|
||||||
## Requirements
|
## Types
|
||||||
|
- Entries expose: `title`, `username`, `password`, `url`, `notes`, optional `groupPath`, optional `otp`.
|
||||||
|
- Groups expose: `name`, `path`.
|
||||||
|
- Open options support `password` and optional `keyFile`.
|
||||||
|
- Find queries support partial matching on `title`, `username`, `url`, and `groupPath`.
|
||||||
|
|
||||||
- Node.js or Bun
|
## Runtime details
|
||||||
- Python 3
|
- Python path defaults to `.venv/bin/python3`.
|
||||||
- `pykeepass` installed in the Python environment used by the bridge
|
- It can be overridden with `PYTHON_PATH`.
|
||||||
- A project-local `.venv` works well
|
- `bun run setup:python` creates `.venv` if needed and installs `pykeepass`.
|
||||||
|
- The bridge also works with an existing project-local virtual environment.
|
||||||
|
|
||||||
## Python setup
|
## Fixtures and tests
|
||||||
|
- Bundled fixtures: `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`.
|
||||||
|
- Companion JSON fixture: `tests/fixtures/data.kdbx.json` stores the password and expected content.
|
||||||
|
- Unit tests in `tests/unit/` mock the child process and validate bridge parsing, error handling, command forwarding, and payload shaping.
|
||||||
|
- Integration tests in `tests/integration/` use `data.kdbx` to verify entries, groups, partial search, OTP/TOTP output, write persistence on a temporary copy, and nested group creation when `pykeepass` is installed.
|
||||||
|
- The integration test runner checks for `pykeepass` and skips cleanly when it is unavailable.
|
||||||
|
- Memory tracking files: `.memory/state.md` and `.memory/todo.md`.
|
||||||
|
|
||||||
Install dependencies with:
|
## Main scripts
|
||||||
|
- `bun run test` / `bun test`
|
||||||
|
- `bun run src/example.ts`
|
||||||
|
- `bun run src/test-integration.ts`
|
||||||
|
- `bun run setup:python`
|
||||||
|
|
||||||
```bash
|
## Current direction
|
||||||
bun run setup:python
|
Keep improving failure-path coverage, keep write support minimal and predictable, and continue validating persistence on temporary copies and nested group behavior.
|
||||||
```
|
|
||||||
|
|
||||||
Manual alternative:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m pip install pykeepass
|
|
||||||
```
|
|
||||||
|
|
||||||
## Core behavior
|
|
||||||
|
|
||||||
- Read-only library; it does not modify databases.
|
|
||||||
- `openKeePassDatabase(path, options)` opens a database through the Python bridge.
|
|
||||||
- `listEntries()` returns all entry fields exposed by the bridge: `title`, `username`, `password`, `url`, `notes`, `groupPath`, and `otp` when present.
|
|
||||||
- `findEntries(query)` performs partial matching and returns full entries.
|
|
||||||
- `listGroups()` returns group names and paths.
|
|
||||||
- `close()` is currently a no-op.
|
|
||||||
|
|
||||||
## Fixture facts
|
|
||||||
|
|
||||||
- Bundled fixtures: `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`
|
|
||||||
- Fixture passwords and expected content live in companion JSON files
|
|
||||||
- `data.kdbx` contains four entries: `root`, `otp1`, `f1-item1`, `f2-item1`
|
|
||||||
- The fixture tree is `Racine/ -> root, otp1, Folder1/ -> f1-item1, Folder2/ -> f2-item1`
|
|
||||||
- Integration tests cover entries, groups, and the `otp1` OTP/TOTP value
|
|
||||||
- Canonical OTP value is the full `otpauth://...` URI returned by `pykeepass`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The bridge currently launches a Python process per call; simple but expensive.
|
|
||||||
- Errors from the bridge are propagated to TypeScript, including exit code when available.
|
|
||||||
- The API is still flatter than the real KeePass model.
|
|
||||||
- More failure-path tests are needed.
|
|
||||||
- Future improvement: a persistent Python process if performance becomes important.
|
|
||||||
|
|||||||
+6
-22
@@ -1,24 +1,8 @@
|
|||||||
# State
|
# State
|
||||||
|
|
||||||
## Current focus
|
- Hardened bridge error handling and nested group path resolution.
|
||||||
Read-only TypeScript wrapper around `pykeepass` via a Python JSON bridge.
|
- Added unit coverage for invalid JSON, empty output, nested group path forwarding, and keyFile payloads.
|
||||||
|
- Added integration coverage for creating groups on temporary copies.
|
||||||
## Current API
|
- Latest test run passed: 20 tests, 0 failures.
|
||||||
- `openKeePassDatabase(path, options)`
|
- VS Code red squiggles on Bun/Node imports were addressed by including `bun-types` in `tsconfig.json` and covering `tests/**/*.ts`.
|
||||||
- `listEntries()`
|
- Project renamed to ts-pykeepass-wrapper; current focus remains the TypeScript wrapper + Python bridge for KeePass.
|
||||||
- `findEntries(query)`
|
|
||||||
- `listGroups()`
|
|
||||||
- `close()` is a no-op
|
|
||||||
|
|
||||||
## Runtime model
|
|
||||||
- TypeScript starts the Python bridge
|
|
||||||
- Python uses `pykeepass`
|
|
||||||
- JSON is exchanged over stdin/stdout
|
|
||||||
- Bridge errors and empty/invalid JSON are surfaced to TypeScript
|
|
||||||
|
|
||||||
## Current fixture/test status
|
|
||||||
- Bundled fixtures: `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`
|
|
||||||
- Integration tests validate entries, groups, and OTP/TOTP output for `data.kdbx`
|
|
||||||
|
|
||||||
## Next step
|
|
||||||
Keep tightening failure-path coverage and improve the API shape only if needed.
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Todo
|
||||||
|
|
||||||
|
- Keep write-path behavior predictable and well-documented.
|
||||||
|
- Preserve minimal API surface until update/delete/move is required.
|
||||||
|
- Consider typed bridge errors in TypeScript if more granularity is needed later.
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
# kdbx-lib
|
# ts-pykeepass-wrapper
|
||||||
|
|
||||||
TypeScript wrapper around `pykeepass` for reading KeePass `.kdbx` files.
|
TypeScript wrapper around `pykeepass` for reading and modifying KeePass `.kdbx` files.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This project uses:
|
This project uses:
|
||||||
- TypeScript as the public API
|
- TypeScript as the public API
|
||||||
- Python as the runtime backend
|
- Python as the runtime backend
|
||||||
- `pykeepass` to read KeePass databases
|
- `pykeepass` to read and update KeePass databases
|
||||||
|
|
||||||
The TypeScript layer launches a Python bridge and exchanges JSON through stdin/stdout.
|
The TypeScript layer launches a Python bridge per request and exchanges JSON through stdin/stdout.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js or Bun
|
- Node.js or Bun
|
||||||
- Python 3
|
- Python 3
|
||||||
- `pykeepass` installed in the Python environment used by the bridge (the project provides `bun run setup:python`)
|
- `pykeepass` installed in the Python environment used by the bridge (the project provides `bun run setup:python`)
|
||||||
|
- The bridge defaults to `.venv/bin/python3`, or you can override with `PYTHON_PATH`
|
||||||
|
|
||||||
## Python setup
|
## Python setup
|
||||||
|
|
||||||
@@ -28,10 +29,10 @@ bun run setup:python
|
|||||||
If you prefer manual installation:
|
If you prefer manual installation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m pip install pykeepass
|
python3 -m venv .venv && .venv/bin/pip install pykeepass
|
||||||
```
|
```
|
||||||
|
|
||||||
The bridge also works with a project-local virtual environment such as `.venv` if you want to pin Python dependencies.
|
The bridge also works with a project-local virtual environment such as `.venv`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ bun run src/example.ts
|
|||||||
Creates a database wrapper.
|
Creates a database wrapper.
|
||||||
|
|
||||||
#### Options
|
#### Options
|
||||||
- `password`: KeePass master password, read from the fixture JSON in examples/tests when applicable
|
- `password`: KeePass master password
|
||||||
- `keyFile`: optional key file path
|
- `keyFile`: optional key file path
|
||||||
|
|
||||||
### `listEntries()`
|
### `listEntries()`
|
||||||
@@ -73,6 +74,31 @@ Finds entries by partial match and returns the full entry objects.
|
|||||||
### `listGroups()`
|
### `listGroups()`
|
||||||
Returns all groups, including their names and paths.
|
Returns all groups, including their names and paths.
|
||||||
|
|
||||||
|
### `createEntry(entry)`
|
||||||
|
Creates a new entry in the target database and persists it immediately.
|
||||||
|
|
||||||
|
#### Entry input
|
||||||
|
- `title`: entry title
|
||||||
|
- `username`: optional username
|
||||||
|
- `password`: optional password
|
||||||
|
- `url`: optional URL
|
||||||
|
- `notes`: optional notes
|
||||||
|
- `groupPath`: optional target group path
|
||||||
|
|
||||||
|
`groupPath` is resolved as an existing group path when possible. Nested paths such as `Folder1/SubFolder` are supported when the target group exists.
|
||||||
|
|
||||||
|
### `createGroup(group)`
|
||||||
|
Creates a new group and persists it immediately.
|
||||||
|
|
||||||
|
#### Group input
|
||||||
|
- `name`: group name
|
||||||
|
- `path`: optional parent group path
|
||||||
|
|
||||||
|
`path` is resolved as an existing parent group path when possible, including nested paths.
|
||||||
|
|
||||||
|
### `save()`
|
||||||
|
Persists the current database state.
|
||||||
|
|
||||||
### `close()`
|
### `close()`
|
||||||
No-op for now.
|
No-op for now.
|
||||||
|
|
||||||
@@ -80,7 +106,8 @@ No-op for now.
|
|||||||
|
|
||||||
- The bridge currently launches a Python process per call.
|
- The bridge currently launches a Python process per call.
|
||||||
- This is simple and robust for a first version.
|
- This is simple and robust for a first version.
|
||||||
- Errors from the Python bridge are propagated to the TypeScript API with the bridge exit code when available.
|
- Errors from the Python bridge are propagated to the TypeScript API, including invalid or empty output. Bridge failures are normalized to distinguish invalid requests and backend errors.
|
||||||
- A persistent Python process can be added later if needed.
|
- A persistent Python process can be added later if needed.
|
||||||
- Bundled fixtures include `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`; their companion JSON files store the password and expected content for tests/examples.
|
- Write operations currently open, modify, and save the database per command.
|
||||||
- Integration tests validate the bundled `data.kdbx` entry-by-entry and group-by-group against `tests/fixtures/data.kdbx.json`.
|
- Bundled fixtures include `tests/fixtures/data.kdbx` and `tests/fixtures/empty.kdbx`; the companion JSON file stores the password and expected content for tests/examples.
|
||||||
|
- Integration tests validate the bundled `data.kdbx` entry-by-entry and group-by-group against `tests/fixtures/data.kdbx.json`, and may skip cleanly when `pykeepass` is unavailable.
|
||||||
|
|||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
current_branch=$(git branch --show-current)
|
||||||
|
|
||||||
|
if [ -z "$current_branch" ]; then
|
||||||
|
echo "Erreur : impossible de détecter la branche courante."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$current_branch" = "main" ]; then
|
||||||
|
echo "Erreur : ce script ne peut pas être exécuté depuis la branche main."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
start_branch="$current_branch"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
if [ "$(git branch --show-current)" != "$start_branch" ]; then
|
||||||
|
git checkout "$start_branch" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
merge_or_fail() {
|
||||||
|
source_branch="$1"
|
||||||
|
target_branch="$2"
|
||||||
|
|
||||||
|
if ! git merge "$source_branch"; then
|
||||||
|
echo "Erreur : conflit lors du merge de $source_branch vers $target_branch."
|
||||||
|
echo "Résolvez les conflits manuellement, puis relancez le script."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "Branche courante détectée : $current_branch"
|
||||||
|
read -r -p "Confirmer l'exécution sur cette branche ? [y/N] " confirm
|
||||||
|
case "$confirm" in
|
||||||
|
y|Y|yes|YES)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Annulé."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$current_branch" != "dev" ]; then
|
||||||
|
git checkout dev
|
||||||
|
merge_or_fail "$current_branch" "dev"
|
||||||
|
git push
|
||||||
|
current_branch="dev"
|
||||||
|
fi
|
||||||
|
|
||||||
|
git checkout main
|
||||||
|
merge_or_fail dev main
|
||||||
|
git push
|
||||||
|
git checkout dev
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "kdbx-lib",
|
"name": "ts-pykeepass-wrapper",
|
||||||
"packageManager": "bun@1.0.0",
|
"packageManager": "bun@1.0.0",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:unit": "bun test",
|
"test:unit": "bun test",
|
||||||
"test:integration": "bun run src/test-integration.ts",
|
"test:integration": "bun run src/test-integration.ts",
|
||||||
"setup:python": "python3 -m pip install pykeepass"
|
"setup:python": "test -x .venv/bin/python3 || python3 -m venv .venv && .venv/bin/pip install pykeepass"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+28
-16
@@ -3,18 +3,20 @@ import { fileURLToPath } from "node:url";
|
|||||||
import type {
|
import type {
|
||||||
KeePassCommand,
|
KeePassCommand,
|
||||||
KeePassEntry,
|
KeePassEntry,
|
||||||
|
KeePassEntryInput,
|
||||||
KeePassFindQuery,
|
KeePassFindQuery,
|
||||||
KeePassGroup,
|
KeePassGroup,
|
||||||
|
KeePassGroupInput,
|
||||||
KeePassOpenOptions,
|
KeePassOpenOptions,
|
||||||
KeePassResponse,
|
KeePassResponse,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export class KeePassDatabase {
|
export class KeePassDatabase {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly path: string,
|
private path: string,
|
||||||
private readonly options: KeePassOpenOptions,
|
private options: KeePassOpenOptions,
|
||||||
private readonly pythonPath = "python3",
|
private pythonPath = process.env.PYTHON_PATH ?? ".venv/bin/python3",
|
||||||
private readonly bridgePath = new URL("./python/bridge.py", import.meta.url)
|
private bridgePath = new URL("./python/bridge.py", import.meta.url)
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async listEntries(): Promise<KeePassEntry[]> {
|
async listEntries(): Promise<KeePassEntry[]> {
|
||||||
@@ -32,8 +34,19 @@ export class KeePassDatabase {
|
|||||||
return response;
|
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> {
|
async close(): Promise<void> {
|
||||||
// No persistent process is kept alive yet.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,19 +60,23 @@ export class KeePassDatabase {
|
|||||||
|
|
||||||
const bridgeFile = fileURLToPath(this.bridgePath);
|
const bridgeFile = fileURLToPath(this.bridgePath);
|
||||||
const result = await new Promise<{ stdout: string; stderr: string; code: number }>((resolve, reject) => {
|
const result = await new Promise<{ stdout: string; stderr: string; code: number }>((resolve, reject) => {
|
||||||
const child = spawn(this.pythonPath, [bridgeFile], {
|
const child = spawn(this.pythonPath, [bridgeFile], { stdio: ["pipe", "pipe", "pipe"] });
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
let stdout = "";
|
let stdout = "";
|
||||||
let stderr = "";
|
let stderr = "";
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
|
||||||
|
const fail = (error: unknown) => {
|
||||||
|
if (!settled) {
|
||||||
|
settled = true;
|
||||||
|
reject(error instanceof Error ? error : new Error(String(error)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
child.stdin.write(payload);
|
child.stdin.write(payload);
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error);
|
fail(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
child.stdout.on("data", (chunk) => {
|
child.stdout.on("data", (chunk) => {
|
||||||
@@ -70,12 +87,7 @@ export class KeePassDatabase {
|
|||||||
stderr += chunk.toString();
|
stderr += chunk.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("error", (error) => {
|
child.on("error", fail);
|
||||||
if (!settled) {
|
|
||||||
settled = true;
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
if (!settled) {
|
if (!settled) {
|
||||||
|
|||||||
+75
-1
@@ -60,6 +60,41 @@ def load_db(db_path, password, key_file=None):
|
|||||||
return PyKeePass(db_path, password=password)
|
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]
|
||||||
|
|
||||||
|
segments = [segment for segment in normalized.strip("/").split("/") if segment]
|
||||||
|
matching_groups = db.find_groups(name=segments[-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():
|
def read_payload():
|
||||||
if len(sys.argv) > 1 and sys.argv[1].strip():
|
if len(sys.argv) > 1 and sys.argv[1].strip():
|
||||||
return json.loads(sys.argv[1])
|
return json.loads(sys.argv[1])
|
||||||
@@ -123,11 +158,50 @@ def main():
|
|||||||
emit({"ok": True, "data": groups})
|
emit({"ok": True, "data": groups})
|
||||||
return 0
|
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}"})
|
emit({"ok": False, "error": f"Unknown command: {command}"})
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
except ValueError as exc:
|
||||||
|
emit({"ok": False, "error": f"Invalid KeePass request: {exc}"})
|
||||||
|
return 1
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
emit({"ok": False, "error": str(exc)})
|
emit({"ok": False, "error": f"KeePass backend error: {exc}"})
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+18
-1
@@ -25,10 +25,27 @@ export type KeePassFindQuery = {
|
|||||||
groupPath?: string;
|
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 =
|
export type KeePassCommand =
|
||||||
| { command: "list-entries" }
|
| { command: "list-entries" }
|
||||||
| { command: "find-entries"; query: KeePassFindQuery }
|
| { 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> =
|
export type KeePassResponse<T> =
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import { readFile } from "node:fs/promises";
|
import { copyFile, readFile, rm } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import { openKeePassDatabase } from "../../src/keepass";
|
import { openKeePassDatabase } from "../../src/keepass";
|
||||||
|
|
||||||
type FixtureEntry = {
|
type FixtureEntry = {
|
||||||
@@ -25,8 +28,19 @@ type FixtureData = {
|
|||||||
const FIXTURE_PATH = "tests/fixtures/data.kdbx";
|
const FIXTURE_PATH = "tests/fixtures/data.kdbx";
|
||||||
const FIXTURE_DATA_PATH = "tests/fixtures/data.kdbx.json";
|
const FIXTURE_DATA_PATH = "tests/fixtures/data.kdbx.json";
|
||||||
|
|
||||||
|
async function withTempCopy<T>(filePath: string, fn: (tempPath: string) => Promise<T>): Promise<T> {
|
||||||
|
const tempPath = join(tmpdir(), `ts-pykeepass-wrapper-${randomUUID()}.kdbx`);
|
||||||
|
await copyFile(filePath, tempPath);
|
||||||
|
try {
|
||||||
|
return await fn(tempPath);
|
||||||
|
} finally {
|
||||||
|
await rm(tempPath, { force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensurePyKeePass(): Promise<boolean> {
|
async function ensurePyKeePass(): Promise<boolean> {
|
||||||
const child = Bun.spawn(["python3", "-c", "import pykeepass; print('ok')"], {
|
const python = process.env.PYTHON_PATH ?? ".venv/bin/python3";
|
||||||
|
const child = Bun.spawn([python, "-c", "import pykeepass; print('ok')"], {
|
||||||
stdout: "pipe",
|
stdout: "pipe",
|
||||||
stderr: "pipe",
|
stderr: "pipe",
|
||||||
});
|
});
|
||||||
@@ -95,7 +109,7 @@ test("lists the groups from the bundled data fixture", async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const db = openKeePassDatabase(FIXTURE_PATH, { password });
|
const db = openKeePassDatabase(FIXTURE_PATH, { password });
|
||||||
const groups = await db.listGroups();
|
const groups = await db.listGroups();
|
||||||
expect(groups).toEqual([
|
expect(groups).toEqual([
|
||||||
{ name: "Racine", path: "" },
|
{ name: "Racine", path: "" },
|
||||||
{ name: "Folder1", path: "Folder1" },
|
{ name: "Folder1", path: "Folder1" },
|
||||||
@@ -124,6 +138,56 @@ test("finds entries in the bundled data fixture", async () => {
|
|||||||
expect(entries[0]?.groupPath).toBe("Folder1");
|
expect(entries[0]?.groupPath).toBe("Folder1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("creates entries in a temporary copy of the bundled fixture and persists them", async () => {
|
||||||
|
const [{ password }, pykeepassReady] = await Promise.all([
|
||||||
|
readFile(FIXTURE_DATA_PATH, "utf8").then((raw) => JSON.parse(raw) as FixtureData),
|
||||||
|
ensurePyKeePass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!pykeepassReady) {
|
||||||
|
console.log("Skipping integration test: pykeepass is not installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withTempCopy(FIXTURE_PATH, async (tempPath) => {
|
||||||
|
const db = openKeePassDatabase(tempPath, { password });
|
||||||
|
const createdEntry = await db.createEntry({
|
||||||
|
title: "TempEntry",
|
||||||
|
username: "temp-user",
|
||||||
|
password: "temp-pass",
|
||||||
|
});
|
||||||
|
expect(createdEntry.title).toBe("TempEntry");
|
||||||
|
expect(createdEntry.username).toBe("temp-user");
|
||||||
|
|
||||||
|
const persisted = await db.findEntries({ title: "TempEntry" });
|
||||||
|
expect(persisted).toHaveLength(1);
|
||||||
|
expect(persisted[0]?.username).toBe("temp-user");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("creates nested groups on a temporary copy", async () => {
|
||||||
|
const [{ password }, pykeepassReady] = await Promise.all([
|
||||||
|
readFile(FIXTURE_DATA_PATH, "utf8").then((raw) => JSON.parse(raw) as FixtureData),
|
||||||
|
ensurePyKeePass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!pykeepassReady) {
|
||||||
|
console.log("Skipping integration test: pykeepass is not installed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withTempCopy(FIXTURE_PATH, async (tempPath) => {
|
||||||
|
const db = openKeePassDatabase(tempPath, { password });
|
||||||
|
const createdGroup = await db.createGroup({ name: "Nested", path: "Folder1" });
|
||||||
|
expect(createdGroup.path).toBe("Folder1/Nested");
|
||||||
|
|
||||||
|
const groups = await db.listGroups();
|
||||||
|
expect(groups.some((group) => group.path === "Folder1/Nested")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
test("uses the JSON fixture content as the source of truth for expectations", async () => {
|
test("uses the JSON fixture content as the source of truth for expectations", async () => {
|
||||||
const { content } = JSON.parse(await readFile(FIXTURE_DATA_PATH, "utf8")) as FixtureData;
|
const { content } = JSON.parse(await readFile(FIXTURE_DATA_PATH, "utf8")) as FixtureData;
|
||||||
|
|
||||||
@@ -145,4 +209,3 @@ test("uses the JSON fixture content as the source of truth for expectations", as
|
|||||||
"\t\t- f2-item1",
|
"\t\t- f2-item1",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+234
-26
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import { KeePassDatabase } from "../../src/keepass";
|
import { KeePassDatabase, openKeePassDatabase } from "../../src/keepass";
|
||||||
|
|
||||||
const spawnMock = mock(() => {
|
const spawnMock = mock(() => {
|
||||||
throw new Error("spawn should be mocked per test");
|
throw new Error("spawn should be mocked per test");
|
||||||
@@ -9,33 +9,23 @@ mock.module("node:child_process", () => ({
|
|||||||
spawn: spawnMock,
|
spawn: spawnMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function mockSuccessfulBridgeResponse(data: unknown) {
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: { write: () => undefined, end: () => undefined },
|
||||||
|
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data })) },
|
||||||
|
stderr: { on: () => undefined },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(0));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("KeePassDatabase", () => {
|
describe("KeePassDatabase", () => {
|
||||||
test("listEntries parses successful bridge response", async () => {
|
test("listEntries parses successful bridge response", async () => {
|
||||||
spawnMock.mockImplementation(() => {
|
mockSuccessfulBridgeResponse([{ title: "Entry" }]);
|
||||||
const listeners: Record<string, ((chunk: Buffer | string) => void)[]> = {};
|
|
||||||
const child = {
|
|
||||||
stdin: {
|
|
||||||
write: () => undefined,
|
|
||||||
end: () => {
|
|
||||||
listeners.close?.forEach((cb) => cb(Buffer.from("")));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stdout: {
|
|
||||||
on: (event: string, cb: (chunk: Buffer | string) => void) => {
|
|
||||||
listeners[event] ??= [];
|
|
||||||
listeners[event].push(cb);
|
|
||||||
if (event === "data") {
|
|
||||||
cb(JSON.stringify({ ok: true, data: [{ title: "Entry" }] }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
stderr: { on: () => undefined },
|
|
||||||
on: (event: string, cb: (code?: number | null) => void) => {
|
|
||||||
if (event === "close") queueMicrotask(() => cb(0));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return child as never;
|
|
||||||
});
|
|
||||||
|
|
||||||
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
const entries = await db.listEntries();
|
const entries = await db.listEntries();
|
||||||
@@ -60,4 +50,222 @@ describe("KeePassDatabase", () => {
|
|||||||
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
await expect(db.listEntries()).rejects.toThrow("boom");
|
await expect(db.listEntries()).rejects.toThrow("boom");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("throws on empty bridge output", async () => {
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: { write: () => undefined, end: () => undefined },
|
||||||
|
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(" ") },
|
||||||
|
stderr: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb("bridge failed") },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(1));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await expect(db.listEntries()).rejects.toThrow("bridge failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on invalid JSON output", async () => {
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: { write: () => undefined, end: () => undefined },
|
||||||
|
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb("not json") },
|
||||||
|
stderr: { on: () => undefined },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(0));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await expect(db.listEntries()).rejects.toThrow("Invalid JSON from Python bridge");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws a useful error when the bridge exits without output", async () => {
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: { write: () => undefined, end: () => undefined },
|
||||||
|
stdout: { on: () => undefined },
|
||||||
|
stderr: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb("bridge crashed") },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(1));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await expect(db.listEntries()).rejects.toThrow("bridge crashed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws on spawn error", async () => {
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: { write: () => undefined, end: () => undefined },
|
||||||
|
stdout: { on: () => undefined },
|
||||||
|
stderr: { on: () => undefined },
|
||||||
|
on: (event: string, cb: (error: Error) => void) => {
|
||||||
|
if (event === "error") queueMicrotask(() => cb(new Error("spawn failed")));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await expect(db.listEntries()).rejects.toThrow("spawn failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createEntry forwards the create-entry command", async () => {
|
||||||
|
mockSuccessfulBridgeResponse({ title: "New" });
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
const created = await db.createEntry({ title: "New" });
|
||||||
|
|
||||||
|
expect(created).toEqual({ title: "New" });
|
||||||
|
expect(spawnMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createGroup forwards the create-group command", async () => {
|
||||||
|
mockSuccessfulBridgeResponse({ name: "Folder", path: "" });
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
const created = await db.createGroup({ name: "Folder" });
|
||||||
|
|
||||||
|
expect(created).toEqual({ name: "Folder", path: "" });
|
||||||
|
expect(spawnMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("createEntry forwards nested group paths in the payload", async () => {
|
||||||
|
let payload = "";
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
payload += chunk;
|
||||||
|
},
|
||||||
|
end: () => undefined,
|
||||||
|
},
|
||||||
|
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data: { title: "New" } })) },
|
||||||
|
stderr: { on: () => undefined },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(0));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await db.createEntry({ title: "New", groupPath: "Folder/SubFolder" });
|
||||||
|
|
||||||
|
expect(JSON.parse(payload)).toMatchObject({
|
||||||
|
command: "create-entry",
|
||||||
|
entry: { title: "New", groupPath: "Folder/SubFolder" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save forwards the save command", async () => {
|
||||||
|
mockSuccessfulBridgeResponse(null);
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await expect(db.save()).resolves.toBeUndefined();
|
||||||
|
expect(spawnMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("findEntries forwards the query payload", async () => {
|
||||||
|
let payload = "";
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
payload += chunk;
|
||||||
|
},
|
||||||
|
end: () => undefined,
|
||||||
|
},
|
||||||
|
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data: [] })) },
|
||||||
|
stderr: { on: () => undefined },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(0));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await db.findEntries({ title: "abc", username: "u", url: "https://x", groupPath: "Folder" });
|
||||||
|
|
||||||
|
expect(JSON.parse(payload)).toMatchObject({
|
||||||
|
command: "find-entries",
|
||||||
|
path: "db.kdbx",
|
||||||
|
password: "secret",
|
||||||
|
query: { title: "abc", username: "u", url: "https://x", groupPath: "Folder" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("listGroups forwards the list-groups command", async () => {
|
||||||
|
let payload = "";
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
payload += chunk;
|
||||||
|
},
|
||||||
|
end: () => undefined,
|
||||||
|
},
|
||||||
|
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data: [] })) },
|
||||||
|
stderr: { on: () => undefined },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(0));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await db.listGroups();
|
||||||
|
|
||||||
|
expect(JSON.parse(payload)).toMatchObject({
|
||||||
|
command: "list-groups",
|
||||||
|
path: "db.kdbx",
|
||||||
|
password: "secret",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes keyFile in the bridge payload", async () => {
|
||||||
|
let payload = "";
|
||||||
|
spawnMock.mockImplementation(() => {
|
||||||
|
const child = {
|
||||||
|
stdin: {
|
||||||
|
write: (chunk: string) => {
|
||||||
|
payload += chunk;
|
||||||
|
},
|
||||||
|
end: () => undefined,
|
||||||
|
},
|
||||||
|
stdout: { on: (_event: string, cb: (chunk: Buffer | string) => void) => cb(JSON.stringify({ ok: true, data: [] })) },
|
||||||
|
stderr: { on: () => undefined },
|
||||||
|
on: (event: string, cb: (code?: number | null) => void) => {
|
||||||
|
if (event === "close") queueMicrotask(() => cb(0));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return child as never;
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = new KeePassDatabase("db.kdbx", { password: "secret", keyFile: "keyfile.key" }, "python3", new URL("file:///tmp/bridge.py"));
|
||||||
|
await db.listEntries();
|
||||||
|
|
||||||
|
expect(JSON.parse(payload)).toMatchObject({
|
||||||
|
path: "db.kdbx",
|
||||||
|
password: "secret",
|
||||||
|
keyFile: "keyfile.key",
|
||||||
|
command: "list-entries",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("openKeePassDatabase returns a KeePassDatabase instance", () => {
|
||||||
|
const db = openKeePassDatabase("db.kdbx", { password: "secret" });
|
||||||
|
expect(db).toBeInstanceOf(KeePassDatabase);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-2
@@ -8,8 +8,9 @@
|
|||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
|
"types": ["bun-types"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user