Skip to content

API Surfaces

Draft

Capsule has one public server transport: Kynos REST with a checked-in OpenAPI 3.2 contract. This doc owns the surface-to-module map, the document model, and the HTTP carriage of the universal handshake (whose rules Threat Model — Validation owns). Error-code identity is owned by Internationalization; publication by Developer Documentation.

SurfaceModuleOwner doc
Authentication (sessions, TOTP, OIDC)capsule-server::authAuthentication
Resumable upload (POST /v1/upload, HEAD/PATCH /v1/upload/{id})capsule-server::uploadTransfer — Upload
Lifecycle writes (POST /v1/albums/{album_id}/ops)capsule-server::routes::opsAuthorization
Album roster (PUT /v1/albums/{album_id}/roster)capsule-server::membershipThreat Model — Validation
Blob fetch (GET /v1/blob/{hash}, Range)capsule-server::blobTransfer — Download
Sync feedcapsule-server::syncTransfer — Download
Federation capabilities and report intakecapsule-server::federationFederation
Federation pull — no own route: GET /v1/sync?album_id= and GET /v1/blob/{hash} with a capability as bearercapsule-server::federationFederation
Share serving (/s/{opaque_id})capsule-server::shareShare Links
Guest drops (POST /d/{opaque_id}, inbox, adoption)capsule-server::dropWeb Upload
Storage verification (POST /v1/storage/verify)capsule-server::verifyTransfer — Durability
Device enrollment (/v1/auth/devices/enroll)capsule-server::enrollmentDevice Enrollment
Version / handshakecapsule-serverThreat Model — Validation
Library queries (timeline, albums, search)none — client-side over library.sqliteOrganization

The sync feed carries only opaque envelopes, encrypted metadata and blob references; content is fetched separately with Range. Content queries have no server surface because the key-free server cannot evaluate them. The signed manifest travels as opaque canonical CBOR, never re-modelled as wire fields (re-encoding would detach its signatures). The web client’s authenticated view is a key-free projection of the sync feed; in-browser decrypt is post-v1.

The checked-in OpenAPI document is the public contract and Spargen’s input. Kynos owns routing, middleware and deterministic schema emission; Capsule owns auth, protocol headers, error bodies, upload state, blob ports and business rules. One transport keeps negotiation, errors, streaming and test harnesses uniform. Its semantics are independent of the HTTP version underneath (HTTP Transport). GraphQL and gRPC are retired and MUST NOT return; legacy-review/ may inform contract tests but is never mounted.

The Document Is 3.2, and Its Version Is Pinned

Section titled “The Document Is 3.2, and Its Version Is Pinned”

The server emits with openapi_as(SpecVersion::V3_2), not plain openapi(), which would pick the lowest version that expresses the API and churn the committed contract when that changes. Enabling Kynos’s openapi32 feature alone does not produce 3.2 (see the Kynos row in Dependencies). openapi_as errors on a construct 3.2 cannot express rather than silently dropping operations.

Generated from the contract: every request and response body, every typed parameter, and the byte-serving endpoints (blob fetch with Range, derivative reads). A hand-written byte path would be a second parser.

Hand-written: only the resumable upload state machine — chunk scheduling, resumption from the committed set, retry laddering, connection budget — which is orchestration over generated calls, not parsing.

Every public route applies the same headers:

HeaderDirection
X-Capsule-Protocolrequest; required on every gated route
X-Capsule-Crypto-Suiterequest on writes; validated when present
X-Capsule-Sidecar-Schemarequest; validated when present, absence not refused
X-Capsule-Protocol-Min / -Maxresponse, on every response (including 401, 413, 426, and an unrouted 404/405)
X-Capsule-Min-Client-Buildresponse, on every response; advisory (0.0.0 = no cutoff)
X-Request-Idresponse, on every response (including an unrouted 404/405); a UUIDv7 the server mints per request. An inbound X-Request-Id is ignored, never echoed

Rules (carriage in capsule-server/src/negotiation.rs; pinned by capsule-server/tests/conformance.rs):

  • A write whose X-Capsule-Protocol is outside [Min, Max] is 426 with error.protocol.version_unsupported; the window is only in the headers.
  • A read is admitted at any grammatical protocol date, past or future.
  • A missing or malformed header on a gated operation is 400 error.request.malformed.
  • Advertised and enforced windows come from one source and cannot differ.

Exempt from the request gate (still carrying response headers):

  • GET /v1/version — the probe before a client knows the window.
  • GET /.well-known/capsule/attestation-keys, GET /.well-known/capsule/server-info, GET /.well-known/capsule/deprecation, GET /.well-known/capsule/revoked-jti — public discovery.
  • GET /s/{opaque_id}, /s/{opaque_id}/wrapped-secret, /s/{opaque_id}/blob/{hash} — a 426 would break Share Links’ indistinguishable 404.
  • POST /d/{opaque_id}, PATCH /d/{opaque_id}/{upload_id}, GET /d/{opaque_id}/{upload_id} — the link pins versions at issuance (Web Upload).
  • POST /v1/auth/refresh, POST /v1/auth/logout — refusing them would cut off reads still allowed, or keep open a session its holder closed. Login and register stay gated.

Credentials use Authorization: Bearer. Session tokens and federation capabilities are distinct token types verified by their modules, but the document has exactly one bearer security component for both, so the generated client uses one credential key.

Status is coarse; clients MUST switch on the error.* code in the ApiError body, never on status alone.

Every problem body also carries a request_id member: the same id as that response’s X-Request-Id header, which names the request’s server log lines. The document declares it on every problem schema but never as required, so a client stays lenient toward a server that predates it; a client surfaces it for a user to quote, and never parses or switches on it.

Rejection classHTTP status
Structural (bad envelope, unknown enum, sizes)400
Unauthenticated or expired token401
Unauthorized (capability, quota-hard, suspension)403
Not found, including indistinguishable-404 surfaces404
Stale state (chain, cursor, directory regression)409
Payload too large413
Write forwarded as TLS early data (HTTP Transport)425
Unsupported chunk media type415
Protocol outside [Min, Max]426
Rate limited429
Server fault the server can explain (store outage)503 with Retry-After
Storage full507
Unexplained internal state500, logged at ERROR
  • Every handshake rule yields the same headers, status and code across representative routes of each module.
  • Two generations of the contract are byte-identical and declare OpenAPI 3.2; the Spargen client regenerates without drift.
  • Streaming upload and ranged download handle cancellation, backpressure, retry and body limits without live infrastructure.
  • Invalid federation capabilities are rejected compartmentalised and indistinguishably.