Transfer Protocol
DraftThe transfer protocol moves blob bytes between a client and its home server, in both directions. It is Capsule-owned: session state, encrypted-blob provenance, finalization and access are one end-to-end security contract, so no generic transfer library or TUS compatibility is offered. The server never holds a key; every byte it moves is opaque ciphertext, except the signed provenance manifest, which is server-visible by design.
| Page | Covers |
|---|---|
| Upload | Resumable upload sessions, chunk rules, finalization, staged tiers T0/T1/T2, error codes |
| Download | Tiered and ranged fetch, blob access scoping, the fallback ladder, prefetch |
| Durability | Storage verification, deep re-hash, custody receipts, releasing local originals |
What changed — the metadata feed a client reads before deciding what to fetch — is Sync, not part of this protocol. Implementations: capsule-server::upload, capsule-server::blob, capsule-server::verify, capsule-server::attestation, capsule-sdk::upload, capsule-sdk::fetch, capsule-sdk::verify, capsule-sdk::staged. The wire shapes are the OpenAPI contract; these pages fix the semantics the schema cannot express.
Invariants
Section titled “Invariants”Every implementation, on either side, MUST uphold:
- Blobs are immutable and content-addressed. A blob is named by the hash of its stored bytes (ciphertext content hash); bytes at an address never change. Identical bytes are one object.
- The wire is strict. A request that violates the contract is rejected with a machine-readable
error.*code, never silently corrected or partially honoured. - A chunk is acknowledged only after it is durably committed. The server fsyncs the staged range and records it in the committed set before answering
204; an acknowledged byte is a byte on disk, and no write ever touches a committed byte. - Finalize re-hashes before commit. The server recomputes the content hash over the staged bytes and re-runs the envelope checks before the blob is committed to its address. A mismatch is corruption, never retried server-side.
- An index row never points at a missing blob. The blob is committed (atomic rename plus directory fsync) before any index row references it. The failure direction is an unreferenced blob, which GC collects, never a dangling reference.
- Custody and receipt are one fact. A receipt is written in the same transaction that indexes the committed blob; neither exists without the other.
- A client releases a local original only on server custody: a
durablestorage verdict, a verified signed custody receipt whose server-recomputed hash matches the bytes the client sent (the read-back hash), andverify_assetacceptance. See Verify Before Destroy. - Resume is driven by server truth, not client state. The committed set comes from
HEAD, in-flight sessions fromGET /v1/upload/sessions, held tiers from the sync feed. Client-side queues are rebuildable caches. - Every transfer step is idempotent. Re-creating a session, re-sending a chunk, re-finalizing, re-fetching a range and re-verifying all converge on the same state.
- Every session terminates. A session is driven to
CompletedorFailedProcessing, or discarded; no staged file or reserved row outlives it. - The server never produces manifest bytes. Manifests travel as
provenanceblobs and are served verbatim. - Semantics never depend on the transport. Every operation means the same over HTTP/1.1, HTTP/2 and HTTP/3, and correctness survives the loss of any connection at any moment (HTTP Transport).
- No request waits on unbounded server work. A request answers with the state as it stands; finalization and other long work run detached and are observed through
HEAD.
client: encrypt + sign bundle │ POST /v1/upload ─▶ PATCH chunks ─▶ finalize (claim → re-hash → envelope → commit → index → receipt) ▼server blob store + asset index ──▶ sync feed advances │ ▼ other devices: GET /v1/sync (metadata) ─▶ GET /v1/blob/{hash} (ranged, per tier)uploader: POST /v1/storage/verify + receipt ─▶ release local original