Backend API¶
All endpoints are served under /api/ by a single NinjaAPI declared in backend/api/api.py. Authentication is a bearer token (Authorization: Bearer <token>) issued at enrolment. Request and response shapes live in backend/api/schemas.py.
Base URL (production): https://blindproof.tomd.org/api/
Endpoints¶
GET /health¶
Unauthenticated liveness probe. Returns {"status": "ok"}.
POST /enrol¶
Create a new user account.
Request (EnrolIn): {"email": "...", "password": "..."}
Response (EnrolOut): {"token": "...", "argon2_salt_hex": "..."}
The salt is generated server-side on enrolment and returned so the client can derive its master key. The password is used for account authentication; it is separate from the author's BlindProof passphrase (which we never see — it is used on the client to derive the encryption keys).
POST /login¶
Return a fresh bearer token and the user's Argon2 salt for an existing account.
Request (LoginIn): {"email": "...", "password": "..."}
Response (LoginOut): {"token": "...", "argon2_salt_hex": "..."}
Used by the desktop GUI's Sign-in flow when re-linking to an existing account.
GET /whoami — authenticated¶
Identity probe. Returns {"email": "..."}. Useful for the client to confirm a stored token is still valid.
POST /snapshots — authenticated¶
Upload a snapshot: ciphertext plus non-sensitive metadata.
Request (SnapshotIn):
{
"ciphertext_ref": "uuid-hex-string",
"ciphertext_b64": "base64-of-AES-GCM-ciphertext",
"nonce_hex": "24-hex-chars",
"captured_at": "2026-04-22T14:00:00+00:00",
"file_type": "md",
"path_ciphertext_b64": "base64-of-AES-GCM-encrypted-path",
"path_nonce_hex": "24-hex-chars",
"plaintext_hmac_hex": "64-hex-chars",
"ciphertext_size": 1234,
"word_count": 520,
"char_count": 3102
}
Response (SnapshotOut): the assigned snapshot id and server-stamped uploaded_at.
Notes:
- The server derives nothing from the manuscript — ciphertext is opaque bytes written to the blob store, plus the HMAC commitment for later Merkle aggregation.
path_ciphertext_b64is the file path encrypted with the client'senc_key, so even path names stay private.ciphertext_refis a client-chosen UUID-v4; it's used as the blob storage key end-to-end (client filename == server blob key).
GET /snapshots/{snap_id} — authenticated¶
Fetch a single snapshot: metadata plus the raw ciphertext body. Used for restore flows from another device.
Response (SnapshotFullOut): all metadata fields plus ciphertext_b64.
Only the authenticated user's own snapshots are returned; requesting someone else's snap_id returns 404.
GET /user/metadata — authenticated¶
List all snapshots for the authenticated user, metadata only — no ciphertext.
Response (UserMetadataOut): {"snapshots": [SnapshotMetadataOut, ...]}.
This is what the dashboard reads.
POST /proof-bundle — authenticated¶
Assemble and return the signed zip bundle for the authenticated user.
Request (ProofBundleIn): optional filters (from_date, to_date, include_attestation_text).
Response: a Content-Type: application/zip body. The four files inside are described in Proof bundle format.
On first call, the user's authorship attestation text is persisted — subsequent bundles read that stored text to keep bundles stable and comparable.
Authentication¶
BearerAuth (in backend/api/api.py) looks up the token against the AuthToken model and attaches the user to request.auth for handlers to use. Tokens are long-lived; there is no refresh flow. Rotating a token means calling /login again (the old token remains valid until manually revoked — no automatic expiry in the POC).
Error responses¶
django-ninja's default error envelope: HTTP status + {"detail": "..."}. The handlers don't customise this; wrong credentials are 401, missing resources are 404, validation errors are 422.
What the API deliberately does not expose¶
- No plaintext-echoing endpoints. Nothing accepts, returns, or logs plaintext, file paths, file names, or titles in the clear. If you find a log statement that could leak plaintext, it's a bug.
- No cross-user reads. Every authenticated handler scopes queries to
request.auth. - No aggregate analytics endpoints. The dashboard reads per-user data from
/user/metadataonly.