Why: the set has to be installable and publishable, not just buildable, and the next person needs the reasoning, not only the code. What: - android/: Kotlin client with no third-party dependencies. A DocumentsProvider publishes the mirrored databases to every file picker, so KeePassDX opens them as ordinary documents; closing a file it wrote triggers a sync. A foreground service holds the event stream open. Mirror.decide transcribes the Go merge table, so both platforms resolve a conflict identically. - Makefile as the single entrypoint, with file targets so only what changed is rebuilt: host and cross builds, both .deb packages, the macOS DMG with its /Applications symlink, the Windows zip, the APK under its F-Droid publishing name, and manual-only android-publish. - versions.mk holds one version per platform plus the Android versionCode; `make bump` and `make changelog` move them. Debian changelog is tracked. - systemd units (hardened system unit for the server, user unit for the agent), macOS bundle and launch agent, Windows Task Scheduler job. - docs/HANDOVER.md on architecture and every decision that shaped it, docs/PROTOCOL.md for the API, README for the quick start. Proved: make check and make test-race both pass; built both .deb packages and inspected their contents, built the Windows zip, cross-compiled all five desktop targets, and exercised make bump. The Android module is written but not compiled: no JDK or Android SDK in this environment, which HANDOVER.md states plainly.
4.7 KiB
KeepOrg HTTP API
Every /api/ endpoint takes and returns JSON and needs
Authorization: Bearer <token>, except the login, device and invite
endpoints. Errors carry {"code": "...", "error": "..."}; code is stable
and meant to be matched on, error is for humans.
Three endpoints serve HTML, because a browser is the right place for them:
/device, /invite/{token} and /oidc/callback.
Authentication
| Method | Path | Body | Answer |
|---|---|---|---|
| POST | /api/v1/auth/login |
{username, password, totp, label} |
{token, expires_at, user}; 401 totp_required when a second factor is enrolled and missing, 401 totp_invalid, 401 bad_credentials |
| POST | /api/v1/auth/logout |
– | revokes the calling token |
| GET | /api/v1/auth/whoami |
– | the account and its group memberships |
| POST | /api/v1/auth/totp/enroll |
– | {secret, uri}; not active until confirmed |
| POST | /api/v1/auth/totp/confirm |
{code} |
{totp_enabled: true} |
| POST | /api/v1/auth/device/start |
{label} |
{device_code, user_code, verification_url, interval, expires_in} |
| POST | /api/v1/auth/device/poll |
{device_code} |
202 {status: "pending"}, then 200 {token, ...} once; 410 expired |
A device code is single use: the first successful poll consumes it.
Synchronization
| Method | Path | Notes |
|---|---|---|
| GET | /api/v1/databases |
{databases: [{id, name, perm, merge_strategy, version, sha256, size, updated_at, updated_by}]} |
| GET | /api/v1/databases/{id}/blob |
bytes; ETag: "<version>", X-KeepOrg-SHA256; 204 when the database has no content yet; ?version=<n> reads a stored older version |
| PUT | /api/v1/databases/{id}/blob |
body is the KDBX file. If-Match is required |
| GET | /api/v1/events |
SSE. Accepts ?access_token= because an event stream cannot always set headers |
PUT semantics
If-Match carries the version the edit was based on, or * to force.
| Condition | Status | Body |
|---|---|---|
If-Match missing |
428 precondition_required |
– |
| version matches, bytes differ | 200 |
{version, sha256, unchanged: false} |
| version matches, bytes identical | 200 |
{version, sha256, unchanged: true} |
| version stale | 409 conflict |
{current_version, merge_strategy}, plus ETag and X-KeepOrg-Strategy |
If-Match: *, strategy is client-wins |
200 |
the server copy is replaced |
If-Match: *, any other strategy |
409 force_not_allowed |
– |
| read-only access | 403 forbidden |
– |
| no access | 404 not_found |
a database you cannot read does not exist to you |
larger than max_db_size_mb |
413 too_large |
– |
X-KeepOrg-SHA256 on a PUT is optional; when present it must match the
body or the request is rejected with 400 checksum_mismatch.
Events
event: db-updated
data: {"type":"db-updated","database_id":"...","name":"Team","version":7,"sha256":"...","by":"alice"}
event: db-list-changed
data: {"type":"db-list-changed","database_id":"..."}
db-updated follows an accepted push; db-list-changed follows a grant,
rename, strategy change or deletion. A comment line (: keepalive) arrives
every 25 seconds. Reconnect on any error and reconcile fully; events are
notifications, never state.
Administration
Permission needed is in the last column. "Group admin" means administering the group named or any group above it.
| Method | Path | Body / query | Needs |
|---|---|---|---|
| GET | /api/v1/groups |
– | any account; lists what you can see |
| POST | /api/v1/groups |
{name, parent_id} |
group admin of the parent; server admin for a root group |
| DELETE | /api/v1/groups/{id} |
– | group admin; refuses while it has children, members or databases |
| POST | /api/v1/groups/{id}/members |
{username or user_id, role} |
group admin |
| DELETE | /api/v1/groups/{id}/members/{user} |
– | group admin |
| POST | /api/v1/databases |
{name, owner_group_id, merge_strategy} |
group admin of the owner |
| PATCH | /api/v1/databases/{id} |
{name?, merge_strategy?} |
database admin |
| DELETE | /api/v1/databases/{id} |
– | database admin; deletes every stored version |
| POST | /api/v1/grants |
{group_id, database_id, perm} |
database admin |
| DELETE | /api/v1/grants |
?group_id=&database_id= |
database admin |
| POST | /api/v1/invites |
{group_id, role, ttl_hours, max_uses} |
group admin |
| GET | /api/v1/invites |
– | your own invites, and those of groups you administer |
| GET | /api/v1/users |
– | yourself, plus members of groups you administer |
| GET | /healthz |
– | unauthenticated liveness |
merge_strategy is one of server-wins, client-wins, manual. perm is
one of read, write, admin. role is member or admin.