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.
92 lines
4.7 KiB
Markdown
92 lines
4.7 KiB
Markdown
# 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`.
|