Validation Invariants
The cross-cutting refuse-by-default rules every Capsule receiver runs before persisting any incoming write. These are the operational core of the threat model — a server or client that skips one of them silently widens the blast radius for the entire client class taxonomy.
The server-side invariants are enforced in capsule-api (every write path passes through them); the client-side invariants are enforced via the single verify_asset chokepoint in capsule-core::crypto plus the per-receiver decoder paths. The protocol handshake is a one-shot pre-flight check on every request; idempotency and atomicity invariants are properties of specific write surfaces, each cross-linked to the doc that owns the surface.
Server-Side Validation Invariants
Section titled “Server-Side Validation Invariants”The server holds no keys — it cannot verify any signature against a key it owns. But it does validate the structure of every write before persisting state. These checks are refuse-by-default and intentionally exhaustive; a buggy server that skips one of them silently widens the blast radius for the entire client class taxonomy.
This list is the canonical statement; Filesystem, Import, Federation, Authorization, and Authentication reference it without restating.
Invariants carry stable numbers (referenced across docs as “invariant 17”, “items 1–18”, etc.); they are grouped by write phase but the numbering is continuous.
On POST /upload (session creation)
Section titled “On POST /upload (session creation)”- 1.
X-Capsule-Protocolis within the server’s[Min, Max]range. Otherwise426 Upgrade Required, no session created. - 2.
crypto_suite_idis a row of the Primitives Inventory. Otherwise400. - 3.
hashlength matches the digest size forcrypto_suite_id(32 bytes for SHA-256). Otherwise400. - 4.
size∈ (0,max_file_size]. Otherwise400/413. - 5.
content_type∈ closed enum for this protocol version. Otherwise400. - 6.
album_idexists; authenticated user has server-visible write capability on it; album’s pinnedprotocol_versionequals the request’s. Otherwise403. - 7.
created_by_deviceis in the user’s published device directory, and the directory entry’sadded_atprecedes the request’stimestamp. Otherwise403. - 8.
timestamppasses a gross-drift sanity bound (default ±30 days of server clock, configurable). This is a non-security guard that surfaces a wildly-wrong honest client, not an authorization control — authorization and ordering ride the epoch and chain, and the server records its own trustedreceived_atas the authoritative time for time-based policy. The clienttimestampis stored verbatim for audit. See Keys — Write Authorization. Otherwise400.
On each PATCH /upload/{id} chunk
Section titled “On each PATCH /upload/{id} chunk”- 9. Offset is exactly the current received-byte count. Otherwise
409, withX-Capsule-Offsetreturned. - 10. Non-final chunk size is a multiple of 4 KiB. Otherwise
400. - 11. Cumulative received ≤ declared
size. Otherwise400/413, session moves toFailedProcessing. - 12. The
(upload_id, offset, chunk_hash)idempotency tuple is new OR matches an exact prior PATCH. Otherwise (same offset, different hash)409+ corruption error.
At finalization
Section titled “At finalization”- 13. Total received == declared
size. OtherwiseFailedProcessing. - 14. Recomputed ciphertext hash == declared
hash. OtherwiseFailedProcessing+ corruption error. - 15. Manifest envelope re-validated (rerun 1–8) inside the finalization transaction.
On non-upload writes (lifecycle action manifest, metadata-update, derivative-add/replace, trash-restore)
Section titled “On non-upload writes (lifecycle action manifest, metadata-update, derivative-add/replace, trash-restore)”- 16.
actionis in the closed enum. Otherwise400. - 17.
prior_provenance_hashequals the last accepted manifest’s content hash for thisasset_id. Otherwise409(stale-revival). - 18.
amk_versionis monotonic per album (never regresses) and within the range the album’s admin-signed MLS commit chain attests. The server’s stored counter is a structural backstop; the authoritative ceiling is MLS, so a server cannot fabricate a future epoch a client will honor — see Write Authorization. Otherwise400.
On federation pull (server-to-server)
Section titled “On federation pull (server-to-server)”- 19. Capability token verifies under home server’s signing key;
expin future;jtinot in revocation list (cached ≤ 15 min). Otherwise401/403. - 20. All checks (1)–(18) re-applied — federation does not unlock looser rules.
- 21. Per-peer rate budgets unbroken (events/hour, bytes/hour, CPU/hour). Otherwise
429.
On the /sync feed, directory publish, and federated reports
Section titled “On the /sync feed, directory publish, and federated reports”- 22. The
sync_cursorcarries a server MAC under a server-only key; a forged or mutated cursor is rejected (400). This is the authenticity layer; the client independently enforces per-albumsync_seqmonotonicity (client-side invariants below). Owner: Import — Download & Sync. - 23. A published
DeviceDirectoryhasdirectory_versionstrictly greater than the version currently stored for that user, and the master signature covers it. A non-advancing or regressing publish is rejected (409). Owner: Cryptography — Device Directory. - 24. A federated report (an out-of-band moderation message, not a state write) carries a valid signature from the reporting server and is within that peer’s report rate budget; otherwise it is dropped before reaching the admin queue. Owner: Moderation — Federated Reporting.
Every rejection is logged with a structured reason code; the rejected hash is remembered (bounded, see Federation — Soft-Fail Semantics) so divergence between Capsule’s view and a permissive peer’s view is detectable.
Client-Side Validation Invariants
Section titled “Client-Side Validation Invariants”Mirror checklist that every client implements before applying any received data — local or remote. A client that skips one of these is in the faulty class.
- Run
verify_asseton every receivedAssetManifest. Quarantine on failure; never silent-drop, never silent-accept. - Reject an incoming
sidecar_schemagreater than the client’smax_known_sidecar_schema. Refuse to write that sidecar; refuse to read in normal mode (read-only opt-in is allowed). - Reject an incoming
protocol_versionoutside[Min, Max]known to the client. The same handshake the server runs. - Reject an unknown enum value for any field whose enum is closed at the current schema (notably
action,content_type,gps.source,DerivativeManifest.role). Unknown CBOR map keys are preserved per Postel’s Law and never executed. - Maintain a local
latest_provenance_hashperasset_id. Refuse to apply any manifest whoseprior_provenance_hashis behind the local value. Surface it. - Maintain a per-user
directory_versionhigh-water mark. Refuse aDeviceDirectorywhosedirectory_versionis below it (a server attempting to roll back a revocation or hide a device); pin and surface the regression. - Reject an OR-set remove whose
add_idwas never observed locally as an add. - Refuse to follow a
revoke_all_sessionsconfirmation that did not include a master-key proof. - Decode remote-origin asset bytes only in the sandboxed decoder.
Protocol and Capability Negotiation
Section titled “Protocol and Capability Negotiation”Every versioned API surface — client-to-server uploads, sync feed, federation pull, peering — runs the same compatibility gate. The gate is fail-closed: a mismatch is a hard reject before any state is written, never a silent degrade.
Universal Headers
Section titled “Universal Headers”| Header | Sent by | Meaning |
|---|---|---|
X-Capsule-Protocol | client / peer | YYYY-MM-DD protocol version the request is written against |
X-Capsule-Crypto-Suite | client / peer on writes | u16 suite id from the Primitives Inventory |
X-Capsule-Sidecar-Schema | client on metadata-update | u16 schema version declared at sidecar_schema field 0 |
X-Capsule-Protocol-Min | server on every response | the lowest protocol version this server accepts |
X-Capsule-Protocol-Max | server on every response | the highest protocol version this server accepts |
X-Capsule-Min-Client-Build | server on responses | semver deprecation cutoff; advisory unless the path is hard-deprecated |
Fail-Closed Rules
Section titled “Fail-Closed Rules”X-Capsule-Protocoloutside[Min, Max]on a write:426 Upgrade Required. No session created, no row written.X-Capsule-Crypto-Suitenot in the inventory:400 Bad Request.X-Capsule-Sidecar-Schemaabove the server’s max known:400 Bad Request. (The server does not parse sidecars itself, but it refuses to acknowledge writes whose schema number it does not index.)- Reads of any past version succeed. Read invariants are deliberately stable per Versioning, so a current server still serves v_{k-N} blobs from years ago.
- Federation capability is an additional
401/403layer on top of the protocol gate. A valid token never substitutes for a valid protocol header.
The handshake is one-shot per request, not a negotiation. Either both sides agree by inspection, or the request fails. There is no back-and-forth that could leak partial state.
Idempotency Invariants
Section titled “Idempotency Invariants”Every write surface has a single idempotency key. Duplicates are no-ops; conflicts (same key, different content) are corruption errors.
| Surface | Idempotency key | Duplicate behavior |
|---|---|---|
Upload chunk (PATCH /upload/{id}) | (upload_id, offset, chunk_hash) | Returns current offset; no double-write |
Session creation (POST /upload) | (owner_id, hash, album_id) — server’s existing dedup check | Returns the existing session; no second session |
| Lifecycle manifest write | (asset_id, prior_provenance_hash, manifest_hash) | No-op append; chain advances exactly once |
| Metadata-update operation | Operation id (UUIDv7) + (asset_id, prior_provenance_hash) | Re-applying the same op is structurally identical |
| Federation capability proof | (peer_id, jti) | Refresh with same jti returns the same response |
| Federation pull | (peer_id, sync_cursor) — the sync cursor itself is the key | Re-pull returns the same page |
| MLS commit | Handled by OpenMLS; commits are ordered by the group’s commit chain | OpenMLS rejects duplicates |
| Album upgrade ceremony | intent_id (UUIDv7); see Versioning | Same intent never produces two forks |
A write surface that does not appear here is, by default, not idempotent and must be designed before it ships.
Atomicity Invariants
Section titled “Atomicity Invariants”Multi-write operations that must succeed-as-one or not at all. A partial success on any of these is itself a damage scenario.
- Asset bundle finalization. The manifest, ciphertext blob, metadata blob, and provenance blob commit together in a single Postgres transaction. Server failure between any pair leaves the entire bundle un-finalized; the session moves to
FailedProcessingand the partial blobs are GC’d. (Filesystem — Atomic Writes) - Stack edits. All affected sidecars stage as
.tmpfiles first; renames happen together. Any rename failure discards every.tmpin the bundle. (Filesystem — Atomic Writes) - AMK epoch bump + write-tier key rotation. A new AMK and a new write-tier key are minted as a single MLS commit. The two cannot exist out of sync.
- Album upgrade ceremony. The cutover is one MLS commit, the
AlbumTombstone. Until applied, the client is in v_old; after, in v_new. (Versioning — Album Upgrade Ceremony) - Lifecycle manifest + provenance record. Writing a lifecycle manifest and appending its provenance entry are the same act, because the provenance entry is the manifest plus the chain link. There is no separate “now record provenance” step that can race.