Add vault-backed Karakeep API helper

This commit is contained in:
Fizzlepoof
2026-05-15 17:44:30 +00:00
parent 308ac205fb
commit 3aad8d037b
2 changed files with 155 additions and 0 deletions

View File

@@ -12,6 +12,22 @@ This directory includes repo-side helpers for homelab operational tasks.
See `docs/operations/KITCHENOWL_RECIPE_IMPORT.md` for setup and usage.
## Karakeep API helper
- `bin/karakeep_api.py` — tiny Karakeep REST helper backed by the local encrypted vault entry
- reads `.secrets/karakeep_secrets.json.enc`
- uses `baseUrl` + `apiKey`
- targets `/api/v1/...` routes by default
- supports ad hoc GET/POST/PATCH/DELETE calls with query/body args
Examples:
```bash
automation/bin/karakeep_api.py bookmarks --query limit=5 --pretty
automation/bin/karakeep_api.py tags --pretty
automation/bin/karakeep_api.py /api/health --raw-path --pretty
```
## Backup scripts
- `bin/pd_backup_postgres.sh` — creates gzipped `pg_dump` exports for the configured Postgres DB list (quote space-separated DB names in `.env`, e.g. `POSTGRES_DB_LIST="n8n paperless"`)

139
automation/bin/karakeep_api.py Executable file
View File

@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Small Karakeep API helper that reads its token from the local encrypted vault.
Default behavior:
- reads `.secrets/karakeep_secrets.json.enc`
- uses `baseUrl` + `apiKey`
- targets `/api/v1/...` routes unless an absolute path is given
Examples:
automation/bin/karakeep_api.py bookmarks --pretty
automation/bin/karakeep_api.py tags --pretty
automation/bin/karakeep_api.py /api/health --raw-path --pretty
automation/bin/karakeep_api.py bookmarks --query limit=5 --pretty
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any
from urllib import error, parse, request
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent.parent
WORKSPACE = Path('/home/fizzlepoof/.openclaw/workspace')
DEFAULT_SECRETS_DIR = WORKSPACE / '.secrets'
DEFAULT_SECRET_FILE = DEFAULT_SECRETS_DIR / 'karakeep_secrets.json.enc'
DEFAULT_KEY_FILE = DEFAULT_SECRETS_DIR / 'encryption.key'
DEFAULT_TIMEOUT = 30
def decrypt_json(path: Path, key_file: Path) -> dict[str, Any]:
raw = subprocess.check_output([
'openssl', 'enc', '-d', '-aes-256-cbc', '-pbkdf2',
'-in', str(path),
'-pass', f'file:{key_file}',
])
return json.loads(raw)
def parse_kv(items: list[str]) -> dict[str, str]:
out: dict[str, str] = {}
for item in items:
if '=' not in item:
raise SystemExit(f'Expected key=value, got: {item}')
key, value = item.split('=', 1)
out[key] = value
return out
def build_url(base_url: str, endpoint: str, raw_path: bool) -> str:
base = base_url.rstrip('/')
if raw_path:
if not endpoint.startswith('/'):
endpoint = '/' + endpoint
return base + endpoint
endpoint = endpoint.strip('/')
return f'{base}/api/v1/{endpoint}'
def api_request(method: str, url: str, api_key: str, *, query: dict[str, str] | None = None,
body: dict[str, Any] | list[Any] | None = None, timeout: int = DEFAULT_TIMEOUT) -> tuple[int, Any, dict[str, str]]:
if query:
url += ('&' if '?' in url else '?') + parse.urlencode(query, doseq=True)
data = None
headers = {
'Authorization': f'Bearer {api_key}',
'Accept': 'application/json',
'User-Agent': 'doris-karakeep-helper/1.0',
}
if body is not None:
data = json.dumps(body).encode('utf-8')
headers['Content-Type'] = 'application/json'
req = request.Request(url, data=data, method=method.upper(), headers=headers)
try:
with request.urlopen(req, timeout=timeout) as resp:
raw = resp.read().decode('utf-8', 'replace')
ctype = resp.headers.get('Content-Type', '')
payload = json.loads(raw) if 'json' in ctype else raw
return resp.status, payload, dict(resp.headers.items())
except error.HTTPError as exc:
raw = exc.read().decode('utf-8', 'replace')
try:
payload = json.loads(raw)
except Exception:
payload = raw
return exc.code, payload, dict(exc.headers.items())
def main() -> int:
parser = argparse.ArgumentParser(description='Query Karakeep using the encrypted local vault secret.')
parser.add_argument('endpoint', help='Endpoint alias/path. Default base is /api/v1/. Example: bookmarks, tags, lists')
parser.add_argument('--method', default='GET', help='HTTP method (default: GET)')
parser.add_argument('--query', action='append', default=[], metavar='KEY=VALUE', help='Query params')
parser.add_argument('--data', action='append', default=[], metavar='KEY=VALUE', help='JSON body fields')
parser.add_argument('--json-body', help='Raw JSON body string')
parser.add_argument('--raw-path', action='store_true', help='Use endpoint as-is instead of prefixing /api/v1/')
parser.add_argument('--pretty', action='store_true', help='Pretty-print JSON output')
parser.add_argument('--headers', action='store_true', help='Print response headers to stderr')
parser.add_argument('--timeout', type=int, default=DEFAULT_TIMEOUT)
parser.add_argument('--secret-file', default=str(DEFAULT_SECRET_FILE))
parser.add_argument('--key-file', default=str(DEFAULT_KEY_FILE))
args = parser.parse_args()
secret = decrypt_json(Path(args.secret_file), Path(args.key_file))
base_url = secret['baseUrl']
api_key = secret['apiKey']
query = parse_kv(args.query)
body: dict[str, Any] | list[Any] | None = None
if args.json_body:
body = json.loads(args.json_body)
elif args.data:
body = parse_kv(args.data)
url = build_url(base_url, args.endpoint, args.raw_path)
status, payload, headers = api_request(args.method, url, api_key, query=query, body=body, timeout=args.timeout)
if args.headers:
for k, v in headers.items():
print(f'{k}: {v}', file=sys.stderr)
if isinstance(payload, (dict, list)):
if args.pretty:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
print(json.dumps(payload))
else:
print(payload)
return 0 if 200 <= status < 300 else 1
if __name__ == '__main__':
raise SystemExit(main())