diff --git a/SLICES.md b/SLICES.md
index 9ac8c572..ca3059ea 100644
--- a/SLICES.md
+++ b/SLICES.md
@@ -252,7 +252,7 @@ row's remainder now lives.
| S-C22 | Structured `duplicate_blob` ref + adopt in OpenAPI | server | S-C37 | S | RETIRED | done\* | server half; adopt endpoint → `S-C5`; undescribed extension → `S-C38` |
| S-C23 | `revoke_all_sessions` with master-key proof | server | S-C42 | M | RETIRED | done | `S-C48` closed the access-token window it left open |
| S-C24 | Album-upgrade server halves (quiescence/drain/lineage) | server | S-C42 | M-L | RETIRED | done\* | the ceremony's wire vocabulary was `mls`-gated and therefore unreachable; the projection deliberately gets no lineage |
-| S-C25 | Album provisioning + UUID album ids (unblocks push) | server | S-C29 | M | RETIRED | done\* | also lands the first real `WriteAuthority`; sharing widens it → `S-C4`/`S-C5` |
+| S-C25 | Album provisioning + UUID album ids (unblocks push) | server | S-C29 | M | RETIRED | done\* | also lands the first real `WriteAuthority`; sharing widened it in `S-C51`; the `AlbumStore` Postgres adapter is still owed |
| S-C26 | Retire the plaintext album name/description columns | server | S-C25 | S | RETIRED | done | the Kynos schema never declared them; a document tripwire keeps it that way |
| S-C27 | Wire-contract types on plain serde behind an adapter | server | — | M | RETIRED | part 1 done | DTO move → Kynos rebuild; status gaps → `S-C28` |
| S-C28 | Publish the statuses the server actually returns | server | S-C27 | S | RETIRED | done\* | auth surface closed; folds into each remaining port |
@@ -266,7 +266,7 @@ row's remainder now lives.
| S-C36 | Kynos's framework rejections carry no `error.*` code | server | S-C33 | M | RETIRED | done | a Capsule interceptor fills the member in; the upstream seam is still the better fix |
| S-C37 | The asset index port, one sequence instead of two | server | S-C27, S-C29 | L | RETIRED | done | Postgres adapter landed under the row lock the design rests on; absorbs `S-C21`, unblocks `S-C22` |
| S-C38 | Problem extensions are absent from the OpenAPI document | server | S-C34 | M | RETIRED | done\* | `code` is universal and derived; the sixteen other members ride a small table |
-| S-C39 | Blob fetch has no read authority, so its `403` is unwritable | server | S-C10 | M | RETIRED | part | the authority lands and owner-scopes the path; the `403` needs a membership fact → `S-C51` |
+| S-C39 | Blob fetch has no read authority, so its `403` is unwritable | server | S-C10 | M | RETIRED | done | the authority landed owner-scoped; the `403` landed with `S-C51`'s membership fact |
| S-C40 | `awaiting-original` is not observable on the blob path | server | S-C10, S-C37 | M | RETIRED | done | the promise is the open upload session, so it needed no lifetime of its own |
| S-C41 | The `deep` re-hash, with the limiter that makes it safe | server | S-C3, S-C32 | M | RETIRED | done\* | coalescing is deliberately absent, and the reason is in the note |
| S-C42 | Nothing verifies the device directory's own signature | server | S-C9 | M | RETIRED | done | trust-on-first-publish anchor; unblocks `S-C23` |
@@ -278,7 +278,7 @@ row's remainder now lives.
| S-C48 | The bearer scheme never reads the session ledger | server | S-C23, S-C29 | M | RETIRED | done\* | fails closed as `401`; the honest `503` needs the seam `S-C36` wants |
| S-C49 | Moderation's federated halves have no federation to hang on | server | S-C8, S-C32 | M | RETIRED | blocked | found by `S-C8`; report intake and the blocklist both need the federation layer |
| S-C50 | The share-link privacy strip is specified where it cannot run | docs | S-C4 | S | ACTIVE | done | both docs now name the issuing client, with containment as the server's half |
-| S-C51 | Server-side album membership, which two authorities are waiting on | server | S-C25, S-C39 | L | RETIRED | blocked | found by `S-C39`; the read `403` and the widening of album *write* access are one missing fact |
+| S-C51 | Server-side album membership, which two authorities are waiting on | server | S-C25, S-C39 | L | RETIRED | done | the owner-signed roster is the fact; `PUT /v1/albums/{album_id}/roster`, the write widening, the blob `403` and `GET /v1/sync?album_id=` land together; shared bytes across owners → #462 |
| S-C52 | The server keeps one manifest per asset, not the chain it is documented to hold | server | S-C16, S-C43, S-C45 | M | RETIRED | done | retention decided *for*, and scrub check 4 lands with it |
| S-C53 | Account creation has no surface on the rebuilt server | server | S-C13 | M | RETIRED | done | registration lands; the unported operations are decided one by one on `S-C54`–`S-C58` |
| S-C54 | The profile surface, and a password change that is not a reset | server | S-C53 | M | RETIRED | done | three operations where Salvo had one; the address becomes immutable and `/validate` and password reset are deleted rather than owed |
@@ -2190,9 +2190,10 @@ working on a surface written after it.
- **The name refusal is a `422`, not a silent drop.** The body is strict, so a `name` or
`description` is refused — a client is told the server will not hold album titles rather than
left to assume it did. `S-C26` retires the columns themselves.
-- **Owed:** sharing widens "writable" from *owner* to *member*, which is `S-C4`/`S-C5`; until
- then an album is writable only by the account it was provisioned to, which is the safe
- direction. The Postgres adapter is owed with the rest.
+- **Owed:** sharing widens "writable" from *owner* to *member* — landed with `S-C51`, not with
+ `S-C4`/`S-C5` (a share link is not a member): `album_write_access` is keyed on the caller and
+ answers a writer on the album's roster with the owner's namespace. The Postgres adapter for
+ `AlbumStore` is still owed with the rest.
[`WriteAuthority`]: #s-c20--ground-invariant-7s-floor-in-the-device-directory
@@ -2686,8 +2687,13 @@ design/moderation.md states the per-surface rule as *"takedown of known content
re-reading a landed, tested contract on an inference is not this slice's to do. Recorded here for
whoever owns that question.
-- **Done when:** the account unshared from an album receives `403` — **not met**, and blocked on
- `S-C51`. ✅ what did land: `another_accounts_live_blob_is_unknown_rather_than_served`,
+- **Done when:** the account unshared from an album receives `403` — **met with `S-C51`**
+ (2026-09-03): `MembershipAuthority` replaces `OwnedAssetAuthority`, `BlobReadAccess::Revoked`
+ renders `403 error.blob.access_revoked` for an account the membership store holds a revoked
+ row for, and the authority is still asked first. ✅ `a_former_member_is_told_access_was_revoked`,
+ `a_former_member_gets_the_403_before_any_policy_refusal`,
+ `a_never_member_is_indistinguishable_from_an_unknown_address_body_and_headers`, plus what
+ landed with the `part`: `another_accounts_live_blob_is_unknown_rather_than_served`,
`a_strangers_refusal_is_indistinguishable_from_an_unknown_address`,
`a_stranger_cannot_tell_a_takedown_from_an_unknown_address`, and the authority's own unit cases.
**Tier:** Unit + Integration.
@@ -3878,8 +3884,8 @@ them was incidental:
- **Gap** (found 2026-08-31 landing `S-C39`): the server holds no fact about who, other than the
owner, may read or write an album. Two separate authorities are pinned to "owner only" by the
same absence:
- - `OwnedAssetAuthority` cannot render the `403` the download contract describes, because there
- is no membership to withdraw;
+ - `OwnedAssetAuthority` (since replaced by `MembershipAuthority`) cannot render the `403` the
+ download contract describes, because there is no membership to withdraw;
- `ProvisionedAuthority::album_write_access` has answered `Denied` for anything but the owner
since `S-C25`, with a comment deferring the widening to `S-C4`/`S-C5` — which landed as
**link** and **drop** capabilities and did not add it, correctly: a share link is not a
@@ -3893,10 +3899,32 @@ them was incidental:
end-to-end encryption exists to avoid.
- **Blocked on:** the same signed-capability primitive federation needs, so it should be designed
with `S-C49` rather than beside it.
+- **Landed 2026-09-03 (#405).** The fact is a **full-roster attestation** the album owner signs
+ with a non-revoked device in their published device directory —
+ `capsule_core::crypto::membership::SignedAlbumRoster`, canonical CBOR, strictly monotonic
+ `roster_version`, non-decreasing `amk_epoch`, removal as absence at a higher version — published
+ at `PUT /v1/albums/{album_id}/roster` (invariant 33) and held by the `membership` port
+ (in-memory and Postgres, ordinal 5, one conformance suite). Removal is a *stored* fact: the row
+ is marked with the version and epoch at which the member vanished, which is what lets the blob
+ route disclose `403` to a former member and nothing to anyone else. It is a **transport**
+ control, never a confidentiality one; the server still cannot read the MLS group.
+ Widened on it: `WriteAuthority::album_write_access` is caller-keyed and admits a writer member
+ under the owner's namespace (upload, ops; adoption and finalization re-check); `MembershipAuthority`
+ serves either role and answers `Revoked` → `403 error.blob.access_revoked`;
+ `GET /v1/sync?album_id=` pages the owner's sequence filtered to the album for its members, with
+ the cursor bound to `(caller, album)`. Not here: the federation capability path (#406, which
+ stacks on `MembershipStore`, `CursorScope`, `album_feed_page` and `BlobReadAccess`); rosters
+ published by non-owner admins; bytes shared across unrelated owners, which `find_reference` still
+ decides from the first live row (#462).
- **Done when:** a member of a shared album reads its blobs through `/v1/blob/{hash}` and writes
to it through the upload path; a former member receives `403` on the first and a write refusal
on the second; and a non-member remains unable to tell either from an address that does not
- exist. **Tier:** Unit + Integration.
+ exist — **met**: `a_member_of_either_role_reads_the_owners_blobs`,
+ `a_writer_member_uploads_into_the_owners_album_and_pays_for_it`,
+ `a_former_member_is_told_access_was_revoked`,
+ `a_reader_a_former_member_and_a_stranger_get_the_one_album_refusal`,
+ `a_never_member_is_indistinguishable_from_an_unknown_address_body_and_headers`.
+ **Tier:** Unit + Integration.
### S-D1 — SDK upload client
diff --git a/capsule-android/src/androidMain/res/values/strings.xml b/capsule-android/src/androidMain/res/values/strings.xml
index 4c874840..104b58d6 100644
--- a/capsule-android/src/androidMain/res/values/strings.xml
+++ b/capsule-android/src/androidMain/res/values/strings.xml
@@ -1819,6 +1819,11 @@
Upload only
This album could not be registered because its identifier is malformed.
This album isn\'t available on your account.
+ Only a device on the album owner\'s account can publish its roster.
+ Capsule couldn\'t read that album roster.
+ That album isn\'t yours, or doesn\'t exist.
+ The roster you sent is out of step with the one the server holds.
+ That roster\'s version is too far ahead of the one the server holds.
Capsule couldn\'t set up that album. Please try again.
This album is already being upgraded.
Capsule couldn\'t read that album upgrade request.
@@ -1843,6 +1848,7 @@
There is no two-factor setup waiting to be confirmed.
Capsule couldn\'t reach your account just now. Please try again.
An account with these details already exists.
+ You no longer have access to this album.
That photo file is no longer available.
That photo file isn\'t on the server.
The original photo hasn\'t been uploaded from its device yet.
@@ -1909,6 +1915,7 @@
Too many deep storage checks. Please wait and try again.
The storage-verification request was malformed.
Capsule couldn\'t check whether your photos are safely stored. Please try again.
+ You don\'t have access to that album.
The sync session is out of date. Capsule will resync from the start.
Please sign in again to continue syncing.
Capsule could not reach the server to sync. It will try again.
diff --git a/capsule-cli/src/status.rs b/capsule-cli/src/status.rs
index e4d263a8..4f46d8e8 100644
--- a/capsule-cli/src/status.rs
+++ b/capsule-cli/src/status.rs
@@ -236,12 +236,21 @@ impl ServerStatus {
// exactly the base the generated operation paths hang off.
let api_endpoint = remote.sync_endpoint.clone();
- let client = match capsule_sdk::rest::Client::new(&api_endpoint) {
+ // Over the SDK's one HTTP client rather than the generated `Client::new`, so the probe
+ // carries the same protocol handshake every other request does; `/v1/version` is
+ // exempt from the gate, and a probe that spoke differently from the calls it precedes
+ // would tell the user nothing about them.
+ let client = match capsule_sdk::net::http_client()
+ .map_err(|error| error.to_string())
+ .and_then(|http| {
+ capsule_sdk::rest::Client::with_client(http, &api_endpoint)
+ .map_err(|error| error.to_string())
+ }) {
Ok(client) => client,
Err(error) => {
return Ok(ServerStatus {
api_endpoint,
- connection_status: ConnectionStatus::Error(error.to_string()),
+ connection_status: ConnectionStatus::Error(error),
api_version: None,
response_time: None,
server_health: None,
diff --git a/capsule-core/src/crypto/membership.rs b/capsule-core/src/crypto/membership.rs
new file mode 100644
index 00000000..3844c90d
--- /dev/null
+++ b/capsule-core/src/crypto/membership.rs
@@ -0,0 +1,350 @@
+//! The album roster attestation — the one membership fact a **key-free server** can verify
+//! (slice `S-C51`).
+//!
+//! # What the server cannot see, and what this gives it instead
+//!
+//! Membership of a shared album is decided inside the MLS group, and every control message
+//! that adds or removes a member is AEAD-protected under a group key the server never holds.
+//! `crypto::authority` can classify a commit chain as behind, ahead or forked from a server's
+//! view of it, but it cannot tell the server *who is in the group*. So the server has no
+//! roster — and without one it cannot answer "may this account read this album's blobs", which
+//! is why the blob route and the album write routes have been owner-only.
+//!
+//! The [`SignedAlbumRoster`] is the album owner's **statement** of the roster, signed by one of
+//! the owner account's devices. The server verifies it against the owner's published
+//! [`DeviceDirectory`] — the same trust anchor `S-C42` established and the same check
+//! [`SignedUpgradeIntent::verify`](crate::crypto::upgrade::SignedUpgradeIntent::verify) runs
+//! for the upgrade ceremony — and then holds it as a *transport control*: who may fetch which
+//! bytes. It is **not** a confidentiality control. A former member who kept the AMK for an
+//! epoch can still decrypt what they already downloaded; what the roster does is stop the
+//! server handing them anything further, exactly as design/federation.md says an unshare cuts
+//! read access to the historical photos at the transport level.
+//!
+//! # A full document, versioned, and the owner is implicit
+//!
+//! The roster is the **whole** member list every time, with a strictly monotonic
+//! [`AlbumRoster::roster_version`] — the same shape as the device directory's
+//! `directory_version` (invariant 23). One monotonic field gives idempotency, replay-safety and
+//! ordering at once, and it needs no per-grant ids and no separate revocation artefact:
+//! removal is *absence* at a higher version. The owner account is never listed, because the
+//! owner's access is the album record's `owner_id` fact and a roster that could omit the owner
+//! would be a roster that could lock the owner out.
+//!
+//! # Why this lives here and not in the server crate
+//!
+//! A client signs it. The DSK that signs a roster is on a device, so the type must be
+//! constructible and signable without the server crate — which is the rule `crypto::upgrade`
+//! records for the upgrade intent, for the same reason: a structure defined at both ends is one
+//! added field away from a signature that stops verifying.
+
+use serde::{Deserialize, Serialize};
+use uuid::Uuid;
+
+use crate::crypto::keys::{AmkVersion, DeviceDirectory, HybridSignature, HybridSigningKey};
+
+/// What went wrong encoding or verifying an album roster.
+#[derive(Debug, thiserror::Error, PartialEq, Eq)]
+pub enum MembershipError {
+ /// The roster could not be canonically encoded for signing.
+ #[error("the album roster could not be encoded: {0}")]
+ Encode(String),
+ /// The signature did not verify, or the attesting device is not a live device of the album
+ /// owner's account.
+ #[error("the album roster's attester signature did not verify: {0}")]
+ Attester(&'static str),
+}
+
+/// What a member may do with the album's contents, as far as the server is concerned.
+///
+/// Two values only. The finer MLS-side distinctions (admin, for one) never reach the server,
+/// which needs exactly this: whether to serve bytes, and whether to accept them.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum MemberRole {
+ /// May read the album's blobs and its sync feed.
+ Reader,
+ /// May read, and may add or change assets under the album owner's namespace.
+ Writer,
+}
+
+/// One member of an album, as the roster names them.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct RosterMember {
+ /// The member's account.
+ pub user_id: Uuid,
+ /// What the member may do.
+ pub role: MemberRole,
+}
+
+/// The signed-over content of a roster attestation. Every field is covered by the attesting
+/// device's DSK hybrid signature in the enclosing [`SignedAlbumRoster`].
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct AlbumRoster {
+ /// The album this roster is for.
+ pub album_id: Uuid,
+ /// Strictly monotonic per album. A server refuses a version at or below the one it holds
+ /// unless the bytes are identical (a replay), so a roster can neither be rolled back nor
+ /// silently replaced.
+ ///
+ /// A server also bounds it **above**, by a small step over the version it holds: the field
+ /// is an ordering, not a count, and a version nothing could ever exceed would freeze the
+ /// album's membership permanently. A client that is refused for it re-signs the same
+ /// document one above the version the refusal names.
+ pub roster_version: u64,
+ /// The AMK epoch the group is at after the commit this roster reflects. Non-decreasing
+ /// across versions; the server records the epoch at which a member was granted and the one
+ /// at which they vanished.
+ pub amk_epoch: AmkVersion,
+ /// The album owner's account. The server anchors on this account's published device
+ /// directory, and refuses a roster whose owner is not the album's.
+ pub attested_by_user: Uuid,
+ /// The owner-account device whose DSK signed this roster. Must be present and **not
+ /// revoked** in the owner's directory.
+ pub attested_by_device: Uuid,
+ /// RFC 3339 time the client produced the roster. Audit-only: the server orders by
+ /// `roster_version`, never by this.
+ pub attested_at: String,
+ /// Everyone other than the owner who may read the album, and what they may do. Absence at a
+ /// higher version *is* removal.
+ pub members: Vec,
+}
+
+impl AlbumRoster {
+ /// The canonical-CBOR signing bytes the attesting device's DSK covers.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`MembershipError::Encode`] if the roster cannot be canonically encoded.
+ pub fn signing_bytes(&self) -> Result, MembershipError> {
+ crate::cbor::to_canonical_vec(self).map_err(|e| MembershipError::Encode(e.to_string()))
+ }
+}
+
+/// An [`AlbumRoster`] plus the attesting device's DSK **hybrid** signature over it.
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub struct SignedAlbumRoster {
+ /// The attested roster.
+ pub roster: AlbumRoster,
+ /// The attesting DSK's hybrid signature over [`AlbumRoster::signing_bytes`].
+ pub attester_sig: HybridSignature,
+}
+
+impl SignedAlbumRoster {
+ /// Sign `roster` with the attesting device's DSK.
+ ///
+ /// The caller is responsible for `roster.attested_by_device` naming the device `dsk`
+ /// belongs to; [`verify`](Self::verify) is what checks it, on the other end.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`MembershipError::Encode`] if the roster cannot be canonically encoded.
+ pub fn sign(roster: AlbumRoster, dsk: &HybridSigningKey) -> Result {
+ let attester_sig = dsk.sign(&roster.signing_bytes()?);
+ Ok(Self {
+ roster,
+ attester_sig,
+ })
+ }
+
+ /// Verify the attester's DSK hybrid signature (Ed25519 **and** ML-DSA) against the album
+ /// owner's published device directory.
+ ///
+ /// Stricter than the upgrade intent's check in one respect: a device the directory has
+ /// **revoked** may not attest a roster, whatever it signed before. The entry is retained so
+ /// that older manifests stay verifiable; it is not a licence to keep issuing new documents.
+ ///
+ /// # Errors
+ ///
+ /// Returns [`MembershipError::Attester`] when the directory names a different account, does
+ /// not hold the attesting device, holds it revoked, or the signature does not verify under
+ /// its DSK.
+ pub fn verify(&self, directory: &DeviceDirectory) -> Result<(), MembershipError> {
+ if directory.core.user_id != self.roster.attested_by_user {
+ return Err(MembershipError::Attester(
+ "the roster's attested_by_user is not this directory's account",
+ ));
+ }
+ let entry =
+ directory
+ .device(&self.roster.attested_by_device)
+ .ok_or(MembershipError::Attester(
+ "the attesting device is not in the directory",
+ ))?;
+ if entry.revoked_at.is_some() {
+ return Err(MembershipError::Attester("the attesting device is revoked"));
+ }
+ if !entry
+ .dsk_public
+ .verify(&self.roster.signing_bytes()?, &self.attester_sig)
+ {
+ return Err(MembershipError::Attester(
+ "the attester's DSK signature does not verify",
+ ));
+ }
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::crypto::keys::{DeviceEntry, DirectoryCore};
+
+ const OWNER: Uuid = Uuid::from_u128(0xA11CE);
+ const DEVICE: Uuid = Uuid::from_u128(0xD1);
+ const ALBUM: Uuid = Uuid::from_u128(0xA1B);
+ const BOB: Uuid = Uuid::from_u128(0xB0B);
+ const CAROL: Uuid = Uuid::from_u128(0xCA501);
+
+ fn ik() -> HybridSigningKey {
+ HybridSigningKey::from_seed_bytes(&[1; 32], &[2; 32])
+ }
+
+ fn dsk() -> HybridSigningKey {
+ HybridSigningKey::from_seed_bytes(&[3; 32], &[4; 32])
+ }
+
+ fn other_dsk() -> HybridSigningKey {
+ HybridSigningKey::from_seed_bytes(&[5; 32], &[6; 32])
+ }
+
+ fn directory_for(user_id: Uuid, revoked: bool) -> DeviceDirectory {
+ DirectoryCore {
+ user_id,
+ directory_version: 1,
+ updated_at: "2026-09-01T00:00:00Z".into(),
+ devices: vec![DeviceEntry {
+ device_id: DEVICE,
+ dsk_public: dsk().verifying_key(),
+ dek_public: None,
+ added_at: "2026-09-01T00:00:00Z".into(),
+ revoked_at: revoked.then(|| "2026-09-02T00:00:00Z".to_owned()),
+ }],
+ }
+ .sign(&ik())
+ }
+
+ fn roster() -> AlbumRoster {
+ AlbumRoster {
+ album_id: ALBUM,
+ roster_version: 1,
+ amk_epoch: AmkVersion(1),
+ attested_by_user: OWNER,
+ attested_by_device: DEVICE,
+ attested_at: "2026-09-02T00:00:00Z".into(),
+ members: vec![
+ RosterMember {
+ user_id: BOB,
+ role: MemberRole::Writer,
+ },
+ RosterMember {
+ user_id: CAROL,
+ role: MemberRole::Reader,
+ },
+ ],
+ }
+ }
+
+ #[test]
+ fn a_roster_signed_by_a_live_owner_device_verifies() {
+ let signed = SignedAlbumRoster::sign(roster(), &dsk()).expect("signs");
+ assert_eq!(signed.verify(&directory_for(OWNER, false)), Ok(()));
+ }
+
+ #[test]
+ fn the_signed_roster_round_trips_through_canonical_cbor() {
+ // The server stores the document verbatim and the SDK ships it base64-encoded, so the
+ // bytes must decode back to a value that still verifies.
+ let signed = SignedAlbumRoster::sign(roster(), &dsk()).expect("signs");
+ let bytes = crate::cbor::to_canonical_vec(&signed).expect("encodes");
+ let decoded: SignedAlbumRoster = crate::cbor::from_slice(&bytes).expect("decodes");
+ assert_eq!(decoded, signed);
+ assert_eq!(decoded.verify(&directory_for(OWNER, false)), Ok(()));
+ // And the signing bytes are stable: the same roster encodes to the same bytes.
+ assert_eq!(
+ roster().signing_bytes().expect("encodes"),
+ decoded.roster.signing_bytes().expect("encodes")
+ );
+ }
+
+ #[test]
+ fn a_directory_of_another_account_is_refused() {
+ let signed = SignedAlbumRoster::sign(roster(), &dsk()).expect("signs");
+ assert_eq!(
+ signed.verify(&directory_for(BOB, false)),
+ Err(MembershipError::Attester(
+ "the roster's attested_by_user is not this directory's account"
+ ))
+ );
+ }
+
+ #[test]
+ fn a_device_the_directory_does_not_hold_is_refused() {
+ let mut unknown = roster();
+ unknown.attested_by_device = Uuid::from_u128(0xD2);
+ let signed = SignedAlbumRoster::sign(unknown, &dsk()).expect("signs");
+ assert_eq!(
+ signed.verify(&directory_for(OWNER, false)),
+ Err(MembershipError::Attester(
+ "the attesting device is not in the directory"
+ ))
+ );
+ }
+
+ #[test]
+ fn a_revoked_device_may_not_attest_a_roster() {
+ // Stricter than the upgrade intent: the entry is retained so old manifests verify, not
+ // so the device can keep issuing new documents.
+ let signed = SignedAlbumRoster::sign(roster(), &dsk()).expect("signs");
+ assert_eq!(
+ signed.verify(&directory_for(OWNER, true)),
+ Err(MembershipError::Attester("the attesting device is revoked"))
+ );
+ }
+
+ #[test]
+ fn a_tampered_member_role_does_not_verify() {
+ let mut signed = SignedAlbumRoster::sign(roster(), &dsk()).expect("signs");
+ signed.roster.members[1].role = MemberRole::Writer;
+ assert_eq!(
+ signed.verify(&directory_for(OWNER, false)),
+ Err(MembershipError::Attester(
+ "the attester's DSK signature does not verify"
+ ))
+ );
+ }
+
+ #[test]
+ fn a_tampered_version_or_epoch_does_not_verify() {
+ let refused = Err(MembershipError::Attester(
+ "the attester's DSK signature does not verify",
+ ));
+ let mut bumped = SignedAlbumRoster::sign(roster(), &dsk()).expect("signs");
+ bumped.roster.roster_version = 2;
+ assert_eq!(bumped.verify(&directory_for(OWNER, false)), refused);
+
+ let mut rolled = SignedAlbumRoster::sign(roster(), &dsk()).expect("signs");
+ rolled.roster.amk_epoch = AmkVersion(2);
+ assert_eq!(rolled.verify(&directory_for(OWNER, false)), refused);
+ }
+
+ #[test]
+ fn a_signature_by_the_wrong_key_does_not_verify() {
+ // The right device id, the wrong DSK: what a member forging the owner's attestation
+ // looks like.
+ let signed = SignedAlbumRoster::sign(roster(), &other_dsk()).expect("signs");
+ assert_eq!(
+ signed.verify(&directory_for(OWNER, false)),
+ Err(MembershipError::Attester(
+ "the attester's DSK signature does not verify"
+ ))
+ );
+ }
+
+ #[test]
+ fn roles_encode_as_their_snake_case_tokens() {
+ let bytes = crate::cbor::to_canonical_vec(&MemberRole::Reader).expect("encodes");
+ let value: ciborium::Value = ciborium::from_reader(bytes.as_slice()).expect("decodes");
+ assert_eq!(value, ciborium::Value::Text("reader".into()));
+ }
+}
diff --git a/capsule-core/src/crypto/mod.rs b/capsule-core/src/crypto/mod.rs
index 5bb63611..39ba6f06 100644
--- a/capsule-core/src/crypto/mod.rs
+++ b/capsule-core/src/crypto/mod.rs
@@ -8,6 +8,7 @@
//! ```text
//! hash · primitives · rng · kdf · pwkdf (foundation, no internal deps)
//! └─ keys ─ encryption (key hierarchy + AEAD)
+//! └─ keys ─ membership (the owner-signed album roster)
//! └─ authority ─┐
//! └─ provenance ┴─ verify_asset (the single acknowledgement chokepoint)
//! ```
@@ -21,6 +22,7 @@ pub mod encryption;
pub mod hash;
pub mod kdf;
pub mod keys;
+pub mod membership;
pub mod primitives;
pub mod provenance;
pub mod pwkdf;
diff --git a/capsule-core/src/crypto/provenance/manifest.rs b/capsule-core/src/crypto/provenance/manifest.rs
index b859603a..46330d84 100644
--- a/capsule-core/src/crypto/provenance/manifest.rs
+++ b/capsule-core/src/crypto/provenance/manifest.rs
@@ -126,9 +126,26 @@ pub struct ManifestCore {
/// `delete | derivative-* | trash-restore`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata_blob_hash: Option,
- /// User who produced the asset.
+ /// The account whose device signed **this record**.
+ ///
+ /// Per-record, not per-asset: a `delete` written by a second device — or by a member of a
+ /// shared album — names that writer, not the account that created the asset. The asset's
+ /// original creator is recoverable from the `create` record at the head of the append-only
+ /// provenance chain, which is where it belongs.
+ ///
+ /// The pairing with [`Self::created_by_device`] is load-bearing rather than descriptive:
+ /// [`verify_asset`](crate::crypto::verify_asset::verify_asset) resolves the device *inside
+ /// this account's* published directory (step 6) and verifies [`AssetManifest::device_sig`]
+ /// under that entry's key (step 8), so a record naming anyone but its own signer cannot
+ /// verify. Album write authority is decided separately, by `write_sig` at step 10.
pub created_by_user: Uuid,
- /// Device that produced the asset (resolved in the device directory).
+ /// The device that signed **this record**, resolved in [`Self::created_by_user`]'s directory.
+ ///
+ /// Per-record for the same reason and with the same consequence: it must be the device whose
+ /// DSK produced [`AssetManifest::device_sig`], or step 8 of
+ /// [`verify_asset`](crate::crypto::verify_asset::verify_asset) rejects the manifest. The
+ /// server mirrors the resolvable half key-free as invariant 7 — the device must be in the
+ /// *calling* account's published directory, with `added_at` before the manifest's timestamp.
pub created_by_device: Uuid,
/// Producing client version string.
pub client_version: String,
diff --git a/capsule-core/src/lifecycle/provenance.rs b/capsule-core/src/lifecycle/provenance.rs
index a71d6002..d5331d00 100644
--- a/capsule-core/src/lifecycle/provenance.rs
+++ b/capsule-core/src/lifecycle/provenance.rs
@@ -23,6 +23,27 @@ impl Workspace {
/// fields. Used for metadata-update / delete / trash-restore. `metadata_blob_hash` is set
/// explicitly per the presence-by-action rule (`Some` for a metadata-update that seals a
/// fresh blob, `None` for delete / trash-restore) rather than inherited from `base`.
+ ///
+ /// # Who a continuation names, and why it cannot be the creator
+ ///
+ /// `created_by_user` / `created_by_device` name the **signer of this record**, re-minted per
+ /// write like `timestamp` and `client_version` — never inherited from `base`.
+ ///
+ /// That is not a preference, it is what
+ /// [`verify_asset`] requires: it resolves
+ /// `created_by_device` *inside `created_by_user`'s* published directory (step 6) and then
+ /// verifies `device_sig` under **that entry's** key (step 8). A record naming a device that
+ /// did not sign it fails step 8 and is unverifiable by every reader. Inheriting the pair
+ /// therefore broke the ordinary two-device case — device B deleting an asset created on
+ /// device A produced a manifest claiming A and signed by B — as well as every write by a
+ /// shared album's member.
+ ///
+ /// Album authority is a separate check and is unaffected: step 10 verifies `write_sig`
+ /// under the epoch's attested write-tier key, so naming the acting member as this record's
+ /// author does not weaken the owner's album.
+ ///
+ /// The asset's original creator stays recoverable where it always was — the `create` record
+ /// at the head of the provenance chain, which is append-only.
fn sign_lifecycle(
&self,
album: &AlbumKeys,
@@ -38,6 +59,11 @@ impl Workspace {
retention_until,
metadata_blob_hash,
timestamp: now_rfc3339(),
+ // This record's signer, not the asset's creator — see the doc comment above. The
+ // same pair every create path writes (`import.rs`, `drops.rs`, `drop/mod.rs`), for
+ // the same reason: it is the device whose DSK signs the bytes below.
+ created_by_user: self.account.user_id,
+ created_by_device: self.account.device.device_id,
// Each write records the exact client build that produced *this* record (S-D15), not
// the creator's — so an edit by a different client identifies itself in the chain.
client_version: self.client_version.clone(),
@@ -210,7 +236,7 @@ mod tests {
use super::super::fast_workspace;
use super::*;
- use crate::crypto::keys::Amk;
+ use crate::crypto::keys::{Amk, HybridSigningKey};
/// S-A3: the `Workspace` populates `metadata_blob_hash` per the sealing order, the sidecar
/// binds to the manifest through the prior head, and a one-byte sidecar mutation quarantines.
@@ -307,4 +333,151 @@ mod tests {
);
assert!(del.structural_ok());
}
+
+ /// Re-point `ws` at a different signing device — and optionally a different **account** —
+ /// publishing a directory that holds it. What a second phone, or a shared album's member,
+ /// looks like to everything below the signer.
+ fn become_device(
+ ws: &mut Workspace,
+ account: Option<(Uuid, HybridSigningKey)>,
+ device_id: Uuid,
+ dsk: HybridSigningKey,
+ ) {
+ use crate::crypto::keys::{DeviceEntry, DirectoryCore};
+
+ let entry = DeviceEntry {
+ device_id,
+ dsk_public: dsk.verifying_key(),
+ dek_public: None,
+ // Must precede any manifest it signs; the workspace stamps `now`.
+ added_at: "2020-01-01T00:00:00Z".into(),
+ revoked_at: None,
+ };
+ ws.directory = match account {
+ // A second device of the *same* account: appended to the account's own directory,
+ // which is re-signed by the account IK at a higher version.
+ None => {
+ let mut core = ws.directory.core.clone();
+ core.directory_version += 1;
+ core.devices.push(entry);
+ core.sign(&ws.account.user_ik)
+ }
+ // A different account entirely: its own directory, under its own IK.
+ Some((user_id, ref ik)) => {
+ let directory = DirectoryCore {
+ user_id,
+ directory_version: 1,
+ updated_at: now_rfc3339(),
+ devices: vec![entry],
+ }
+ .sign(ik);
+ ws.account.user_id = user_id;
+ directory
+ }
+ };
+ ws.account.device.device_id = device_id;
+ ws.device_signer = Box::new(dsk);
+ }
+
+ fn imported(lib: &TempDir, src: &TempDir) -> (Workspace, Uuid, Uuid) {
+ let img = src.path().join("photo.jpg");
+ fs::write(&img, b"\xFF\xD8\xFF continuation-authorship bytes").unwrap();
+ let mut ws = fast_workspace(lib.path());
+ let album = ws.create_album("Trip").unwrap();
+ let asset = ws.import_asset(album, &img).unwrap();
+ (ws, album, asset)
+ }
+
+ /// **A second device of the same account continues a chain, and the result verifies.**
+ ///
+ /// The case `sign_lifecycle` used to break outright: it inherited `created_by_user` and
+ /// `created_by_device` from the chain head while signing with the *current* device, so a
+ /// delete from device B claimed device A and failed `verify_asset` step 8 — the device
+ /// signature does not verify under the named entry's key. Ordinary two-device use, no
+ /// sharing required.
+ #[test]
+ fn a_continuation_from_a_second_device_names_it_and_verifies() {
+ let (lib, src) = (TempDir::new().unwrap(), TempDir::new().unwrap());
+ let (mut ws, _album, asset) = imported(&lib, &src);
+
+ let creator = ws.account.device.device_id;
+ let second = Uuid::from_u128(0xD2);
+ become_device(
+ &mut ws,
+ None,
+ second,
+ HybridSigningKey::from_seed_bytes(&[9; 32], &[10; 32]),
+ );
+
+ ws.soft_delete(&asset, 30).unwrap();
+
+ let st = ws.asset(&asset).unwrap();
+ let head = &st.chain.records().last().unwrap().manifest;
+ assert_eq!(head.core.action, Action::Delete);
+ assert_eq!(
+ head.core.created_by_device, second,
+ "the continuation names the device that signed it"
+ );
+ assert_ne!(
+ head.core.created_by_device, creator,
+ "and not the one that created the asset"
+ );
+ assert_eq!(
+ ws.verify(&asset).unwrap(),
+ VerifyOutcome::Accept,
+ "which is the only reason it can verify at all"
+ );
+
+ // The creator is not lost — it is where the append-only chain keeps it.
+ assert_eq!(
+ st.chain.records()[0].manifest.core.created_by_device,
+ creator
+ );
+ assert_eq!(st.chain.records()[0].manifest.core.action, Action::Create);
+ }
+
+ /// **A member of a shared album continues the owner's chain under the member's own account**,
+ /// and it verifies against the *member's* directory.
+ ///
+ /// The write-tier signature is what carries album authority (step 10) and it is unaffected:
+ /// the member holds the epoch's write-tier key, which is what membership *is*. Naming the
+ /// acting member as the record's author therefore does not weaken the owner's album — it is
+ /// the only way the record can be verified by anyone.
+ #[test]
+ fn a_members_continuation_verifies_under_the_members_own_directory() {
+ let (lib, src) = (TempDir::new().unwrap(), TempDir::new().unwrap());
+ let (mut ws, _album, asset) = imported(&lib, &src);
+
+ let owner = ws.account.user_id;
+ let member = Uuid::from_u128(0xB0B);
+ become_device(
+ &mut ws,
+ Some((
+ member,
+ HybridSigningKey::from_seed_bytes(&[11; 32], &[12; 32]),
+ )),
+ Uuid::from_u128(0xD3),
+ HybridSigningKey::from_seed_bytes(&[13; 32], &[14; 32]),
+ );
+
+ ws.soft_delete(&asset, 30).unwrap();
+
+ let st = ws.asset(&asset).unwrap();
+ let head = &st.chain.records().last().unwrap().manifest;
+ assert_eq!(
+ head.core.created_by_user, member,
+ "a member's write is authored by the member"
+ );
+ assert_ne!(head.core.created_by_user, owner);
+ assert_eq!(
+ ws.verify(&asset).unwrap(),
+ VerifyOutcome::Accept,
+ "verified under the member's directory, against the owner's album authority"
+ );
+ assert_eq!(
+ st.chain.records()[0].manifest.core.created_by_user,
+ owner,
+ "and the album's asset is still the owner's creation"
+ );
+ }
}
diff --git a/capsule-docs/src/content/docs/design/api-surfaces.md b/capsule-docs/src/content/docs/design/api-surfaces.md
index b3d664da..b7bcc6c1 100644
--- a/capsule-docs/src/content/docs/design/api-surfaces.md
+++ b/capsule-docs/src/content/docs/design/api-surfaces.md
@@ -21,6 +21,7 @@ the gate that keeps it current — is [Developer Documentation](/design/develope
| Authentication (sessions, TOTP, OIDC) | REST | `capsule-server::auth` | [Authentication](/design/authentication/) |
| Resumable upload (`POST /v1/upload`, then `HEAD/PATCH /v1/upload/{id}`) | REST | `capsule-server::upload` | [Upload Protocol](/design/import/upload-protocol/) |
| Lifecycle writes (`POST /v1/albums/{album_id}/ops`) | REST | `capsule-server::routes::ops` | [Authorization](/design/authorization/#the-lifecycle-write-surface) |
+| Album roster publish (`PUT /v1/albums/{album_id}/roster`) | REST | `capsule-server::membership` | [Threat Model — Validation](/design/threat-model/validation/) (invariant 33) |
| Blob fetch (`GET /v1/blob/{hash}`, HTTP `Range`) | REST | `capsule-server::blob` | [Download & Sync](/design/import/download-sync/) |
| Sync feed (change discovery after a cursor) | REST | `capsule-server::sync` | [Download & Sync](/design/import/download-sync/) |
| Federation pull | REST | `capsule-server::federation` | [Federation](/design/federation/) |
@@ -123,12 +124,46 @@ Every public route applies the same headers:
| Header | Direction |
| --- | --- |
-| `X-Capsule-Protocol` | request |
-| `X-Capsule-Crypto-Suite` | request for writes |
-| `X-Capsule-Sidecar-Schema` | request |
-| `X-Capsule-Protocol-Min` | response |
-| `X-Capsule-Protocol-Max` | response |
-| `X-Capsule-Min-Client-Build` | response |
+| `X-Capsule-Protocol` | request, required on every gated route |
+| `X-Capsule-Crypto-Suite` | request for writes; validated when present |
+| `X-Capsule-Sidecar-Schema` | request on metadata updates; validated when present |
+| `X-Capsule-Protocol-Min` | response, on every response of every operation |
+| `X-Capsule-Protocol-Max` | response, on every response of every operation |
+| `X-Capsule-Min-Client-Build` | response, on every response of every operation; advisory (`0.0.0` = no cutoff) |
+
+The carriage is two Kynos interceptors in `capsule-server/src/negotiation.rs`, and the split
+is the point: `Negotiation` is mounted on the whole router, outside everything that can refuse,
+so the three response headers ride a `413`, a `401` and a `426` exactly as they ride a `200`
+(an unrouted `404`/`405` is the router's own and carries none — Kynos runs interceptors per
+operation, after routing);
+the gate is two `Group`s — `ProtocolGate` holding every non-safe operation and
+`ProtocolReadGate` every gated `GET`/`HEAD` — so an operation is gated by being mounted inside
+one and exempt by being mounted outside both. The two gates are the two halves of the
+fail-closed rules: a **write** with a grammatical `X-Capsule-Protocol` outside `[Min, Max]` is
+`426`; a **read** with the same header is admitted ("reads of any past version succeed" — and a
+future date on a read is admitted too, since the rule is the grammar and nothing else), and a
+missing or malformed header is `400 error.request.malformed` on every gated operation. All
+three read one protocol window — the upload policy's, built from `PROTOCOL_MIN`/`PROTOCOL_MAX`
+at boot — so the window a client is told and the window it is held to cannot be two numbers. A
+`426` carries the window on the headers and the stable `error.protocol.version_unsupported` code
+in the body; nothing restates the window as a body member.
+
+**Exempt from the request gate** (and still carrying the response headers), ten operations:
+
+- `GET /v1/version` — the reachability probe a client hits before it 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, read before any handshake.
+- `GET /s/{opaque_id}`, `GET /s/{opaque_id}/wrapped-secret`, `GET /s/{opaque_id}/blob/{hash}` —
+ [Share Links](/design/share-links/) requires an indistinguishable `404` there, and a `426`
+ would be a probing oracle.
+- `POST /d/{opaque_id}`, `PATCH /d/{opaque_id}/{upload_id}` — the link record pins
+ `protocol_version` and `crypto_suite_id` at issuance ([Web Upload](/design/web-upload/)), so a
+ browser guest has nothing to assert.
+
+`capsule-server/tests/conformance.rs` pins both the gated set and this exempt set against the
+emitted document, and walks every operation on the wire, so a route cannot join or leave the
+gate by accident.
Credentials use `Authorization: Bearer`. Session access tokens and federation capabilities are
different token types verified by their owning modules, even though both use the standard HTTP
diff --git a/capsule-docs/src/content/docs/design/authorization.md b/capsule-docs/src/content/docs/design/authorization.md
index 40ce4185..c97fb09d 100644
--- a/capsule-docs/src/content/docs/design/authorization.md
+++ b/capsule-docs/src/content/docs/design/authorization.md
@@ -48,6 +48,12 @@ The endpoint is deliberately singular — one closed enum, one gate, one transac
The transport row lives in [API Surfaces](/design/api-surfaces/#surface--transport-map). Implementation is planned in `capsule-server::routes::ops` (slice `S-C16`, reusing the upload server's envelope gate).
+## Album Membership on the Server
+
+Album sharing between accounts is an MLS group whose roster the server cannot read, by design. What the server holds instead is the album owner's **signed roster** — the whole member list, each with a role of `reader` or `writer`, under a strictly monotonic `roster_version` and the AMK epoch it reflects, signed by a non-revoked device in the owner's published device directory and published at `PUT /v1/albums/{album_id}/roster` (slice `S-C51`; [invariant 33](/design/threat-model/validation/#server-side-validation-invariants)). Only the owner account publishes; removal is a later roster that omits the member, and the server records the version and epoch at which they vanished rather than deleting the row. The version is bounded above as well as below — at most sixteen past the held one — because monotonicity alone would let a single publish latch the counter where nothing could supersede it and freeze the album's membership permanently; the refusal names the held version, and re-signing the same roster one above it says exactly the same thing.
+
+That stored fact widens two decisions that were owner-only until it existed. A **writer** on the current roster may write to the album through the upload path and `POST /v1/albums/{album_id}/ops`; the write is filed under the *owner's* namespace — the owner's feed is the one every member's devices read — and billed to the uploader. Any account on the roster, in either role, may fetch the album's blobs; a **former** member receives the `403` [Download & Sync](/design/import/download-sync/) describes; an account the roster never named gets the same `404` an unknown address gets. The roster is a transport control over who the server serves, never a confidentiality control: the server executes what the owner's signed statement permits, and, as below, authorizes nothing itself.
+
## The Server Executes But Never Authorizes
Per the principle of [trusting the server for storage, never for authorization](/design/cryptography/), the server **carries out** a remote delete or replace but is **never** the authority that permits it. A server-asserted lifecycle change with no valid write-tier signature is rejected by every client. This bounds the damage a compromised or buggy server can do: it can refuse to store data, but it cannot forge its destruction.
diff --git a/capsule-docs/src/content/docs/design/import/download-sync.md b/capsule-docs/src/content/docs/design/import/download-sync.md
index a6b2e314..4fa790f6 100644
--- a/capsule-docs/src/content/docs/design/import/download-sync.md
+++ b/capsule-docs/src/content/docs/design/import/download-sync.md
@@ -15,12 +15,15 @@ A client never polls assets individually. It holds a single opaque **sync cursor
| Surface | Transport | Purpose |
| --------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /v1/sync?cursor=…&page_size=…` | Kynos REST | Returns a page of asset changes (created, metadata-updated, deleted) after `cursor`, with a `next_cursor`. The feed is monotonic and resumable. |
+| `GET /v1/sync?album_id=…&cursor=…` | Kynos REST | The same page for one **shared album**, read by its owner or by any account on its current roster (`S-C51`): the owner's sequence filtered to the album, so positions are per-album monotonic; the cursor is bound to `(caller, album)`. Anyone else — never a member, removed, or no such album — receives one `403 error.sync.album_access_denied`. The gaps between an album's positions reveal *how many* changes the owner made elsewhere, and nothing else; that volume-only metadata is accepted rather than paid for with a second, per-album numbering the anti-rewind mark would then have to reconcile. |
| `GET /v1/blob/{hash}` | REST (HTTP `Range`) | Fetch a ciphertext blob by its content address; ranged for resumable and partial reads. |
Each sync entry carries the asset's signed manifest as **the exact bytes the client uploaded** in its [provenance blob](/design/cryptography/provenance/#asset-manifest), the small encrypted **metadata blob**, and the asset's **blob manifest** — the content hashes of its original and derivative blobs — never original or derivative bytes. The manifest is passed through, not rebuilt: the server also holds an envelope projection of those same fields for its own key-free checks, but re-serializing *that* would hand the client a manifest detached from its signatures, which is a manifest no one can verify. Discovering a thousand new assets costs a few hundred kilobytes. The client decrypts each metadata blob, learns the asset's dimensions, capture date, and LQIP, and only *then* decides what else, if anything, to fetch. A deleted or modified asset arrives as a tombstone or an updated metadata reference; the client reconciles local state against it (see [Synchronization Scope](#synchronization-scope)).
**Cursor authenticity.** The opaque sync cursor is **MAC'd by the server** (HMAC-SHA256 — a server-internal construction: the cursor is opaque to clients and never verified by them, so it sits outside the client-facing [primitives inventory](/design/cryptography/primitives/#primitives-inventory)) under a server-only key and verified on every `Sync` call (and [federation pull](/design/federation/#federation-reuses-existing-primitives)), so a client cannot forge or mutate a cursor and a cursor lifted from another context is rejected at the boundary. The MAC is the *authenticity* layer; the per-album monotonic `sync_seq` check below is the independent *anti-rewind* layer. They are separate on purpose: a malicious server can always hand back one of its own *older*, validly-MAC'd cursors, and only the client-held high-water mark defeats that. Together they close the [sync-cursor rewind class](/design/threat-model/scenarios/#damage-scenario--invariant-map).
+**Operator note — deploying `S-C51` invalidates every cursor already issued.** The MAC now covers the *scope* a cursor was minted for (the caller, and the album when the page is an album's), so a cursor minted before that change verifies against different input and is refused as inauthentic. Clients recover by themselves — a refused cursor is a re-sync from zero, the same event a server key rotation is — but the recovery is one full feed read per client, so expect a burst of full-feed traffic on the first sync after the upgrade rather than the usual incremental pages. There is nothing for an operator to migrate: cursors are opaque, stateless and server-minted, and the version byte is deliberately unchanged so an old cursor fails as inauthentic rather than as malformed, which is the answer clients already handle.
+
**Sync feed validation.** Every entry in a `Sync` response carries a `protocol_version` (matching the album's pin) and a per-album monotonic `sync_seq` (a `u64`, strictly increasing per album). The client refuses to apply an entry whose `protocol_version` is above its max known (per the [tightened Postel's Law](/design/principles/#postels-law-asymmetric)) and refuses any page whose `sync_seq` regresses against what the client has already seen for that album — a regressing `sync_seq` indicates a malicious or buggy server attempting to rewind the client's view, and the client surfaces it rather than applying it.
## Stale-Revival Detection
@@ -47,9 +50,9 @@ Because every blob is content-addressed, a fetch is skipped entirely when the bl
**When an above-tier fetch cannot succeed.** A lazily-fetched representation may be temporarily or permanently unavailable. The client distinguishes the two: a **transient** failure (network drop, `5xx`) retries with backoff and resumes via `Range`; a **permanent** failure (`410 Gone`, a purged origin, or an unreachable [federated home server](/design/federation/#robustness-against-connectivity-loss)) **degrades gracefully** to the best representation already in hand. A **`403`** is neither: it signals an *authorization change*, not a durability loss — the client re-syncs its membership/capability state for the album before retrying, and only then degrades (the asset may have been unshared), so a revocation event is surfaced as such rather than masked as a missing file — preview → thumbnail → LQIP, down to the always-present LQIP — and surfaces a non-destructive "full resolution unavailable" state on the asset. It never thrashes the fetch, and it never removes the asset's metadata or local index entry over a missing derivative. The asset stays listed and re-fetches automatically once the representation becomes reachable again.
-**What the home server can actually decide, as of `S-C39`.** `GET /v1/blob/{hash}` is **owner-scoped**: an account fetches the blobs of assets filed under it, and every other caller receives `404` — byte-identical to the answer for an address the server never heard of. That closes a real hole (previously any authenticated account could fetch any live ciphertext whose address it could name) and it fixes the `403`/`404` boundary at the only place that does not leak: a `403` confirms the address is referenced by *somebody*, so it is reserved for a caller the server can see once **had** access, and everyone else is told what an unknown address is told.
+**What the home server decides, as of `S-C39` and `S-C51`.** `GET /v1/blob/{hash}` is **membership-scoped**: an account fetches the blobs of assets filed under it and of every album whose current roster names it, in either role; a **former** member — an account the roster once named and no longer does — receives the `403` above; and every other caller receives `404`, byte-identical to the answer for an address the server never heard of. `S-C39` closed a real hole (previously any authenticated account could fetch any live ciphertext whose address it could name) and fixed the `403`/`404` boundary at the only place that does not leak: a `403` confirms the address is referenced by *somebody*, so it is reserved for a caller the server can see once **had** access, and everyone else is told what an unknown address is told.
-The `403` itself is therefore **not yet rendered on this path**, and the reason is a missing fact rather than missing code. Album sharing between accounts is an MLS group whose roster the server cannot read by design, and the surfaces that do let a non-owner reach ciphertext — `/s/{opaque_id}/blob/{hash}` and the drop paths — serve from their own capabilities and answer `404` when those are withdrawn. Server-side album membership is owed as `S-C51`, and it is the same fact that keeps album *write* access pinned to the owner. Until it lands, a client written against this contract sees the `403` arm only from surfaces that have a capability to revoke.
+The fact behind the `403` is the album owner's **signed roster** (`PUT /v1/albums/{album_id}/roster`), verified against the owner's published device directory and stored as who is a member, since which roster version and AMK epoch, and at which version and epoch a member was removed. The server still cannot read the MLS group — the roster is the owner *telling* it, and it is a transport control over who is handed bytes, never a confidentiality control over who can read them: a former member who kept an epoch's key can still decrypt what they already fetched, which is why the client protocol pairs removal with an AMK epoch bump (the roster carries the new epoch; the server checks only that it does not regress). The surfaces that let a non-account reach ciphertext — `/s/{opaque_id}/blob/{hash}` and the drop paths — serve from their own capabilities and answer `404` when those are withdrawn; they do not route through membership.
## Resumption and Verification
diff --git a/capsule-docs/src/content/docs/design/threat-model/validation.md b/capsule-docs/src/content/docs/design/threat-model/validation.md
index 389c2e57..a62b55be 100644
--- a/capsule-docs/src/content/docs/design/threat-model/validation.md
+++ b/capsule-docs/src/content/docs/design/threat-model/validation.md
@@ -73,6 +73,10 @@ reintroducing the stale revival that 17 exists to catch, in the code enforcing i
- **23.** A published `DeviceDirectory` has `directory_version` **strictly greater** than the version currently stored for that user, and the master signature covers it. A non-advancing or regressing publish is rejected (`409`). The signature is checked against the account's **identity anchor** — the identity public key its first published directory arrived with (in the required `X-Capsule-Identity-Key` header), immutable thereafter. A publish presenting a different key is rejected `403`: the document is well-formed and correctly signed, and signed by somebody else. Owner: [Cryptography — Device Directory](/design/cryptography/keys/#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](/design/moderation/#federated-reporting).
+### On `PUT /v1/albums/{album_id}/roster` (album roster publish)
+
+- **33.** The signed roster decodes as canonical CBOR, is at most 512 KiB, names the album in the path, lists no account twice and does not list the owner (otherwise `400` — every one of these is decidable from the request alone, so nothing store-held is disclosed); the caller is the album's owner (otherwise `404`, the album ceremonies' "not yours is not found"); its `attested_by_user` is the caller and its `attester_sig` verifies under a **non-revoked** device in the caller's published device directory (otherwise `403`); its `roster_version` is **strictly greater** than the version the server holds, or byte-identical to it (a replay), and its `amk_epoch` does not regress (otherwise `409` carrying `current_version`); and it is **at most sixteen above** the held version — an album with no roster reads as version `0` — because a version nothing could ever supersede would freeze the album's membership for good (otherwise `400 error.album.roster_version_leap`, carrying `current_version` and `max_version` so the owner re-signs the same roster one above what is held). The server stores the consequence — who is a member, with what role, since which version and epoch — and marks a member omitted from a later roster as revoked at that roster's version and epoch rather than deleting the row, so a former member's `403` on the blob route is a stored fact. Owner: [Authorization](/design/authorization/).
+
### On any write whose bundle carries a metadata blob
- **25.** The encrypted metadata blob in the bundle has a content hash equal to the manifest's `metadata_blob_hash`. The server holds no key, but it can compare the content address it stores against the value the signed manifest commits to, so a client cannot present the server a metadata blob different from the one its asset manifest is signed over. A mismatch is rejected (`400`) and no state is written. This applies on `POST /v1/upload` (the `create` bundle), at finalization, and on a non-upload `metadata-update`. Owner: [Metadata — Local and Server Metadata Equivalence](/design/metadata/#local-and-server-metadata-equivalence).
@@ -163,6 +167,7 @@ Every write surface has a single idempotency key. Duplicates are no-ops; conflic
| Share-link / upload-link creation | Client-supplied operation id (UUIDv7) | Retried create returns the already-minted link |
| Share-link / upload-link revoke | `link_id` | Second revoke is a no-op |
| Drop adoption (`POST /v1/drops/{drop_id}/adopt`) | `drop_id` — the atomic inbox→album promotion (invariant 32) | A retry after success finds the inbox row gone and returns the already-promoted asset |
+| Album roster publish (`PUT /v1/albums/{album_id}/roster`) | `(album_id, roster_version)` (invariant 33) | Identical bytes: `200` with `replayed: true`, nothing written. Same version, different bytes: `409 error.album.roster_stale` |
A write surface that does not appear here is, by default, **not** idempotent and must be designed before it ships.
diff --git a/capsule-i18n/src/bundles/en.json b/capsule-i18n/src/bundles/en.json
index 318b9021..b1d6be1c 100644
--- a/capsule-i18n/src/bundles/en.json
+++ b/capsule-i18n/src/bundles/en.json
@@ -1828,6 +1828,11 @@
"drop.upload_only_badge": "Upload only",
"error.album.invalid_id": "This album could not be registered because its identifier is malformed.",
"error.album.not_available": "This album isn't available on your account.",
+ "error.album.roster_attester": "Only a device on the album owner's account can publish its roster.",
+ "error.album.roster_malformed": "Capsule couldn't read that album roster.",
+ "error.album.roster_not_found": "That album isn't yours, or doesn't exist.",
+ "error.album.roster_stale": "The roster you sent is out of step with the one the server holds.",
+ "error.album.roster_version_leap": "That roster's version is too far ahead of the one the server holds.",
"error.album.unavailable": "Capsule couldn't set up that album. Please try again.",
"error.album.upgrade_in_flight": "This album is already being upgraded.",
"error.album.upgrade_malformed": "Capsule couldn't read that album upgrade request.",
@@ -1852,6 +1857,7 @@
"error.auth.totp_not_pending": "There is no two-factor setup waiting to be confirmed.",
"error.auth.unavailable": "Capsule couldn't reach your account just now. Please try again.",
"error.auth.user_already_exists": "An account with these details already exists.",
+ "error.blob.access_revoked": "You no longer have access to this album.",
"error.blob.gone": "That photo file is no longer available.",
"error.blob.not_found": "That photo file isn't on the server.",
"error.blob.pending_upload": "The original photo hasn't been uploaded from its device yet.",
@@ -1918,6 +1924,7 @@
"error.storage.deep_rate_limited": "Too many deep storage checks. Please wait and try again.",
"error.storage.invalid_request": "The storage-verification request was malformed.",
"error.storage.unavailable": "Capsule couldn't check whether your photos are safely stored. Please try again.",
+ "error.sync.album_access_denied": "You don't have access to that album.",
"error.sync.cursor_invalid": "The sync session is out of date. Capsule will resync from the start.",
"error.sync.unauthenticated": "Please sign in again to continue syncing.",
"error.sync.unavailable": "Capsule could not reach the server to sync. It will try again.",
diff --git a/capsule-i18n/src/generated.rs b/capsule-i18n/src/generated.rs
index a71cd1ac..1352a76e 100644
--- a/capsule-i18n/src/generated.rs
+++ b/capsule-i18n/src/generated.rs
@@ -38,6 +38,21 @@ pub mod error_codes {
/// `error.album.not_available`
pub const ALBUM_NOT_AVAILABLE: &str = "error.album.not_available";
+ /// `error.album.roster_attester`
+ pub const ALBUM_ROSTER_ATTESTER: &str = "error.album.roster_attester";
+
+ /// `error.album.roster_malformed`
+ pub const ALBUM_ROSTER_MALFORMED: &str = "error.album.roster_malformed";
+
+ /// `error.album.roster_not_found`
+ pub const ALBUM_ROSTER_NOT_FOUND: &str = "error.album.roster_not_found";
+
+ /// `error.album.roster_stale`
+ pub const ALBUM_ROSTER_STALE: &str = "error.album.roster_stale";
+
+ /// `error.album.roster_version_leap`
+ pub const ALBUM_ROSTER_VERSION_LEAP: &str = "error.album.roster_version_leap";
+
/// `error.album.unavailable`
pub const ALBUM_UNAVAILABLE: &str = "error.album.unavailable";
@@ -110,6 +125,9 @@ pub mod error_codes {
/// `error.auth.user_already_exists`
pub const AUTH_USER_ALREADY_EXISTS: &str = "error.auth.user_already_exists";
+ /// `error.blob.access_revoked`
+ pub const BLOB_ACCESS_REVOKED: &str = "error.blob.access_revoked";
+
/// `error.blob.gone`
pub const BLOB_GONE: &str = "error.blob.gone";
@@ -308,6 +326,9 @@ pub mod error_codes {
/// `error.storage.unavailable`
pub const STORAGE_UNAVAILABLE: &str = "error.storage.unavailable";
+ /// `error.sync.album_access_denied`
+ pub const SYNC_ALBUM_ACCESS_DENIED: &str = "error.sync.album_access_denied";
+
/// `error.sync.cursor_invalid`
pub const SYNC_CURSOR_INVALID: &str = "error.sync.cursor_invalid";
diff --git a/capsule-sdk/src/albums.rs b/capsule-sdk/src/albums.rs
index 5e969203..f5d6322c 100644
--- a/capsule-sdk/src/albums.rs
+++ b/capsule-sdk/src/albums.rs
@@ -27,22 +27,27 @@ pub use crate::upload::StaticToken;
// ─── Errors ───────────────────────────────────────────────────────────────────
-/// A failure provisioning an album.
+/// A failure on the album surface: provisioning, or publishing a roster (`S-C51`).
#[derive(Debug, thiserror::Error)]
pub enum AlbumError {
/// The HTTP request failed on the wire, or the session could not authorize it.
- #[error("album provisioning transport: {0}")]
+ #[error("album request transport: {0}")]
Transport(String),
- /// The server refused the provisioning request.
- #[error("album provisioning refused with status {status}")]
+ /// The server refused the request.
+ #[error("album request refused with status {status}")]
Status {
/// The HTTP status code.
status: u16,
/// The stable `error.*` code, when the server supplied one.
code: Option,
+ /// On either roster-version refusal — the `409 error.album.roster_stale` that is behind
+ /// the server, and the `400 error.album.roster_version_leap` that is too far ahead of it
+ /// — the version the server holds, which is the one a caller re-signs above. Absent on
+ /// every other refusal.
+ current_version: Option,
},
/// The response body was missing a field or otherwise unparsable.
- #[error("malformed album provisioning response: {0}")]
+ #[error("malformed album response: {0}")]
Malformed(String),
}
@@ -99,6 +104,9 @@ impl AlbumTransport {
/// Build a transport over a fixed bearer token (tests; callers holding a live token).
/// Same URL layout as [`Self::with_session`].
+ ///
+ /// `http` **must** come from [`crate::net::http_builder`] or [`crate::net::http_client`]: a
+ /// client built any other way sends no protocol handshake, and every gated route refuses it.
pub fn with_static_token(
http: reqwest::Client,
base_url: impl Into,
@@ -126,8 +134,59 @@ impl AlbumTransport {
.map_err(|e| AlbumError::Transport(e.to_string())),
}
}
+
+ /// A `spargen`-generated client for the API root this transport's album endpoint hangs off,
+ /// carrying the same credential.
+ ///
+ /// The roster publish goes through this rather than through [`Self::send`]: everything that
+ /// parses or serializes in this repository is generated, and `publish_album_roster` is a
+ /// JSON operation the document fully describes — request body, success body, and the
+ /// `409`/`400` problem shapes with their extension members. Only `provision` still hand-writes
+ /// its DTOs, and only because `POST /v1/albums` predates this seam.
+ ///
+ /// The root is derived by trimming the endpoint's `/v1/albums` suffix, because the generated
+ /// operations carry their own absolute paths while this transport is constructed with the
+ /// album endpoint (`{origin}/v1/albums`) that `POST {base}` provisions against.
+ fn generated(&self) -> Result {
+ let root = self
+ .base_url
+ .strip_suffix("/v1/albums")
+ .unwrap_or(&self.base_url);
+ let (http, credential) = match &self.auth {
+ AlbumAuth::Session(session) => {
+ let session = session.clone();
+ // The session's own pre-flight refresh and single-flight coalescing, consulted
+ // per request; the reactive `401` replay is the caller's, below.
+ let provider: crate::rest::TokenProvider = std::sync::Arc::new(move || {
+ let session = session.clone();
+ Box::pin(async move {
+ session
+ .bearer()
+ .await
+ .map_err(|error| crate::rest::AuthError::new(error.to_string()))
+ })
+ });
+ (
+ crate::net::http_client()
+ .map_err(|error| AlbumError::Transport(error.to_string()))?,
+ crate::rest::Credential::Provider(provider),
+ )
+ }
+ AlbumAuth::Static { http, token } => (
+ http.clone(),
+ crate::rest::Credential::Bearer(token.clone().into()),
+ ),
+ };
+ Ok(crate::rest::Client::with_client(http, root)
+ .map_err(|error| AlbumError::Transport(error.to_string()))?
+ .with_credential(BEARER_SCHEME, credential))
+ }
}
+/// The security-scheme key the document declares for the bearer JWT; the generated client
+/// attaches the registered credential to every operation whose `security` names it.
+const BEARER_SCHEME: &str = "bearer";
+
// ─── Wire DTOs (mirror the server's transport JSON) ───────────────────────────
/// The `POST /v1/albums` request body. One field, deliberately: the server's body is strict,
@@ -143,6 +202,8 @@ struct ProvisionAlbumResponseWire {
created: bool,
}
+/// The one field `provision` reads off a refusal. The roster publish reads its problems through
+/// the generated client's typed error instead, which is why nothing here describes extensions.
#[derive(Deserialize)]
struct ApiErrorWire {
#[serde(default)]
@@ -159,6 +220,24 @@ pub struct ProvisionedAlbum {
pub created: bool,
}
+/// What the server holds for an album after a roster publish (`S-C51`).
+///
+/// `replayed` is informational: the same bytes again are a success that wrote nothing, exactly
+/// as re-provisioning is, so a client that lost an acknowledgement re-PUTs without branching.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct PublishedRoster {
+ /// The album, echoed.
+ pub album_id: Uuid,
+ /// The roster version the server holds after this call.
+ pub roster_version: u64,
+ /// The AMK epoch that roster reflects.
+ pub amk_epoch: u64,
+ /// How many members it names, the owner excluded.
+ pub member_count: u64,
+ /// Whether this call replayed the roster already held.
+ pub replayed: bool,
+}
+
// ─── Client ───────────────────────────────────────────────────────────────────
/// The album-provisioning client.
@@ -204,6 +283,7 @@ impl AlbumClient {
return Err(AlbumError::Status {
status: status.as_u16(),
code,
+ current_version: None,
});
}
@@ -224,6 +304,165 @@ impl AlbumClient {
created: wire.created,
})
}
+
+ /// Publish `signed` as the roster of the album it names (`S-C51`).
+ ///
+ /// Orchestration only, and deliberately thin: the roster is signed in
+ /// `capsule_core::crypto::membership` by one of the owner's devices, base64-encoded, and
+ /// handed to the **generated** `publish_album_roster` operation, so every byte that is
+ /// parsed or serialized on this path comes from the committed OpenAPI document. Idempotent
+ /// under `(album_id, roster_version)`: the same bytes again succeed with `replayed`.
+ ///
+ /// Two refusals a caller acts on: a `409` (`error.album.roster_stale`) means the server holds
+ /// a roster this one does not supersede, and a `400 error.album.roster_version_leap` means
+ /// the version is too far *ahead* of the held one. Both carry
+ /// [`current_version`](AlbumError::Status), and the repair for both is the same — re-sign the
+ /// roster one above it.
+ ///
+ /// Under a session, a `401` is refreshed once and replayed, exactly as the sync feed does:
+ /// the credential provider's pre-flight refresh cannot cover a token revoked mid-flight.
+ ///
+ /// # Errors
+ ///
+ /// [`AlbumError::Transport`] when the request did not complete, [`AlbumError::Status`] with
+ /// the server's `error.*` code when it was refused, [`AlbumError::Malformed`] when the roster
+ /// could not be encoded or the response could not be read.
+ #[instrument(skip(self, signed), fields(album_id = %signed.roster.album_id, roster_version = signed.roster.roster_version))]
+ pub async fn publish_roster(
+ &self,
+ signed: &capsule_core::crypto::membership::SignedAlbumRoster,
+ ) -> Result {
+ use base64::Engine as _;
+
+ let album_id = signed.roster.album_id;
+ let bytes = capsule_core::cbor::to_canonical_vec(signed)
+ .map_err(|e| AlbumError::Malformed(format!("roster encoding: {e}")))?;
+ let body = crate::rest::types::RosterRequest {
+ roster_cbor: base64::engine::general_purpose::STANDARD.encode(bytes),
+ };
+
+ let client = self.transport.generated()?;
+ let wire = match publish(&client, album_id, &body).await {
+ Ok(wire) => wire,
+ Err(error) if is_unauthenticated(&error) => match &self.transport.auth {
+ AlbumAuth::Session(session) => {
+ tracing::info!("the roster publish answered 401; refreshing once and retrying");
+ session.refresh().await?;
+ publish(&client, album_id, &body)
+ .await
+ .map_err(publish_refusal)?
+ }
+ // A fixed token cannot be refreshed, so retrying would ask the same question
+ // twice.
+ AlbumAuth::Static { .. } => return Err(publish_refusal(error)),
+ },
+ Err(error) => return Err(publish_refusal(error)),
+ };
+
+ let echoed = Uuid::parse_str(&wire.album_id)
+ .map_err(|e| AlbumError::Malformed(format!("response album_id: {e}")))?;
+ if echoed != album_id {
+ return Err(AlbumError::Malformed(format!(
+ "server echoed album {echoed}, not the requested {album_id}"
+ )));
+ }
+ tracing::info!(
+ roster_version = wire.roster_version,
+ replayed = wire.replayed,
+ "album roster published"
+ );
+ Ok(PublishedRoster {
+ album_id: echoed,
+ roster_version: counter(wire.roster_version, "roster_version")?,
+ amk_epoch: counter(wire.amk_epoch, "amk_epoch")?,
+ member_count: counter(wire.member_count, "member_count")?,
+ replayed: wire.replayed,
+ })
+ }
+}
+
+/// One call of the generated roster operation.
+///
+/// The protocol date is a required parameter of every gated operation in the document, so the
+/// generated signature asks for it; the value is this build's own, the same one the transport
+/// sends as a default header.
+async fn publish(
+ client: &crate::rest::Client,
+ album_id: Uuid,
+ body: &crate::rest::types::RosterRequest,
+) -> Result<
+ crate::rest::types::RosterResponse,
+ crate::rest::Error,
+> {
+ Ok(client
+ .publish_album_roster(
+ album_id.hyphenated().to_string(),
+ capsule_core::crypto::primitives::PROTOCOL_VERSION,
+ None,
+ body,
+ )
+ .await?
+ .into_inner())
+}
+
+/// Whether the refusal was the credential's.
+fn is_unauthenticated(error: &crate::rest::Error) -> bool {
+ matches!(
+ error,
+ crate::rest::Error::Api(response)
+ if matches!(
+ response.inner(),
+ crate::rest::PublishAlbumRosterError::Status401(_)
+ )
+ )
+}
+
+/// Map the generated operation's typed error onto this module's.
+///
+/// The `code` is what a caller switches on, and `current_version` is what the two version
+/// refusals — the `409` that is behind and the `400` that is too far ahead — both carry so the
+/// caller can re-sign one above what the server holds.
+fn publish_refusal(error: crate::rest::Error) -> AlbumError {
+ use crate::rest::PublishAlbumRosterError as Refusal;
+
+ let crate::rest::Error::Api(response) = error else {
+ return AlbumError::Transport(error.to_string());
+ };
+ let status = response.status().as_u16();
+ let (code, current_version) = match response.into_inner() {
+ Refusal::Status400(problem) => (
+ Some(problem.code.clone()),
+ problem
+ .current_version
+ .and_then(|held| u64::try_from(held).ok()),
+ ),
+ Refusal::Status409(problem) => (
+ Some(problem.code.clone()),
+ problem
+ .current_version
+ .and_then(|held| u64::try_from(held).ok()),
+ ),
+ // The body-less refusal: a request past the transport's size backstop.
+ Refusal::Status413 => (None, None),
+ Refusal::Status401(problem)
+ | Refusal::Status403(problem)
+ | Refusal::Status404(problem)
+ | Refusal::Status415(problem)
+ | Refusal::Status422(problem)
+ | Refusal::Status426(problem)
+ | Refusal::Status500(problem) => (Some(problem.code.clone()), None),
+ };
+ tracing::warn!(status, ?code, ?current_version, "roster publish refused");
+ AlbumError::Status {
+ status,
+ code,
+ current_version,
+ }
+}
+
+/// A counter the document types as a signed integer, as the SDK speaks it.
+fn counter(value: i64, field: &str) -> Result {
+ u64::try_from(value).map_err(|_| AlbumError::Malformed(format!("{field}: {value} is negative")))
}
#[cfg(test)]
diff --git a/capsule-sdk/src/albums/tests.rs b/capsule-sdk/src/albums/tests.rs
index 16bb318b..021a1c3b 100644
--- a/capsule-sdk/src/albums/tests.rs
+++ b/capsule-sdk/src/albums/tests.rs
@@ -19,6 +19,13 @@
//! | `a_malformed_id_carries_the_invalid_id_code` | the 400 path |
//! | `an_echoed_mismatch_is_malformed` | the server cannot silently rebind another album |
//! | `the_request_is_authorized` | the bearer rides every call |
+//! | `publish_roster_sends_the_signed_bytes_verbatim` | the roster on the wire is the one the device signed (`S-C51`) |
+//! | `a_published_roster_reports_what_the_server_holds` | the success mapping, replay included |
+//! | `a_stale_roster_carries_the_distinct_code` | the `409` is switchable by code |
+//! | `a_roster_echo_mismatch_is_malformed` | the server cannot silently answer for another album |
+//! | `a_version_leap_carries_the_held_version_too` | the `400` too-far-ahead refusal is switchable and carries `current_version` |
+//! | `the_widest_version_the_server_can_name_still_decodes` | the recovery hint survives at the top of the server's range, where an unbounded counter would not |
+//! | `the_roster_publish_goes_through_the_generated_operation` | the path, method and body come from the committed contract, not from a hand-written request |
use std::sync::{Arc, Mutex};
@@ -236,3 +243,261 @@ async fn the_request_is_authorized() {
"every provisioning call rides the caller's bearer"
);
}
+
+// ─── Roster publish (`S-C51`) ─────────────────────────────────────────────────
+
+/// A roster for [`album`] at `version`, signed by a fresh device key.
+fn signed_roster(version: u64) -> capsule_core::crypto::membership::SignedAlbumRoster {
+ use capsule_core::crypto::keys::{AmkVersion, HybridSigningKey};
+ use capsule_core::crypto::membership::{AlbumRoster, MemberRole, RosterMember};
+
+ let roster = AlbumRoster {
+ album_id: album(),
+ roster_version: version,
+ amk_epoch: AmkVersion(2),
+ attested_by_user: Uuid::from_u128(0xA11CE),
+ attested_by_device: Uuid::from_u128(0xD1),
+ attested_at: "2026-09-02T00:00:00Z".to_owned(),
+ members: vec![RosterMember {
+ user_id: Uuid::from_u128(0xB0B),
+ role: MemberRole::Writer,
+ }],
+ };
+ capsule_core::crypto::membership::SignedAlbumRoster::sign(
+ roster,
+ &HybridSigningKey::from_seed_bytes(&[7; 32], &[8; 32]),
+ )
+ .expect("a roster signs")
+}
+
+/// The canonical success body the server sends for a roster publish.
+fn held(id: Uuid, version: u64, replayed: bool) -> MockResponse {
+ MockResponse::new(200, "OK").json_body(format!(
+ r#"{{"album_id":"{id}","roster_version":{version},"amk_epoch":2,"member_count":1,"replayed":{replayed}}}"#
+ ))
+}
+
+/// **The bytes on the wire are the bytes the device signed.** The client base64-encodes the
+/// canonical CBOR and changes nothing: a re-serialization would be a roster whose signature no
+/// longer verifies, and the server decides a replay on these exact bytes.
+#[tokio::test]
+async fn publish_roster_sends_the_signed_bytes_verbatim() {
+ use base64::Engine as _;
+
+ let id = album();
+ let signed = signed_roster(3);
+ let (server, seen) = recording(move |_| held(id, 3, false)).await;
+ // At the production layout — `{origin}/v1/albums` — so the path is the server's own.
+ let client = AlbumClient::new(AlbumTransport::with_static_token(
+ reqwest::Client::new(),
+ format!("{}/v1/albums", server.base_url().trim_end_matches('/')),
+ StaticToken("test-token".into()),
+ ));
+ client.publish_roster(&signed).await.expect("publish");
+
+ let requests = seen.lock().expect("recorded requests");
+ assert_eq!(requests[0].method, "PUT");
+ assert_eq!(
+ requests[0].path,
+ format!("/v1/albums/{}/roster", id.hyphenated())
+ );
+ let body: serde_json::Value = serde_json::from_slice(&requests[0].body).expect("JSON");
+ let object = body.as_object().expect("a JSON object");
+ assert_eq!(object.keys().collect::>(), vec!["roster_cbor"]);
+ let bytes = base64::engine::general_purpose::STANDARD
+ .decode(body["roster_cbor"].as_str().expect("a string"))
+ .expect("standard base64");
+ let expected = capsule_core::cbor::to_canonical_vec(&signed).expect("encodes");
+ assert_eq!(
+ bytes, expected,
+ "the wire carries the canonical encoding of exactly what was signed"
+ );
+ assert_eq!(
+ capsule_core::cbor::canonicalize(&bytes).expect("decodes"),
+ bytes,
+ "and those bytes are canonical, which is the form the server stores and replays on"
+ );
+ assert_eq!(
+ body["roster_cbor"].as_str().expect("a string"),
+ base64::engine::general_purpose::STANDARD.encode(&expected),
+ "standard base64 with padding, the alphabet the server decodes"
+ );
+ assert!(
+ requests[0].header("authorization").is_some(),
+ "the bearer rides the roster publish too"
+ );
+}
+
+#[tokio::test]
+async fn a_published_roster_reports_what_the_server_holds() {
+ let id = album();
+ let (server, _) = recording(move |_| held(id, 3, true)).await;
+ let result = client_for(&server)
+ .publish_roster(&signed_roster(3))
+ .await
+ .expect("publish");
+ assert_eq!(
+ result,
+ PublishedRoster {
+ album_id: id,
+ roster_version: 3,
+ amk_epoch: 2,
+ member_count: 1,
+ replayed: true,
+ }
+ );
+}
+
+/// The `409` is the one refusal a client acts on differently — re-sync and republish above the
+/// version the server names — so its code must come through.
+#[tokio::test]
+async fn a_stale_roster_carries_the_distinct_code() {
+ let (server, _) = recording(|_| {
+ MockResponse::new(409, "Conflict").json_body(
+ r#"{"type":"about:blank","title":"Roster stale","status":409,"detail":"the server holds roster version 4, which this does not supersede","code":"error.album.roster_stale","current_version":4}"#.to_owned(),
+ )
+ })
+ .await;
+ let error = client_for(&server)
+ .publish_roster(&signed_roster(3))
+ .await
+ .expect_err("a stale roster is refused");
+ assert_eq!(error.error_code(), Some(error_codes::ALBUM_ROSTER_STALE));
+ assert!(
+ matches!(
+ error,
+ AlbumError::Status {
+ status: 409,
+ current_version: Some(4),
+ ..
+ }
+ ),
+ "the held version rides the refusal, so the caller can republish above it: {error:?}"
+ );
+}
+
+/// A server answering for a different album than the one asked about is a malformed answer,
+/// not a success with the wrong id in it.
+#[tokio::test]
+async fn a_roster_echo_mismatch_is_malformed() {
+ let other = Uuid::parse_str("0198f3c2-9c4a-7b3d-8f21-4d7c9a1b2eff").expect("a uuid");
+ let (server, _) = recording(move |_| held(other, 3, false)).await;
+ let error = client_for(&server)
+ .publish_roster(&signed_roster(3))
+ .await
+ .expect_err("a mismatched echo is refused");
+ assert!(matches!(error, AlbumError::Malformed(_)), "{error:?}");
+}
+
+/// The other version refusal: a roster so far *ahead* of the held one that the server would be
+/// latched if it took it. A `400` rather than the `409`, and it carries the held version for the
+/// same reason — the caller re-signs one above it.
+#[tokio::test]
+async fn a_version_leap_carries_the_held_version_too() {
+ let (server, _) = recording(|_| {
+ MockResponse::new(400, "Bad Request").json_body(
+ r#"{"type":"about:blank","title":"Roster version leap","status":400,"detail":"roster version 9999 is past 17","code":"error.album.roster_version_leap","declared":9999,"current_version":1,"max_version":17}"#.to_owned(),
+ )
+ })
+ .await;
+ let error = client_for(&server)
+ .publish_roster(&signed_roster(9999))
+ .await
+ .expect_err("a leap is refused");
+ assert_eq!(
+ error.error_code(),
+ Some(error_codes::ALBUM_ROSTER_VERSION_LEAP)
+ );
+ assert!(
+ matches!(
+ error,
+ AlbumError::Status {
+ status: 400,
+ current_version: Some(1),
+ ..
+ }
+ ),
+ "the held version rides this refusal too: {error:?}"
+ );
+}
+
+/// **The wire shape is the contract's, not this module's.** `publish_roster` is orchestration
+/// over the generated `publish_album_roster` operation, so the method, the path and the required
+/// protocol header come from `capsule-server/openapi.json` rather than from a hand-written
+/// request this module could drift.
+#[tokio::test]
+async fn the_roster_publish_goes_through_the_generated_operation() {
+ let id = album();
+ let (server, seen) = recording(move |_| held(id, 3, false)).await;
+ let client = AlbumClient::new(AlbumTransport::with_static_token(
+ reqwest::Client::new(),
+ format!("{}/v1/albums", server.base_url().trim_end_matches('/')),
+ StaticToken("test-token".into()),
+ ));
+ client
+ .publish_roster(&signed_roster(3))
+ .await
+ .expect("publish");
+
+ let requests = seen.lock().expect("recorded requests");
+ assert_eq!(requests[0].method, "PUT");
+ assert_eq!(
+ requests[0].path,
+ format!("/v1/albums/{}/roster", id.hyphenated())
+ );
+ assert_eq!(
+ requests[0].header("x-capsule-protocol"),
+ Some(capsule_core::crypto::primitives::PROTOCOL_VERSION),
+ "the generated operation carries the protocol date the document declares required"
+ );
+ assert_eq!(
+ requests[0].header("authorization"),
+ Some("Bearer test-token")
+ );
+}
+
+/// **The refusal has to survive at the top of the server's range.**
+///
+/// spargen lowers every integer in the contract as `i64` and emits no `u64` at all, so a
+/// counter above `i64::MAX` would not be an API error at all — the generated client would fail
+/// to *decode* the body, the typed error would never be built, and `code` and `current_version`
+/// would be replaced by an undifferentiated transport failure. The server bounds every counter
+/// it can emit at `MAX_ROSTER_VERSION` (`i64::MAX`) precisely so this holds; the case pins the
+/// boundary rather than a comfortable value in the middle of the range, and the declared
+/// version — the one number a caller controls and the server therefore cannot bound — is not an
+/// extension member at all.
+#[tokio::test]
+async fn the_widest_version_the_server_can_name_still_decodes() {
+ let ceiling = i64::MAX as u64;
+ let body = format!(
+ r#"{{"type":"about:blank","title":"Roster version leap","status":400,"detail":"roster version 18446744073709551615 is past {ceiling}, the highest this album will accept while it holds version {ceiling}","code":"error.album.roster_version_leap","current_version":{ceiling},"max_version":{ceiling}}}"#
+ );
+ let (server, _) =
+ recording(move |_| MockResponse::new(400, "Bad Request").json_body(body.clone())).await;
+
+ let error = client_for(&server)
+ .publish_roster(&signed_roster(u64::MAX))
+ .await
+ .expect_err("a leap is refused");
+
+ assert_eq!(
+ error.error_code(),
+ Some(error_codes::ALBUM_ROSTER_VERSION_LEAP),
+ "the structured code survives at the boundary: {error:?}"
+ );
+ assert!(
+ matches!(
+ error,
+ AlbumError::Status {
+ status: 400,
+ current_version: Some(held),
+ ..
+ } if held == ceiling
+ ),
+ "and so does the recovery hint: {error:?}"
+ );
+ assert!(
+ !matches!(error, AlbumError::Transport(_)),
+ "a decode failure would have collapsed this into a transport error"
+ );
+}
diff --git a/capsule-sdk/src/auth.rs b/capsule-sdk/src/auth.rs
index 6c7db931..15b588bd 100644
--- a/capsule-sdk/src/auth.rs
+++ b/capsule-sdk/src/auth.rs
@@ -391,9 +391,10 @@ pub struct AuthClient {
impl AuthClient {
/// Build a client against the auth base URL (e.g. `https://api.example.com/auth`).
pub fn new(base_url: &str) -> Result {
- let http = reqwest::Client::builder()
- .build()
- .map_err(AuthError::Transport)?;
+ // The SDK's one HTTP client: every request this client sends — and every request a
+ // `Session` built from it executes on behalf of the upload, album and verify paths —
+ // carries the protocol handshake the server's gate requires.
+ let http = crate::net::http_client().map_err(AuthError::Transport)?;
Self::from_parts(
base_url,
Arc::new(SystemClock),
@@ -404,6 +405,9 @@ impl AuthClient {
/// Assemble a client from explicit parts (clock + HTTP client + skew). Used by
/// [`AuthClient::new`] and by tests that inject a controllable clock.
+ ///
+ /// `http` **must** come from [`crate::net::http_builder`] or [`crate::net::http_client`]: a
+ /// client built any other way sends no protocol handshake, and every gated route refuses it.
fn from_parts(
base_url: &str,
clock: Arc,
diff --git a/capsule-sdk/src/client.rs b/capsule-sdk/src/client.rs
index ca9f6971..bf32b4a1 100644
--- a/capsule-sdk/src/client.rs
+++ b/capsule-sdk/src/client.rs
@@ -46,7 +46,10 @@ pub enum ClientError {
/// Cheap to build; holds one [`rest::Client`](crate::rest::Client) whose bearer credential is
/// an async provider backed by the session. Because the provider is consulted per request,
/// token rotation (refresh) is picked up with no rebuild. Deref-transparent: call any
-/// generated operation directly, e.g. `client.get_quota().await`.
+/// generated operation directly, e.g. `client.get_quota(PROTOCOL_VERSION, None).await` — every
+/// gated operation takes the protocol date as its first argument, because the document
+/// declares `X-Capsule-Protocol` required there (issue #404); the transport sends the same value
+/// as a default header regardless.
pub struct AuthenticatedClient {
base_url: String,
session: Session,
@@ -126,12 +129,11 @@ fn build_client(base_url: &str, session: Session) -> Result
Ok(client)
}
-/// The generated client's transport: rustls only (the SDK's `reqwest` has no default features
-/// and only `rustls-tls`), matching the rest of the SDK's network stack.
+/// The generated client's transport: the SDK's one HTTP client
+/// ([`crate::net::http_client`]) — rustls only, carrying the protocol handshake on every request
+/// it sends, the generated operations included.
fn reqwest_client() -> reqwest::Client {
- reqwest::Client::builder()
- .build()
- .expect("a default rustls reqwest client is always constructible")
+ crate::net::http_client().expect("a default rustls reqwest client is always constructible")
}
#[cfg(test)]
@@ -154,6 +156,8 @@ mod tests {
struct Recorded {
path: String,
authorization: Option,
+ protocol: Option,
+ crypto_suite: Option,
}
struct MockResponse {
@@ -248,6 +252,8 @@ mod tests {
requests.lock().unwrap().push(Recorded {
path: path.clone(),
authorization: headers.get("authorization").cloned(),
+ protocol: headers.get("x-capsule-protocol").cloned(),
+ crypto_suite: headers.get("x-capsule-crypto-suite").cloned(),
});
let response = handler(path).await;
@@ -320,6 +326,46 @@ mod tests {
assert_eq!(version.version.as_str(), "9.9.9");
}
+ /// Every request the typed client sends carries the protocol handshake (issue #404) —
+ /// proving the transport-level default reaches the wire through the generated operation
+ /// with no argument at the call site, on an operation the server does not even gate.
+ #[tokio::test]
+ async fn every_request_carries_the_protocol_handshake() {
+ let handler: Handler = Arc::new(|_| {
+ Box::pin(async move {
+ MockResponse {
+ status: 200,
+ body: r#"{"name":"capsule-api","version":"9.9.9"}"#.to_string(),
+ }
+ })
+ });
+ let server = start_mock(handler).await;
+ let session = session_with(&server.base_url, "access-1", "refresh-1", far_future());
+ let client = AuthenticatedClient::new(&server.base_url, session).unwrap();
+
+ client.get_version().await.unwrap();
+
+ let requests = server.requests.lock().unwrap();
+ let version = requests
+ .iter()
+ .find(|r| r.path == "/v1/version")
+ .expect("version endpoint was hit");
+ assert_eq!(
+ version.protocol.as_deref(),
+ Some(capsule_core::crypto::primitives::PROTOCOL_VERSION),
+ "the protocol date this build speaks must ride every request"
+ );
+ assert_eq!(
+ version.crypto_suite.as_deref(),
+ Some(
+ capsule_core::crypto::primitives::CRYPTO_SUITE_ID
+ .to_string()
+ .as_str()
+ ),
+ "and so must the suite it seals under"
+ );
+ }
+
/// An authenticated operation carries the session's access token as a bearer header —
/// proving the token-provider seam attaches the credential the schema's `security`
/// requirement names.
@@ -343,7 +389,11 @@ mod tests {
let session = session_with(&server.base_url, "access-1", "refresh-1", far_future());
let client = AuthenticatedClient::new(&server.base_url, session).unwrap();
- let quota = client.get_quota().await.unwrap().into_inner();
+ let quota = client
+ .get_quota(capsule_core::crypto::primitives::PROTOCOL_VERSION, None)
+ .await
+ .unwrap()
+ .into_inner();
assert_eq!(quota.used, 0);
let requests = server.requests.lock().unwrap();
@@ -398,7 +448,11 @@ mod tests {
);
let client = AuthenticatedClient::new(&server.base_url, session).unwrap();
- let quota = client.get_quota().await.unwrap().into_inner();
+ let quota = client
+ .get_quota(capsule_core::crypto::primitives::PROTOCOL_VERSION, None)
+ .await
+ .unwrap()
+ .into_inner();
assert_eq!(quota.used, 7);
assert_eq!(
diff --git a/capsule-sdk/src/net.rs b/capsule-sdk/src/net.rs
index 4bc3bd7f..8f7589ff 100644
--- a/capsule-sdk/src/net.rs
+++ b/capsule-sdk/src/net.rs
@@ -721,9 +721,59 @@ pub const DIAL_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
/// (which would double server load): there is no per-request fan-out anywhere in
/// the SDK; a request rides exactly one dialed connection.
pub fn dial_client() -> reqwest::Result {
- reqwest::Client::builder()
- .connect_timeout(DIAL_CONNECT_TIMEOUT)
- .build()
+ http_builder().connect_timeout(DIAL_CONNECT_TIMEOUT).build()
+}
+
+// ─── The one HTTP client ─────────────────────────────────────────────────────
+
+/// The request half of the protocol handshake, as default headers for a `reqwest` client.
+///
+/// Every route the server gates requires `X-Capsule-Protocol` and refuses without it
+/// (`capsule-server/src/negotiation.rs`, issue #404), and `X-Capsule-Crypto-Suite` names the
+/// suite this build seals under. Both are constants of the build, so they belong on the
+/// transport once rather than on every call: a `reqwest` default header rides every request
+/// the client sends, the spargen-generated operations included. A header set explicitly on a
+/// request still wins, which is how the hand-written upload path keeps pinning a per-transport
+/// protocol date.
+///
+/// `X-Capsule-Sidecar-Schema` is deliberately absent: the design scopes it to metadata updates,
+/// and a schema number is a property of one write rather than of the transport.
+#[must_use]
+pub fn protocol_headers() -> reqwest::header::HeaderMap {
+ use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
+
+ let mut headers = HeaderMap::with_capacity(2);
+ headers.insert(
+ HeaderName::from_static("x-capsule-protocol"),
+ HeaderValue::from_static(capsule_core::crypto::primitives::PROTOCOL_VERSION),
+ );
+ headers.insert(
+ HeaderName::from_static("x-capsule-crypto-suite"),
+ HeaderValue::from(capsule_core::crypto::primitives::CRYPTO_SUITE_ID),
+ );
+ headers
+}
+
+/// The builder every SDK transport starts from: rustls only (the SDK's `reqwest` has no
+/// default features and only `rustls-tls`) and the protocol handshake as default headers.
+///
+/// One builder rather than one client because [`dial_client`] adds a connect timeout on top
+/// and the auth, sync and typed clients do not; what they share is the handshake, and this is
+/// the one place it is installed. A transport built any other way sends no handshake and is
+/// refused by every gated route, which is why nothing in this crate calls
+/// `reqwest::Client::builder()` directly outside tests.
+pub fn http_builder() -> reqwest::ClientBuilder {
+ reqwest::Client::builder().default_headers(protocol_headers())
+}
+
+/// The plain SDK client: [`http_builder`], built.
+///
+/// # Errors
+///
+/// Whatever `reqwest` refuses to build with — in practice nothing, since the builder carries
+/// no configuration a platform can lack.
+pub fn http_client() -> reqwest::Result {
+ http_builder().build()
}
#[cfg(test)]
@@ -1069,4 +1119,32 @@ mod tests {
fn dial_client_builds() {
assert!(dial_client().is_ok());
}
+
+ /// Every SDK transport carries the two build constants the server's gate reads.
+ #[test]
+ fn the_handshake_headers_are_the_build_constants() {
+ let headers = protocol_headers();
+ assert_eq!(
+ headers
+ .get("x-capsule-protocol")
+ .and_then(|v| v.to_str().ok()),
+ Some(capsule_core::crypto::primitives::PROTOCOL_VERSION)
+ );
+ assert_eq!(
+ headers
+ .get("x-capsule-crypto-suite")
+ .and_then(|v| v.to_str().ok()),
+ Some(
+ capsule_core::crypto::primitives::CRYPTO_SUITE_ID
+ .to_string()
+ .as_str()
+ )
+ );
+ assert_eq!(
+ headers.len(),
+ 2,
+ "the sidecar schema is a property of one write"
+ );
+ assert!(http_client().is_ok());
+ }
}
diff --git a/capsule-sdk/src/sync.rs b/capsule-sdk/src/sync.rs
index 02035411..eb0feb79 100644
--- a/capsule-sdk/src/sync.rs
+++ b/capsule-sdk/src/sync.rs
@@ -527,14 +527,27 @@ impl SyncConsumer {
.filter(|value| !value.is_empty())
.map(str::to_owned),
page_size: Some(i64::from(page_size)),
+ // The suite and the sidecar schema are validated when present and a feed pull has
+ // no use for either; the suite already rides the transport's default headers.
+ ..rest::SyncFeedParams::default()
};
- Ok(self.client.sync_feed(params).await?.into_inner())
+ // The protocol date is a required parameter of every gated operation in the document,
+ // so the generated signature asks for it; the value is the build's own, the same one the
+ // transport's default header carries.
+ Ok(self
+ .client
+ .sync_feed(capsule_core::crypto::primitives::PROTOCOL_VERSION, params)
+ .await?
+ .into_inner())
}
}
/// A generated client for `base_url` carrying `credential` under the bearer scheme.
fn build_client(base_url: &str, credential: rest::Credential) -> Result {
- let client = rest::Client::with_client(reqwest::Client::new(), base_url)
+ // The SDK's one HTTP client, so the feed pull carries the protocol handshake.
+ let http =
+ crate::net::http_client().map_err(|error| SyncError::Transport(error.to_string()))?;
+ let client = rest::Client::with_client(http, base_url)
.map_err(|error| SyncError::Transport(error.to_string()))?
.with_credential(BEARER_SCHEME, credential);
Ok(client)
@@ -585,6 +598,9 @@ fn map_error(error: rest::Error) -> SyncError {
match error {
rest::Error::Api(response) => {
let (code, message) = match response.into_inner() {
+ // The 400 includes the protocol gate's malformed-handshake answer (issue #404).
+ // There is no 426 to map: the feed is a read, and a read is admitted at any
+ // grammatical protocol date — the window rides the response headers instead.
rest::SyncFeedError::Status400(problem)
| rest::SyncFeedError::Status401(problem)
| rest::SyncFeedError::Status403(problem)
diff --git a/capsule-sdk/src/upload.rs b/capsule-sdk/src/upload.rs
index a15687a3..054e736a 100644
--- a/capsule-sdk/src/upload.rs
+++ b/capsule-sdk/src/upload.rs
@@ -309,6 +309,9 @@ impl UploadTransport {
/// Build a transport over a fixed bearer token (tests; callers that already
/// hold a live token). Same URL layout as [`Self::with_session`].
+ ///
+ /// `http` **must** come from [`crate::net::http_builder`] or [`crate::net::http_client`]: a
+ /// client built any other way sends no protocol handshake, and every gated route refuses it.
pub fn with_static_token(
http: reqwest::Client,
base_url: impl Into,
diff --git a/capsule-sdk/src/verify.rs b/capsule-sdk/src/verify.rs
index 73952fc4..0680799c 100644
--- a/capsule-sdk/src/verify.rs
+++ b/capsule-sdk/src/verify.rs
@@ -106,6 +106,9 @@ impl VerifyTransport {
}
/// Build a transport over a fixed bearer token (tests; callers holding a live token).
+ ///
+ /// `http` **must** come from [`crate::net::http_builder`] or [`crate::net::http_client`]: a
+ /// client built any other way sends no protocol handshake, and every gated route refuses it.
pub fn with_static_token(
http: reqwest::Client,
base_url: impl Into,
diff --git a/capsule-server/.env.example b/capsule-server/.env.example
index fffa4840..c94a8ace 100644
--- a/capsule-server/.env.example
+++ b/capsule-server/.env.example
@@ -125,10 +125,19 @@ VALKEY_URL=redis://127.0.0.1:6379
# ── The protocol window ──────────────────────────────────────────────────────────────────────
#
-# Both ends inclusive, both published, and both default to the version `capsule-core` speaks.
-# Widen `PROTOCOL_MIN` only with a deprecation announcement behind it.
-# PROTOCOL_MIN=2026-05-31
-# PROTOCOL_MAX=2026-05-31
+# Both ends inclusive, `YYYY-MM-DD`, validated as dates, and both published on every response as
+# `X-Capsule-Protocol-Min`/`-Max`. A write with a protocol date outside the window is refused
+# with 426; a read is admitted at any date. The defaults are the policy's year window
+# (`capsule-server/src/upload/policy.rs`), which the version `capsule-core` speaks sits inside;
+# `PROTOCOL_MIN=PROTOCOL_MAX` is a legitimate choice. Narrow `PROTOCOL_MIN` only with a
+# deprecation announcement behind it.
+# PROTOCOL_MIN=2026-01-01
+# PROTOCOL_MAX=2026-12-31
+
+# The semver client build below which this server will stop answering, published on every
+# response as `X-Capsule-Min-Client-Build`. Advisory: nothing refuses on it, and `0.0.0` — the
+# default — means no cutoff has been announced. MAJOR.MINOR.PATCH, validated.
+# MIN_CLIENT_BUILD=0.0.0
# ── Operational knobs ────────────────────────────────────────────────────────────────────────
#
diff --git a/capsule-server/migration/src/lib.rs b/capsule-server/migration/src/lib.rs
index 24e0c4be..b14e9e03 100644
--- a/capsule-server/migration/src/lib.rs
+++ b/capsule-server/migration/src/lib.rs
@@ -1,9 +1,10 @@
//! The server's PostgreSQL schema, one migration per ordinal.
//!
-//! # What the four ordinals cover
+//! # What the five ordinals cover
//!
-//! The durable ports issue #402 lands adapters for, and no others: the asset index, the account
-//! cluster, the device-cohort map and the quota ledger. The remaining durable ports keep their
+//! The four durable ports issue #402 lands adapters for — the asset index, the account
+//! cluster, the device-cohort map and the quota ledger — plus album membership (`S-C51`, #405).
+//! The remaining durable ports keep their
//! in-memory adapters and gain ordinals with their adapters, so a migration never describes a
//! table nothing reads.
//!
@@ -23,6 +24,7 @@ mod m20260902_000001_asset_index;
mod m20260902_000002_accounts;
mod m20260902_000003_cohorts;
mod m20260902_000004_quota;
+mod m20260902_000005_album_membership;
/// The server's migrator.
pub struct Migrator;
@@ -35,6 +37,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260902_000002_accounts::Migration),
Box::new(m20260902_000003_cohorts::Migration),
Box::new(m20260902_000004_quota::Migration),
+ Box::new(m20260902_000005_album_membership::Migration),
]
}
}
diff --git a/capsule-server/migration/src/m20260902_000005_album_membership.rs b/capsule-server/migration/src/m20260902_000005_album_membership.rs
new file mode 100644
index 00000000..e0e7fd34
--- /dev/null
+++ b/capsule-server/migration/src/m20260902_000005_album_membership.rs
@@ -0,0 +1,138 @@
+//! Album membership (`S-C51`): the roster the owner attested, and who it makes a member.
+
+use sea_orm_migration::prelude::*;
+
+#[derive(DeriveMigrationName)]
+pub(crate) struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ // One row per album: the roster the server currently accepts, with the signed document
+ // verbatim. A replay is decided on the document's bytes, and an operator can re-verify
+ // what was accepted against the owner's directory of the day.
+ manager
+ .create_table(
+ Table::create()
+ .table(AlbumRosters::Table)
+ .if_not_exists()
+ .col(
+ ColumnDef::new(AlbumRosters::AlbumId)
+ .text()
+ .not_null()
+ .primary_key(),
+ )
+ .col(
+ ColumnDef::new(AlbumRosters::RosterVersion)
+ .big_integer()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(AlbumRosters::AmkEpoch)
+ .big_integer()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(AlbumRosters::AttestedByDevice)
+ .text()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(AlbumRosters::ReceivedAt)
+ .big_integer()
+ .not_null(),
+ )
+ .col(ColumnDef::new(AlbumRosters::Document).binary().not_null())
+ .to_owned(),
+ )
+ .await?;
+
+ // One row per account that has ever been on one of the album's rosters. A member the
+ // owner removes keeps their row with the two `revoked_*` columns set: the blob route's
+ // `403` is reserved for a caller the server can see once *had* access, and a deleted row
+ // would make a former member indistinguishable from a stranger.
+ manager
+ .create_table(
+ Table::create()
+ .table(AlbumMembers::Table)
+ .if_not_exists()
+ .col(ColumnDef::new(AlbumMembers::AlbumId).text().not_null())
+ .col(ColumnDef::new(AlbumMembers::UserId).text().not_null())
+ .col(ColumnDef::new(AlbumMembers::Role).text().not_null())
+ .col(
+ ColumnDef::new(AlbumMembers::SinceVersion)
+ .big_integer()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(AlbumMembers::GrantedEpoch)
+ .big_integer()
+ .not_null(),
+ )
+ .col(
+ ColumnDef::new(AlbumMembers::RevokedAtVersion)
+ .big_integer()
+ .null(),
+ )
+ .col(
+ ColumnDef::new(AlbumMembers::RevokedEpoch)
+ .big_integer()
+ .null(),
+ )
+ .primary_key(
+ Index::create()
+ .col(AlbumMembers::AlbumId)
+ .col(AlbumMembers::UserId),
+ )
+ .to_owned(),
+ )
+ .await?;
+ // "Which albums is this account on" — the sync side's question, and the one the
+ // primary key does not answer.
+ manager
+ .create_index(
+ Index::create()
+ .if_not_exists()
+ .name("idx_album_members_user")
+ .table(AlbumMembers::Table)
+ .col(AlbumMembers::UserId)
+ .to_owned(),
+ )
+ .await?;
+
+ Ok(())
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .drop_table(Table::drop().table(AlbumMembers::Table).to_owned())
+ .await?;
+ manager
+ .drop_table(Table::drop().table(AlbumRosters::Table).to_owned())
+ .await?;
+ Ok(())
+ }
+}
+
+#[derive(DeriveIden)]
+enum AlbumRosters {
+ Table,
+ AlbumId,
+ RosterVersion,
+ AmkEpoch,
+ AttestedByDevice,
+ ReceivedAt,
+ Document,
+}
+
+#[derive(DeriveIden)]
+enum AlbumMembers {
+ Table,
+ AlbumId,
+ UserId,
+ Role,
+ SinceVersion,
+ GrantedEpoch,
+ RevokedAtVersion,
+ RevokedEpoch,
+}
diff --git a/capsule-server/openapi.json b/capsule-server/openapi.json
index 3e80f9f5..85b3d426 100644
--- a/capsule-server/openapi.json
+++ b/capsule-server/openapi.json
@@ -5,33 +5,45 @@
"version": "0.0.0"
},
"paths": {
- "/v1/version": {
- "get": {
- "summary": "Reports the server's name and version.",
- "description": "Unauthenticated and side-effect free. Clients use it as a reachability probe before\nattempting a protocol handshake, so it must stay cheap and must never fail for a reason\nthe caller could act on — there is no failure variant, and the return type says so.",
- "operationId": "get_version",
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/VersionResponse"
- }
- }
- }
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
"/v1/auth/register": {
"post": {
"summary": "Create an account, and open its first session.",
"description": "# Why it signs you in\n\nThe alternative is `201` with no body and a client that immediately posts the same\ncredentials to `/v1/auth/login`, which is one more round trip for one more chance to fail and\nnothing gained. It also makes the CLI's `capsule register` mean what a person expects: after\nit, you are registered *and* signed in.\n\n# What it does not do\n\n**It does not publish a device directory**, and the account is therefore unable to upload\nuntil its client publishes one. That is not an omission here: `S-C20` removed the\naccount-creation fallback for invariant 7's floor precisely so that \"was this device in the\ndirectory\" has an honest answer for a brand-new account, and the honest answer is *no*. A\nclient's first action after registering is `POST /v1/auth/devices/directory`.\n\n**It is not rate-limited**, and that is a real gap rather than an oversight — see\n[`crate::auth::registry`] for the fact the limiter is waiting on. This is the one\nunauthenticated write on the surface.\n\n# `200`, where Salvo answered `201`\n\nKynos's `Created` requires a `Location` — a `201` that does not say *where* tells a client\nsomething exists and not how to reach it, which is a defect the type refuses to let you\ncommit. This server exposes no URL for an account: `GET /v1/auth/profile` is among the\noperations `S-C53` records as unported. Inventing a location to satisfy a status would be\ninventing a surface, so the status moved instead. What a caller actually needs — the token\npair — is in the body either way.",
"operationId": "register_user",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"requestBody": {
"content": {
"application/json": {
@@ -45,6 +57,32 @@
"responses": {
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -55,6 +93,32 @@
},
"415": {
"description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -65,6 +129,32 @@
},
"422": {
"description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -75,6 +165,32 @@
},
"200": {
"description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
@@ -85,6 +201,32 @@
},
"409": {
"description": "Account already exists",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -95,6 +237,32 @@
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -104,7 +272,69 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
}
}
}
@@ -114,6 +344,40 @@
"summary": "Exchange an email and password for a session — or for a second-factor challenge.",
"description": "The two advisory identifiers a client may send — `cohort_hash` and `device_id` — are recorded\non the session for the devices listing and gate nothing; an unusable one is dropped rather\nthan refused.\n\n# Two statuses, because there are two outcomes\n\nAn account with a confirmed second factor (`S-C55`) gets **`202`** and a short-lived\nchallenge: the credentials were accepted and the request is not complete. No session is\nopened, no cohort is recorded and no refresh token is minted, because none of those may exist\nfor an authentication that has not finished — and the client's advisory identifiers ride the\n*completing* request instead, since that is what creates the session they describe.\n\nThe retired surface got this wrong in the most consequential way available: it had all four\nTOTP operations and its login never issued a challenge, so a confirmed second factor gated\nnothing at all.",
"operationId": "login_user",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"requestBody": {
"content": {
"application/json": {
@@ -127,6 +391,32 @@
"responses": {
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -137,6 +427,32 @@
},
"415": {
"description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -147,16 +463,68 @@
},
"422": {
"description": "Unprocessable Entity",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "200": {
- "description": "A session was opened; here is its token pair.",
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "A session was opened; here is its token pair.",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
@@ -167,6 +535,32 @@
},
"202": {
"description": "The password verified; a second factor is required to finish.",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
@@ -177,6 +571,32 @@
},
"401": {
"description": "Invalid credentials",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -187,6 +607,32 @@
},
"423": {
"description": "Account locked",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -197,6 +643,32 @@
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -206,7 +678,69 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
}
}
}
@@ -216,6 +750,40 @@
"summary": "Exchange a refresh token for a new pair, rotating the session.",
"description": "The presented session is **closed** and a new one opened in its place, so a refresh token is\ngood exactly once. The session's advisory provenance — its cohort hash and device id — is\ncarried across the rotation, or the devices listing would lose track of a device every time\nits tokens turned over.",
"operationId": "refresh_token",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"requestBody": {
"content": {
"application/json": {
@@ -229,6 +797,32 @@
"responses": {
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -239,6 +833,32 @@
},
"415": {
"description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -249,6 +869,32 @@
},
"422": {
"description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -259,6 +905,32 @@
},
"200": {
"description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
@@ -269,46 +941,66 @@
},
"401": {
"description": "Session expired",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
"schema": {
"$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/v1/auth/logout": {
- "post": {
- "summary": "End the session the presented access token was issued against.",
- "description": "Idempotent: a session that is already closed, expired, or was never opened produces the same\nanswer, because \"there is no longer a session\" is what the caller asked for.",
- "operationId": "logout",
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "500": {
+ "description": "Internal server error",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -319,21 +1011,63 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "500": {
- "description": "Internal server error",
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -341,23 +1075,49 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
- },
- "security": [
- {
- "bearer": []
- }
- ]
+ }
}
},
- "/v1/auth/logout/all/challenge": {
+ "/v1/auth/logout": {
"post": {
- "summary": "Issue a single-use challenge for a global sign-out.",
- "description": "**Authenticated by a session token, unlike the revoke itself.** That is not a contradiction\nof the ceremony's asymmetry: a challenge is worthless without the identity key, so handing\none to a stolen token costs nothing — while issuing them unauthenticated would make this an\noracle for whether an account exists. The account comes from the credential and never from a\nrequest field, so a caller cannot ask for somebody else's challenge.",
- "operationId": "revoke_all_challenge",
+ "summary": "End the session the presented access token was issued against.",
+ "description": "Idempotent: a session that is already closed, expired, or was never opened produces the same\nanswer, because \"there is no longer a session\" is what the caller asked for.",
+ "operationId": "logout",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"responses": {
"401": {
"description": "Unauthorized",
@@ -369,6 +1129,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -381,6 +1165,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -389,18 +1199,63 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/RevokeChallengeResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -410,44 +1265,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/auth/logout/all": {
- "post": {
- "summary": "Close every session for the account the proof establishes.",
- "description": "**No `Auth`, deliberately.** design/authentication.md gates this on proof of master-key\npossession *instead of* a session token, and the reason is the damage scenario: an attacker\nholding a stolen token could otherwise invoke \"log out of all devices\" and lock the\nlegitimate user out of every device they own. Requiring the identity key means a stolen\ntoken can revoke only itself. The account is established by the burned challenge, so there\nis no account field for a caller to aim at either.\n\nThe caller's own session goes with the rest. That is the ceremony, not an oversight.",
- "operationId": "revoke_all",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RevokeAllRequest"
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
}
},
- "required": true
- },
- "responses": {
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "415": {
- "description": "Unsupported Media Type",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -456,38 +1329,34 @@
}
}
},
- "422": {
- "description": "Unprocessable Entity",
- "content": {
- "application/problem+json": {
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/RevokeAllResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "401": {
- "description": "Master-key proof required",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -495,18 +1364,54 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
- }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
}
},
- "/v1/auth/devices": {
- "get": {
- "summary": "List the caller's live sessions and the cohorts they group under.",
- "description": "Scoped by credential with no path parameter, for the same reason the escrow is: the only\naccount entitled to a session ledger is its own, and making that structural beats enforcing\nit.",
- "operationId": "list_devices",
+ "/v1/auth/logout/all/challenge": {
+ "post": {
+ "summary": "Issue a single-use challenge for a global sign-out.",
+ "description": "**Authenticated by a session token, unlike the revoke itself.** That is not a contradiction\nof the ceremony's asymmetry: a challenge is worthless without the identity key, so handing\none to a stolen token costs nothing — while issuing them unauthenticated would make this an\noracle for whether an account exists. The account comes from the credential and never from a\nrequest field, so a caller cannot ask for somebody else's challenge.",
+ "operationId": "revoke_all_challenge",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"responses": {
"401": {
"description": "Unauthorized",
@@ -518,6 +1423,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -530,6 +1459,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -540,16 +1495,68 @@
},
"200": {
"description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/DevicesResponse"
+ "$ref": "#/components/schemas/RevokeChallengeResponse"
}
}
}
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -559,65 +1566,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/auth/devices/{session_id}": {
- "delete": {
- "summary": "Revoke one of the caller's sessions.",
- "description": "Any live token may do this, including for the session making the request — signing this\ndevice out is a legitimate thing to ask for, and refusing it would only push a client into\ncalling `logout` and hoping the two behave the same.\n\n**Only the caller's own sessions.** The ownership check is against the record the store\nreturns rather than against a separate lookup, so there is no window between checking and\nclosing, and a session id belonging to another account answers exactly as an unknown one\ndoes.",
- "operationId": "revoke_session",
- "parameters": [
- {
- "name": "session_id",
- "in": "path",
- "description": "The session's identifier.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "description": "the request body exceeds the configured limit",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -626,21 +1630,34 @@
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -648,9 +1665,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -660,69 +1674,84 @@
]
}
},
- "/v1/auth/devices/directory": {
+ "/v1/auth/logout/all": {
"post": {
- "summary": "Publish the caller's signed device directory.",
- "description": "The bytes are stored verbatim; the server decodes them to read `directory_version` and\nnothing else. The monotonicity comparison is the store's, not this handler's — see\n[`crate::directory`] for why a read-compare-write here would be a rollback window.",
- "operationId": "publish_device_directory",
+ "summary": "Close every session for the account the proof establishes.",
+ "description": "**No `Auth`, deliberately.** design/authentication.md gates this on proof of master-key\npossession *instead of* a session token, and the reason is the damage scenario: an attacker\nholding a stolen token could otherwise invoke \"log out of all devices\" and lock the\nlegitimate user out of every device they own. Requiring the identity key means a stolen\ntoken can revoke only itself. The account is established by the burned challenge, so there\nis no account field for a caller to aim at either.\n\nThe caller's own session goes with the rest. That is the ceremony, not an oversight.",
+ "operationId": "revoke_all",
"parameters": [
{
- "name": "X-Capsule-Identity-Key",
+ "name": "X-Capsule-Protocol",
"in": "header",
- "description": "The account's identity public key, standard base64 over the hybrid `classical ‖ ml`\nlayout. Required: invariant 23's second clause is undefined without it.",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
"requestBody": {
"content": {
- "application/cbor": {
+ "application/json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/RevokeAllRequest"
}
}
},
"required": true
},
"responses": {
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Bad Request",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -732,37 +1761,33 @@
}
},
"415": {
- "description": "Unsupported media type",
- "content": {
- "application/problem+json": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/PublishDirectoryResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "409": {
- "description": "Directory version conflict",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/DirectoryConflictProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -771,66 +1796,34 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/auth/devices/directory/{user_id}": {
- "get": {
- "summary": "Fetch a user's signed device directory, verbatim.",
- "description": "The response body is the exact bytes the owner signed. Re-encoding them would detach the\ndocument from its signature, and the failure would look like the *publisher's* bug.",
- "operationId": "fetch_device_directory",
- "parameters": [
- {
- "name": "user_id",
- "in": "path",
- "description": "The account id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "422": {
+ "description": "Unprocessable Entity",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -841,62 +1834,66 @@
},
"200": {
"description": "OK",
- "content": {
- "application/cbor": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
"type": "string",
- "format": "binary"
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/RevokeAllResponse"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/auth/escrow": {
- "get": {
- "summary": "Fetch the caller's wrapped master key, verbatim.",
- "description": "The bytes are what a client runs its KDF against, so they come back exactly as they went in.\nThe server never derives, unwraps or re-encodes: a re-encoded wrap is a wrap that no longer\nopens, and the failure would look like a lost master key.",
- "operationId": "fetch_escrow",
- "responses": {
"401": {
- "description": "Unauthorized",
+ "description": "Master-key proof required",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -907,39 +1904,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "200": {
- "description": "OK",
- "content": {
- "application/octet-stream": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
"type": "string",
- "format": "binary"
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -949,93 +1941,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- },
- "put": {
- "summary": "Store the caller's wrapped master key, replacing whatever they had.",
- "description": "`PUT`, because there is exactly one escrow per account and this is its address. Storing over\nan existing escrow is the guided re-wrap, and it deletes the old blob in the same operation —\nthe lost recovery secret must stop working, which is the entire point of rotating.",
- "operationId": "store_escrow",
- "requestBody": {
- "content": {
- "application/octet-stream": {
- "schema": {
- "type": "string",
- "format": "binary"
- }
- }
- },
- "required": true
- },
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "description": "the request body exceeds the configured limit",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "415": {
- "description": "Unsupported media type",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "400": {
- "description": "Malformed request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/StoreEscrowResponse"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1043,16 +2004,8 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
}
- ]
+ }
}
},
"/v1/auth/reauthenticate": {
@@ -1060,6 +2013,40 @@
"summary": "Prove a credential again on the current session, without opening a new one.",
"description": "**The only way to satisfy the freshness gate `S-C7` enforces**, and it exists because\nwithout it the gate is unusable: `authenticated_at` is deliberately *not* reset by a refresh,\nso a user signed in an hour ago would otherwise have to sign out entirely to add a device —\nand the session they abandoned would linger in their own devices listing.\n\nIt does not mint tokens and does not rotate the session. The caller keeps the credential\nthey already hold; what changes is one timestamp on the record behind it.\n\n# Errors\n\nThe same refusals as a sign-in, for the same reasons: a wrong password is\n`401 error.auth.invalid_credentials`, a locked account is `403`, and the account directory\nfailing is `500`. A caller that guessed a password here learns exactly what it would learn\nat `/v1/auth/login`, and no more.",
"operationId": "reauthenticate",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"requestBody": {
"content": {
"application/json": {
@@ -1081,6 +2068,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -1093,6 +2104,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1103,6 +2140,32 @@
},
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1113,6 +2176,32 @@
},
"415": {
"description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1123,6 +2212,32 @@
},
"422": {
"description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1133,6 +2248,32 @@
},
"200": {
"description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
@@ -1143,6 +2284,32 @@
},
"423": {
"description": "Account locked",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1153,6 +2320,32 @@
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1162,74 +2355,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/auth/profile": {
- "get": {
- "summary": "The caller's own profile.",
- "description": "There is no `{user_id}` segment, for the reason the escrow surface has none: the account\ncomes from the credential, so reading somebody else's profile is not a forbidden request but\nan unrepresentable one. A directory of *other* people's public facts already exists and is a\ndifferent surface — `GET /v1/auth/devices/directory/{user_id}` — which publishes keys and\nnothing else.",
- "operationId": "get_profile",
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "description": "the request body exceeds the configured limit",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProfileResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1237,9 +2418,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -1247,21 +2425,56 @@
"bearer": []
}
]
- },
- "patch": {
- "summary": "Edit the caller's own profile.",
- "description": "`PATCH`, because the body is a partial: what it does not mention, it does not change. An\nempty body is a valid request and answers `200` with the profile unchanged — a client that\nsent nothing asked for nothing, and refusing it would make \"save\" fail on a form nobody\nedited.",
- "operationId": "update_profile",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/UpdateProfileRequest"
- }
+ }
+ },
+ "/v1/auth/devices/{session_id}": {
+ "delete": {
+ "summary": "Revoke one of the caller's sessions.",
+ "description": "Any live token may do this, including for the session making the request — signing this\ndevice out is a legitimate thing to ask for, and refusing it would only push a client into\ncalling `logout` and hoping the two behave the same.\n\n**Only the caller's own sessions.** The ownership check is against the record the store\nreturns rather than against a separate lookup, so there is no window between checking and\nclosing, and a session id belonging to another account answers exactly as an unknown one\ndoes.",
+ "operationId": "revoke_session",
+ "parameters": [
+ {
+ "name": "session_id",
+ "in": "path",
+ "description": "The session's identifier.",
+ "required": true,
+ "schema": {
+ "type": "string"
}
},
- "required": true
- },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"responses": {
"401": {
"description": "Unauthorized",
@@ -1273,6 +2486,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -1285,6 +2522,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1295,26 +2558,32 @@
},
"400": {
"description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "415": {
- "description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "422": {
- "description": "Unprocessable Entity",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1323,18 +2592,63 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProfileResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
"404": {
"description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1345,6 +2659,32 @@
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1354,117 +2694,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/auth/password": {
- "post": {
- "summary": "Replace the password this account's sessions are opened with.",
- "description": "# Every *other* session ends\n\nA password change whose point is that a credential has leaked would be worthless if the\nsessions opened with the leaked credential kept working. So the change closes every session\nof the account — and then re-opens the caller's own, **under its own session id**, so the\nperson doing the rotation is not signed out of the device they are doing it on while\neverybody else is.\n\nRe-opening the same id rather than minting a new one is what lets this answer `204` with no\nbody: the caller's existing token pair keeps working, because the session it names is still\nthere. Returning a fresh pair was considered and rejected — it would make this a second token\nmint with none of `POST /v1/auth/refresh`'s rotation discipline, for no gain.\n\nThe re-opened record's `authenticated_at` is **now**, and that is not bookkeeping: presenting\nthe current password *is* a credential presentation, so a freshness gate (`S-C7`) measuring\nfrom anything earlier would be measuring from the wrong moment.\n\n# Why the order is verify, write, revoke\n\nVerification first, because a wrong current password must change nothing. The write next,\nbecause a revocation that ran before it would sign everybody out and then fail. The\nrevocation last, and its failure is **logged and not returned**: the password is already\nchanged, so answering `500` would tell the caller the rotation did not happen when it did,\nand they would try again with a current password that is no longer current.",
- "operationId": "change_password",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ChangePasswordRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "description": "the request body exceeds the configured limit",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "415": {
- "description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "422": {
- "description": "Unprocessable Entity",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "423": {
- "description": "Account locked",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1472,9 +2757,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -1484,93 +2766,63 @@
]
}
},
- "/v1/auth/totp/enroll": {
+ "/v1/auth/devices/directory": {
"post": {
- "summary": "Start enrolling an authenticator.",
- "description": "Answers the `otpauth://` URI the app scans. Nothing is gated yet: until a code confirms the\nsecret, sign-in is unchanged — which is what stops a mis-scanned QR code from locking\nsomebody out of their own account.",
- "operationId": "totp_enroll",
- "responses": {
- "401": {
- "description": "Unauthorized",
- "headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
- "required": true,
- "schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
- }
- },
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/EnrollmentResponse"
- }
- }
+ "summary": "Publish the caller's signed device directory.",
+ "description": "The bytes are stored verbatim; the server decodes them to read `directory_version` and\nnothing else. The monotonicity comparison is the store's, not this handler's — see\n[`crate::directory`] for why a read-compare-write here would be a rollback window.",
+ "operationId": "publish_device_directory",
+ "parameters": [
+ {
+ "name": "X-Capsule-Identity-Key",
+ "in": "header",
+ "description": "The account's identity public key, standard base64 over the hybrid `classical ‖ ml`\nlayout. Required: invariant 23's second clause is undefined without it.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
}
},
- "409": {
- "description": "Already active",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
{
- "bearer": []
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
}
- ]
- }
- },
- "/v1/auth/totp/verify-enrollment": {
- "post": {
- "summary": "Confirm an enrollment with a live code.",
- "description": "The confirming code is **spent**: its step goes straight into the replay ledger, so it cannot\nalso complete a sign-in a moment later. That is the one place the ledger's first entry comes\nfrom, and skipping it would leave the newest code in the account's history unused.",
- "operationId": "totp_verify_enrollment",
+ ],
"requestBody": {
"content": {
- "application/json": {
+ "application/cbor": {
"schema": {
- "$ref": "#/components/schemas/CodeRequest"
+ "type": "string",
+ "format": "binary"
}
}
},
@@ -1587,6 +2839,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -1599,6 +2875,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1609,16 +2911,32 @@
},
"400": {
"description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "415": {
- "description": "Unsupported Media Type",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1627,31 +2945,142 @@
}
}
},
- "422": {
- "description": "Unprocessable Entity",
- "content": {
- "application/problem+json": {
+ "415": {
+ "description": "Unsupported media type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
"schema": {
"$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send"
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PublishDirectoryResponse"
+ }
+ }
+ }
},
"409": {
- "description": "Nothing pending",
+ "description": "Directory version conflict",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/DirectoryConflictProblem"
}
}
}
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1661,7 +3090,69 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
}
},
"security": [
@@ -1671,21 +3162,45 @@
]
}
},
- "/v1/auth/totp/disable": {
- "post": {
- "summary": "Remove the second factor, on presentation of a live code.",
- "description": "**A session is not enough.** The whole point of the factor is that a stolen access token is\ninsufficient, and a disable that took only a token would let the token turn off the control\nthat makes it insufficient.",
- "operationId": "totp_disable",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CodeRequest"
- }
+ "/v1/auth/escrow": {
+ "get": {
+ "summary": "Fetch the caller's wrapped master key, verbatim.",
+ "description": "The bytes are what a client runs its KDF against, so they come back exactly as they went in.\nThe server never derives, unwraps or re-encodes: a re-encoded wrap is a wrap that no longer\nopens, and the failure would look like a lost master key.",
+ "operationId": "fetch_escrow",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "required": true
- },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"responses": {
"401": {
"description": "Unauthorized",
@@ -1697,6 +3212,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -1709,16 +3248,32 @@
},
"403": {
"description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1727,18 +3282,71 @@
}
}
},
- "415": {
- "description": "Unsupported Media Type",
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/problem+json": {
+ "application/octet-stream": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "format": "binary"
}
}
}
},
- "422": {
- "description": "Unprocessable Entity",
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1747,12 +3355,35 @@
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "409": {
- "description": "Not enrolled",
- "content": {
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/CodedProblem"
@@ -1760,8 +3391,63 @@
}
}
},
- "500": {
- "description": "Internal server error",
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1769,9 +3455,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -1779,46 +3462,93 @@
"bearer": []
}
]
- }
- },
- "/v1/auth/login/verify-totp": {
- "post": {
- "summary": "Complete a sign-in with a code.",
- "description": "This is where the session is opened — not `POST /v1/auth/login`, which for an account with a\nsecond factor opens nothing. The advisory `cohort_hash` and `device_id` ride *this* request\nfor the same reason: the session they describe is created here.",
- "operationId": "totp_verify_login",
+ },
+ "put": {
+ "summary": "Store the caller's wrapped master key, replacing whatever they had.",
+ "description": "`PUT`, because there is exactly one escrow per account and this is its address. Storing over\nan existing escrow is the guided re-wrap, and it deletes the old blob in the same operation —\nthe lost recovery secret must stop working, which is the entire point of rotating.",
+ "operationId": "store_escrow",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"requestBody": {
"content": {
- "application/json": {
+ "application/octet-stream": {
"schema": {
- "$ref": "#/components/schemas/VerifyLoginRequest"
+ "type": "string",
+ "format": "binary"
}
}
},
"required": true
},
"responses": {
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "415": {
- "description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "422": {
- "description": "Unprocessable Entity",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1827,38 +3557,34 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/TokenResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "401": {
- "description": "Challenge expired",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "429": {
- "description": "Too many attempts",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1867,28 +3593,32 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/v1/auth/devices/enroll": {
- "post": {
- "summary": "Issue a one-time enrollment code for the caller's account.",
- "description": "Gated on a recent credential presentation, not merely on a valid session — a stolen token\nmust not be able to enroll a rogue device. See [`crate::enrollment`] for exactly how much\nthat gate can mean.",
- "operationId": "issue_enrollment_code",
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "415": {
+ "description": "Unsupported media type",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -1899,8 +3629,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
+ "400": {
+ "description": "Malformed request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1911,73 +3667,68 @@
},
"200": {
"description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/EnrollmentCodeResponse"
+ "$ref": "#/components/schemas/StoreEscrowResponse"
}
}
}
},
"500": {
"description": "Internal server error",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/auth/devices/enroll/redeem": {
- "post": {
- "summary": "Redeem a code for a relay channel.",
- "description": "**Unauthenticated, necessarily.** Device B has no account, no session and no key material —\nit is a phone that has just scanned a QR code. The code is the only thing it holds, so the\ncode is the credential.",
- "operationId": "redeem_enrollment_code",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RedeemRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "415": {
- "description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "422": {
- "description": "Unprocessable Entity",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -1986,38 +3737,63 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ChannelResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Code refused",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "429": {
- "description": "Too many attempts",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2025,41 +3801,127 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
- }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
}
},
- "/v1/auth/devices/enroll/channel/{channel_id}": {
+ "/v1/auth/profile": {
"get": {
- "summary": "Take everything pending in one of a channel's mailboxes.",
- "description": "Destructive: a relayed payload is delivered once. Draining one direction leaves the other\nuntouched, so the two devices do not consume each other's mail.",
- "operationId": "drain_enrollment_channel",
+ "summary": "The caller's own profile.",
+ "description": "There is no `{user_id}` segment, for the reason the escrow surface has none: the account\ncomes from the credential, so reading somebody else's profile is not a forbidden request but\nan unrepresentable one. A directory of *other* people's public facts already exists and is a\ndifferent surface — `GET /v1/auth/devices/directory/{user_id}` — which publishes keys and\nnothing else.",
+ "operationId": "get_profile",
"parameters": [
{
- "name": "channel_id",
- "in": "path",
- "description": "The handle a redeemed code returned.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
{
- "name": "direction",
- "in": "query",
- "description": "`to_initiator` or `to_enrollee`.",
- "required": true,
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
"schema": {
- "type": "string"
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
"responses": {
- "400": {
- "description": "Bad Request",
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2070,16 +3932,68 @@
},
"200": {
"description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/DrainResponse"
+ "$ref": "#/components/schemas/ProfileResponse"
}
}
}
},
"404": {
- "description": "Channel not found",
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2090,6 +4004,32 @@
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2099,165 +4039,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- },
- "post": {
- "summary": "Append a payload to one of a channel's two mailboxes.",
- "description": "Unauthenticated and gated by the handle alone. The relay is a dumb pipe by design — see\n[`crate::enrollment`] — and the safety-code check is what defends the ceremony.",
- "operationId": "relay_enrollment_payload",
- "parameters": [
- {
- "name": "channel_id",
- "in": "path",
- "description": "The handle a redeemed code returned.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/RelayRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
- }
- },
- "415": {
- "description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
- }
- },
- "422": {
- "description": "Unprocessable Entity",
- "content": {
- "application/problem+json": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "404": {
- "description": "Channel not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- },
- "delete": {
- "summary": "Close a channel and drop both mailboxes with it.",
- "description": "**The initiator's, and authenticated.** A close is the one relay operation that is not\nidempotent from the other device's point of view — it ends the ceremony — so leaving it on\nthe handle alone would make an abandoned QR code a denial of service. The account is checked\nagainst the channel's recorded initiator, and a channel belonging to another account answers\nexactly as an unknown one does.",
- "operationId": "close_enrollment_channel",
- "parameters": [
- {
- "name": "channel_id",
- "in": "path",
- "description": "The handle a redeemed code returned.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Malformed handshake",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "404": {
- "description": "Channel not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2265,9 +4102,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -2275,18 +4109,50 @@
"bearer": []
}
]
- }
- },
- "/v1/albums": {
- "post": {
- "summary": "Bind an album id to the authenticated caller.",
- "description": "Idempotent: the same id from a second device, or after a recovery, is a success that writes\nnothing.",
- "operationId": "provision_album",
+ },
+ "patch": {
+ "summary": "Edit the caller's own profile.",
+ "description": "`PATCH`, because the body is a partial: what it does not mention, it does not change. An\nempty body is a valid request and answers `200` with the profile unchanged — a client that\nsent nothing asked for nothing, and refusing it would make \"save\" fail on a form nobody\nedited.",
+ "operationId": "update_profile",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ProvisionAlbumRequest"
+ "$ref": "#/components/schemas/UpdateProfileRequest"
}
}
},
@@ -2303,6 +4169,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -2315,6 +4205,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2325,6 +4241,32 @@
},
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2335,16 +4277,32 @@
},
"415": {
"description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "422": {
- "description": "Unprocessable Entity",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2353,28 +4311,34 @@
}
}
},
- "201": {
- "description": "The album was created and bound to the caller",
- "content": {
- "application/json": {
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProvisionAlbumResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "200": {
- "description": "The album id was already provisioned to this account; nothing was written",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProvisionAlbumResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2383,56 +4347,70 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/albums/{album_id}/upgrade": {
- "get": {
- "summary": "Read the ceremony's phase and the drain count.",
- "description": "The one call a proposer polls between steps 2 and 4. `in_flight` reaching zero is the signal\nthat the tombstone may be committed.",
- "operationId": "album_upgrade_phase",
- "parameters": [
- {
- "name": "album_id",
- "in": "path",
- "description": "The album's id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "200": {
+ "description": "OK",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/ProfileResponse"
}
}
}
},
- "403": {
- "description": "Forbidden",
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2441,8 +4419,34 @@
}
}
},
- "400": {
- "description": "Bad Request",
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2451,28 +4455,63 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/UpgradePhaseResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2480,9 +4519,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -2490,28 +4526,52 @@
"bearer": []
}
]
- },
+ }
+ },
+ "/v1/auth/password": {
"post": {
- "summary": "Put an album into upgrade quiescence.",
- "description": "Idempotent under its own `intent_id`: versioning.md is explicit that the same `UpgradeIntent`\nnever produces two forks, and a proposer that lost an acknowledgement re-POSTs the same bytes.",
- "operationId": "begin_album_upgrade",
+ "summary": "Replace the password this account's sessions are opened with.",
+ "description": "# Every *other* session ends\n\nA password change whose point is that a credential has leaked would be worthless if the\nsessions opened with the leaked credential kept working. So the change closes every session\nof the account — and then re-opens the caller's own, **under its own session id**, so the\nperson doing the rotation is not signed out of the device they are doing it on while\neverybody else is.\n\nRe-opening the same id rather than minting a new one is what lets this answer `204` with no\nbody: the caller's existing token pair keeps working, because the session it names is still\nthere. Returning a fresh pair was considered and rejected — it would make this a second token\nmint with none of `POST /v1/auth/refresh`'s rotation discipline, for no gain.\n\nThe re-opened record's `authenticated_at` is **now**, and that is not bookkeeping: presenting\nthe current password *is* a credential presentation, so a freshness gate (`S-C7`) measuring\nfrom anything earlier would be measuring from the wrong moment.\n\n# Why the order is verify, write, revoke\n\nVerification first, because a wrong current password must change nothing. The write next,\nbecause a revocation that ran before it would sign everybody out and then fail. The\nrevocation last, and its failure is **logged and not returned**: the password is already\nchanged, so answering `500` would tell the caller the rotation did not happen when it did,\nand they would try again with a current password that is no longer current.",
+ "operationId": "change_password",
"parameters": [
{
- "name": "album_id",
- "in": "path",
- "description": "The album's id.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
"requestBody": {
"content": {
- "application/cbor": {
+ "application/json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/ChangePasswordRequest"
}
}
},
@@ -2528,6 +4588,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -2540,6 +4624,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2550,6 +4660,32 @@
},
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2559,8 +4695,34 @@
}
},
"415": {
- "description": "Unsupported media type",
- "content": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
"application/problem+json": {
"schema": {
"$ref": "#/components/schemas/CodedProblem"
@@ -2568,18 +4730,99 @@
}
}
},
- "200": {
- "description": "OK",
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/UpgradePhaseResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "404": {
- "description": "Not found",
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "423": {
+ "description": "Account locked",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2588,8 +4831,34 @@
}
}
},
- "409": {
- "description": "Upgrade in flight",
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2600,6 +4869,32 @@
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2609,7 +4904,69 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
}
},
"security": [
@@ -2617,28 +4974,44 @@
"bearer": []
}
]
- },
- "delete": {
- "summary": "Abort a ceremony, returning the album to normal operation.",
- "description": "Named by `intent_id` in the path's own query so that aborting is a statement about *which*\nupgrade — a caller that does not hold the live id gets a `409` rather than the power to\ncancel somebody else's ceremony.",
- "operationId": "abort_album_upgrade",
+ }
+ },
+ "/v1/auth/totp/enroll": {
+ "post": {
+ "summary": "Start enrolling an authenticator.",
+ "description": "Answers the `otpauth://` URI the app scans. Nothing is gated yet: until a code confirms the\nsecret, sign-in is unchanged — which is what stops a mis-scanned QR code from locking\nsomebody out of their own account.",
+ "operationId": "totp_enroll",
"parameters": [
{
- "name": "album_id",
- "in": "path",
- "description": "The album's id.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
{
- "name": "intent_id",
- "in": "query",
- "description": "The ceremony to abort.",
- "required": true,
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
"schema": {
- "type": "string"
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
@@ -2653,6 +5026,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -2665,16 +5062,32 @@
},
"403": {
"description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2685,26 +5098,68 @@
},
"200": {
"description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/UpgradePhaseResponse"
+ "$ref": "#/components/schemas/EnrollmentResponse"
}
}
}
},
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ "409": {
+ "description": "Already active",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "409": {
- "description": "Upgrade in flight",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2715,6 +5170,32 @@
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2724,44 +5205,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/quota": {
- "get": {
- "summary": "Report the authenticated uploader's storage-quota snapshot.",
- "description": "Scoped to the caller, and to nobody else: quota is accounted to the *uploader*, and one\naccount's storage use is not another's business.",
- "operationId": "get_quota",
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "description": "the request body exceeds the configured limit",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "403": {
- "description": "Forbidden",
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2770,18 +5269,34 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/QuotaResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2789,9 +5304,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -2801,10 +5313,55 @@
]
}
},
- "/v1/moderation/record": {
- "get": {
- "summary": "Serve the caller's own moderation record.",
- "operationId": "moderation_record",
+ "/v1/auth/totp/verify-enrollment": {
+ "post": {
+ "summary": "Confirm an enrollment with a live code.",
+ "description": "The confirming code is **spent**: its step goes straight into the replay ledger, so it cannot\nalso complete a sign-in a moment later. That is the one place the ledger's first entry comes\nfrom, and skipping it would leave the newest code in the account's history unused.",
+ "operationId": "totp_verify_enrollment",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodeRequest"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
"401": {
"description": "Unauthorized",
@@ -2816,6 +5373,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -2828,6 +5409,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2836,18 +5443,70 @@
}
}
},
- "200": {
- "description": "OK",
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/ModerationRecordResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "500": {
- "description": "Internal server error",
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2856,101 +5515,200 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/.well-known/capsule/attestation-keys": {
- "get": {
- "summary": "Serve this server's storage-attestation keys and their append-only history.",
- "description": "Cacheable and unauthenticated. It changes only when a key rotates, and a client that pinned\na stale copy still resolves every receipt signed before it fetched — which is the property\nthe append-only ordering buys.",
- "operationId": "attestation_keys",
- "responses": {
- "200": {
- "description": "OK",
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/AttestationKeysResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/.well-known/capsule/server-info": {
- "get": {
- "summary": "Serve this server's public, server-scoped facts.",
- "description": "Unauthenticated by contract: a client deciding whether it can talk to this server at all has\nno credential yet, and a peer resolving the key that verifies a capability token must not\nneed one from the server whose claims it is checking.",
- "operationId": "server_info",
- "responses": {
- "200": {
- "description": "OK",
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Nothing pending",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/ServerInfoResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/.well-known/capsule/deprecation": {
- "get": {
- "summary": "Serve the announced deprecation cutoffs.",
- "description": "The same announcements `server-info` carries, at their own path because that is the URL the\n`Warning:` header on a below-cutoff response points a human at, and because a client polling\nfor a cutoff should not have to refetch the whole discovery record to find one.",
- "operationId": "deprecation_announcements",
- "responses": {
- "200": {
- "description": "OK",
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/DeprecationsResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/.well-known/capsule/revoked-jti": {
- "get": {
- "summary": "Serve the federation capability revocation list.",
- "description": "Bounded by at most 24 hours of revocations, because an entry past the token's own `exp` is\npruned and a capability token cannot be minted to live longer than that. Public: a peer\nchecking whether a token it holds is still good is, by construction, not yet authenticated\nhere, and the record names no user — only opaque `jti`s.\n\n# Errors\n\nReturns `503` if the revocation list cannot be read. Deliberately *not* an empty list: an\nempty list is the strongest possible claim this endpoint can make — nothing is revoked — and\nserving it on a storage failure would turn an outage into a silent un-revocation of every\ntoken, which is exactly what the peer-side fail-closed rule exists to prevent.",
- "operationId": "revoked_jti",
- "responses": {
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/RevokedJtiResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "503": {
- "description": "Revocation list unavailable",
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -2958,29 +5716,51 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
- }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
}
},
- "/v1/upload": {
+ "/v1/auth/totp/disable": {
"post": {
- "summary": "Open an upload session for one blob of an asset bundle.",
- "description": "Runs the refuse-by-default envelope battery — invariants 1–8 and the top-level↔envelope\nconsistency family — **before** anything is written, then stages the session's file and\nrecords the session. A request whose `(owner, hash, album)` tuple already has an active\nsession gets that session back rather than a second one.",
- "operationId": "create_upload",
+ "summary": "Remove the second factor, on presentation of a live code.",
+ "description": "**A session is not enough.** The whole point of the factor is that a stolen access token is\ninsufficient, and a disable that took only a token would let the token turn off the control\nthat makes it insufficient.",
+ "operationId": "totp_disable",
"parameters": [
{
"name": "X-Capsule-Protocol",
"in": "header",
- "description": "The protocol date the client speaks.\n\nRead as a string rather than a typed value so that a malformed one is *this* surface's\ncoded `400` rather than the framework's uncoded one.",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
@@ -2988,7 +5768,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateUploadRequest"
+ "$ref": "#/components/schemas/CodeRequest"
}
}
},
@@ -3005,6 +5785,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -3017,6 +5821,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3027,6 +5857,32 @@
},
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3037,8 +5893,34 @@
},
"415": {
"description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
"schema": {
"$ref": "#/components/schemas/CodedProblem"
}
@@ -3047,6 +5929,32 @@
},
"422": {
"description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3055,130 +5963,164 @@
}
}
},
- "201": {
- "description": "Upload session created",
+ "204": {
+ "description": "the request succeeded and there is no content to send",
"headers": {
- "Location": {
- "description": "Where the session lives.",
- "required": false,
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "X-Capsule-Suggested-Chunk-Size": {
- "description": "The starting chunk size.",
- "required": false,
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 0.0,
- "format": "uint64"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "X-Capsule-Offset": {
- "description": "The authoritative offset, on a resumed session.",
- "required": false,
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 0.0,
- "format": "uint64"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Not enrolled",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
},
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/CreateUploadResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "200": {
- "description": "The active session for these bytes, to resume",
+ "500": {
+ "description": "Internal server error",
"headers": {
- "Location": {
- "description": "Where the session lives.",
- "required": false,
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "X-Capsule-Suggested-Chunk-Size": {
- "description": "The starting chunk size.",
- "required": false,
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 0.0,
- "format": "uint64"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "X-Capsule-Offset": {
- "description": "The authoritative offset, on a resumed session.",
- "required": false,
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 0.0,
- "format": "uint64"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
},
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/CreateUploadResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "409": {
- "description": "Album quiescing",
- "content": {
- "application/problem+json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/DuplicateBlobProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
"426": {
"description": "Protocol version unsupported",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProtocolRangeProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "413": {
- "description": "File too large",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3195,67 +6137,84 @@
]
}
},
- "/v1/upload/{id}": {
- "delete": {
- "summary": "Cancel a session: its record, its accepted chunks and its staged bytes, together.",
- "description": "Refused while finalization is running — it is not interruptible — and refused once the\nsession is terminal, because there is nothing left to cancel and the receipt is what a\nclient should read instead.",
- "operationId": "cancel_upload",
+ "/v1/auth/login/verify-totp": {
+ "post": {
+ "summary": "Complete a sign-in with a code.",
+ "description": "This is where the session is opened — not `POST /v1/auth/login`, which for an account with a\nsecond factor opens nothing. The advisory `cohort_hash` and `device_id` ride *this* request\nfor the same reason: the session they describe is created here.",
+ "operationId": "totp_verify_login",
"parameters": [
{
- "name": "id",
- "in": "path",
- "description": "The session's identifier, as `POST /v1/upload` returned it.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
{
- "name": "X-Capsule-Protocol",
+ "name": "X-Capsule-Crypto-Suite",
"in": "header",
- "description": "The protocol date the client speaks.\n\nRead as a string rather than a typed value so that a malformed one is *this* surface's\ncoded `400` rather than the framework's uncoded one.",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VerifyLoginRequest"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Bad Request",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3264,21 +6223,34 @@
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "426": {
- "description": "Protocol version unsupported",
- "content": {
- "application/problem+json": {
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProtocolRangeProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "404": {
- "description": "Upload session not found",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3287,8 +6259,34 @@
}
}
},
- "409": {
- "description": "Session not active",
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3297,64 +6295,68 @@
}
}
},
- "500": {
- "description": "Internal server error",
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/TokenResponse"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- },
- "head": {
- "summary": "Report a session's progress and state.",
- "description": "The resumption primitive: a client that lost a connection, an acknowledgement, or a process\nasks here and learns the authoritative offset, the declared length and the session's state.\nThe answer carries **no body** — HTTP forbids one on `HEAD`, which is why the protocol puts\nall three on headers.",
- "operationId": "head_upload",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "The session's identifier, as `POST /v1/upload` returned it.",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "X-Capsule-Protocol",
- "in": "header",
- "description": "The protocol date the client speaks.\n\nRead as a string rather than a typed value so that a malformed one is *this* surface's\ncoded `400` rather than the framework's uncoded one.",
- "required": false,
- "schema": {
- "type": [
- "string",
- "null"
- ]
- }
- }
- ],
- "responses": {
"401": {
- "description": "Unauthorized",
+ "description": "Challenge expired",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -3365,8 +6367,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
+ "429": {
+ "description": "Too many attempts",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3375,8 +6403,34 @@
}
}
},
- "400": {
- "description": "Bad Request",
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3385,65 +6439,63 @@
}
}
},
- "200": {
- "description": "Progress and state on X-Capsule-* headers, no body",
+ "413": {
+ "description": "the request body exceeds the configured limit",
"headers": {
- "X-Capsule-Offset": {
- "description": "The next byte the server expects.",
- "required": true,
- "schema": {
- "type": "integer",
- "minimum": 0.0,
- "format": "uint64"
- }
- },
- "X-Capsule-Content-Length": {
- "description": "The declared total, fixed at creation.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "integer",
- "minimum": 0.0,
- "format": "uint64"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "X-Capsule-Upload-Status": {
- "description": "Where the session is in its state machine.",
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Cache-Control": {
- "description": "`no-store`: progress is not cacheable.",
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
"426": {
"description": "Protocol version unsupported",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProtocolRangeProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Upload session not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3451,79 +6503,49 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
}
- ]
- },
- "patch": {
- "summary": "Append a chunk, and finalize when it completes the declared size.",
- "description": "Every rule the [chunk\ncontract](../../../capsule-docs/src/content/docs/design/import/upload-protocol.md) fixes is\nchecked before a byte is written, and the checksum is verified against the received bytes\n*first*, so a chunk corrupted in transit persists nothing.",
- "operationId": "append_chunk",
+ }
+ }
+ },
+ "/v1/auth/devices/enroll": {
+ "post": {
+ "summary": "Issue a one-time enrollment code for the caller's account.",
+ "description": "Gated on a recent credential presentation, not merely on a valid session — a stolen token\nmust not be able to enroll a rogue device. See [`crate::enrollment`] for exactly how much\nthat gate can mean.",
+ "operationId": "issue_enrollment_code",
"parameters": [
{
- "name": "id",
- "in": "path",
- "description": "The session's identifier, as `POST /v1/upload` returned it.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
{
- "name": "X-Capsule-Protocol",
+ "name": "X-Capsule-Crypto-Suite",
"in": "header",
- "description": "The protocol date the client speaks.",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
},
{
- "name": "X-Capsule-Offset",
+ "name": "X-Capsule-Sidecar-Schema",
"in": "header",
- "description": "Where in the blob this chunk starts.",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
- }
- },
- {
- "name": "X-Capsule-Checksum",
- "in": "header",
- "description": "The chunk's SHA-256, bare lowercase hex. Required: the idempotency tuple is undefined\nwithout it.",
- "required": false,
- "schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
- "requestBody": {
- "content": {
- "application/octet-stream": {
- "schema": {
- "type": "string",
- "format": "binary"
- }
- }
- },
- "required": true
- },
"responses": {
"401": {
"description": "Unauthorized",
@@ -3535,6 +6557,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -3547,6 +6593,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3555,18 +6627,70 @@
}
}
},
- "400": {
- "description": "Bad Request",
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/EnrollmentCodeResponse"
}
}
}
},
- "415": {
- "description": "Unsupported media type",
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3575,32 +6699,63 @@
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send",
+ "413": {
+ "description": "the request body exceeds the configured limit",
"headers": {
- "X-Capsule-Offset": {
- "description": "The next byte the server expects.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "integer",
- "minimum": 0.0,
- "format": "uint64"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
"426": {
"description": "Protocol version unsupported",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProtocolRangeProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "404": {
- "description": "Upload session not found",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3609,28 +6764,34 @@
}
}
},
- "409": {
- "description": "Offset mismatch",
- "content": {
- "application/problem+json": {
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/OffsetMismatchProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "413": {
- "description": "Chunk too large",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3647,58 +6808,84 @@
]
}
},
- "/v1/upload/sessions": {
- "get": {
- "summary": "Every upload the caller can resume.",
- "description": "Oldest first, which is the order the store promises and the order a client wants: the oldest\nin-flight session is the one closest to eviction.",
- "operationId": "list_upload_sessions",
+ "/v1/auth/devices/enroll/redeem": {
+ "post": {
+ "summary": "Redeem a code for a relay channel.",
+ "description": "**Unauthenticated, necessarily.** Device B has no account, no session and no key material —\nit is a phone that has just scanned a QR code. The code is the only thing it holds, so the\ncode is the credential.",
+ "operationId": "redeem_enrollment_code",
"parameters": [
{
- "name": "status",
- "in": "query",
- "description": "Return only sessions in this state.\n\nOne of `pending`, `uploading`, `waiting_for_processing`, `completed`,\n`failed_processing` — the same tokens the `X-Capsule-Upload-Status` header carries, so a\nclient filters on the value it was already given rather than on a second vocabulary.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RedeemRequest"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Bad Request",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3707,63 +6894,32 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/SessionsResponse"
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/upload/{id}/receipt": {
- "get": {
- "summary": "Fetch the custody receipt for a finalized upload.",
- "operationId": "get_upload_receipt",
- "parameters": [
- {
- "name": "id",
- "in": "path",
- "description": "The session's identifier, as `POST /v1/upload` returned it.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
- "headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -3774,18 +6930,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3796,93 +6968,66 @@
},
"200": {
"description": "OK",
- "content": {
- "application/cbor": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
"type": "string",
- "format": "binary"
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "409": {
- "description": "Receipt not available",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/ChannelResponse"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/albums/{album_id}/ops": {
- "post": {
- "summary": "Apply one signed lifecycle manifest to an album's asset.",
- "description": "The whole battery runs before anything is written, and a rejection writes nothing —\nincluding the blobs the bundle carries, which are stored only after the manifest has passed\nevery check the server can make without a key.",
- "operationId": "album_lifecycle_op",
- "parameters": [
- {
- "name": "album_id",
- "in": "path",
- "description": "The album's identifier.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/OpRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "404": {
+ "description": "Code refused",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -3893,38 +7038,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "429": {
+ "description": "Too many attempts",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "415": {
- "description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "422": {
- "description": "Unprocessable Entity",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3933,38 +7074,34 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/OpResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "426": {
- "description": "Upgrade required",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProtocolRangeProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "409": {
- "description": "Stale revival",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/StaleRevivalProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -3974,103 +7111,62 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/sync": {
- "get": {
- "summary": "Returns the changes in the caller's library after `cursor`.",
- "description": "Read-only and idempotent: two calls with the same cursor return the same page, because the\ncursor names a position rather than consuming one. That is what makes a lost response\nharmless and a retry free.",
- "operationId": "sync_feed",
- "parameters": [
- {
- "name": "cursor",
- "in": "query",
- "description": "The opaque cursor a previous page returned. Absent means \"from the beginning\".",
- "required": false,
- "schema": {
- "type": [
- "string",
- "null"
- ]
- }
- },
- {
- "name": "page_size",
- "in": "query",
- "description": "How many entries to return. Clamped into the range this server serves.\n\n`u32` and not `usize`: Kynos refuses to describe a platform-width integer, and it is\nright to — a schema whose bounds depend on the server's pointer size is a schema no\nclient can rely on.",
- "required": false,
- "schema": {
- "type": [
- "integer",
- "null"
- ],
- "maximum": 4294967295.0,
- "minimum": 0.0,
- "format": "uint32"
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "description": "the request body exceeds the configured limit",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/SyncPageResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4078,101 +7174,96 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
}
- ]
+ }
}
},
- "/v1/blob/{hash}": {
+ "/v1/auth/devices/enroll/channel/{channel_id}": {
"get": {
- "summary": "Fetch a ciphertext blob by its content address, ranged.",
- "description": "Opaque octets: the server holds no key and this route never learns what it is serving. Any\nauthenticated account may fetch any live address — see [`crate::serve`] for why that is a\ncapability model rather than a hole, and for the `403` the contract describes and nothing\nimplements.\n\nThe one answer that *is* account-scoped is the transient `409`: it reports the caller's own\nin-flight upload and nobody else's (`S-C40`).",
- "operationId": "get_blob",
+ "summary": "Take everything pending in one of a channel's mailboxes.",
+ "description": "Destructive: a relayed payload is delivered once. Draining one direction leaves the other\nuntouched, so the two devices do not consume each other's mail.",
+ "operationId": "drain_enrollment_channel",
"parameters": [
{
- "name": "hash",
+ "name": "channel_id",
"in": "path",
- "description": "The blob's ciphertext content address, lowercase hex.",
+ "description": "The handle a redeemed code returned.",
"required": true,
"schema": {
"type": "string"
}
},
{
- "name": "Range",
- "in": "header",
- "description": "The part of the representation to transfer, per RFC 9110 section 14.2. A field this operation cannot apply is ignored and the whole representation is sent.",
+ "name": "direction",
+ "in": "query",
+ "description": "`to_initiator` or `to_enrollee`.",
+ "required": true,
"schema": {
- "type": "string",
- "pattern": "^bytes=(?:\\d+-\\d*|-\\d+)(?:\\s*,\\s*(?:\\d+-\\d*|-\\d+)){0,7}$"
- },
- "example": "bytes=0-1023"
+ "type": "string"
+ }
},
{
- "name": "If-Range",
+ "name": "X-Capsule-Protocol",
"in": "header",
- "description": "The entity tag the client's partial copy came from, per RFC 9110 section 13.1.5. The `Range` is honoured only if it matches this representation under the strong comparison; otherwise the whole representation is sent.",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
{
- "name": "If-None-Match",
+ "name": "X-Capsule-Crypto-Suite",
"in": "header",
- "description": "The entity tag the client already holds, per RFC 9110 section 13.1.2",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
"schema": {
- "type": "string"
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
},
{
- "name": "If-Modified-Since",
+ "name": "X-Capsule-Sidecar-Schema",
"in": "header",
- "description": "The date the client's copy carries, per RFC 9110 section 13.1.3",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
"schema": {
- "type": "string"
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
"responses": {
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Bad Request",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4182,90 +7273,105 @@
}
},
"200": {
- "description": "the whole representation",
+ "description": "OK",
"headers": {
- "Accept-Ranges": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "ETag": {
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Last-Modified": {
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
},
"content": {
- "application/octet-stream": {
+ "application/json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/DrainResponse"
}
}
}
},
- "206": {
- "description": "the part the request asked for",
+ "404": {
+ "description": "Channel not found",
"headers": {
- "Accept-Ranges": {
- "schema": {
- "type": "string"
- }
- },
- "ETag": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Last-Modified": {
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Content-Range": {
- "description": "The part of the representation enclosed, and its complete length, per RFC 9110 section 14.4.",
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
"required": true,
- "content": {
- "text/plain": {
- "schema": {
- "type": "string",
- "pattern": "^bytes \\d+-\\d+/\\d+$"
- }
- }
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
},
"content": {
- "application/octet-stream": {
+ "application/problem+json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "304": {
- "description": "the client's copy is current",
+ "500": {
+ "description": "Internal server error",
"headers": {
- "ETag": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Last-Modified": {
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "404": {
- "description": "Not found",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4274,95 +7380,123 @@
}
}
},
- "409": {
- "description": "Upload in progress",
- "content": {
- "application/problem+json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "410": {
- "description": "Gone",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
- },
- "security": [
+ }
+ },
+ "post": {
+ "summary": "Append a payload to one of a channel's two mailboxes.",
+ "description": "Unauthenticated and gated by the handle alone. The relay is a dumb pipe by design — see\n[`crate::enrollment`] — and the safety-code check is what defends the ceremony.",
+ "operationId": "relay_enrollment_payload",
+ "parameters": [
{
- "bearer": []
+ "name": "channel_id",
+ "in": "path",
+ "description": "The handle a redeemed code returned.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
}
- ]
- }
- },
- "/v1/storage/verify": {
- "post": {
- "summary": "Confirm that the server holds the copies a client is about to stop holding.",
- "description": "A pure read: it writes no blob, no index row and no verdict. Soundness against a racing\ncollection comes from the standing GC grace window rather than from a per-request lease,\nwhich is why nothing here takes one.",
- "operationId": "verify_storage",
+ ],
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/StorageVerifyRequest"
+ "$ref": "#/components/schemas/RelayRequest"
}
}
},
"required": true
},
"responses": {
- "401": {
- "description": "Unauthorized",
+ "400": {
+ "description": "Bad Request",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4373,6 +7507,32 @@
},
"415": {
"description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4383,6 +7543,32 @@
},
"422": {
"description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4391,63 +7577,61 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/StorageVerifyResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/v1/assets/{asset_id}/receipts": {
- "get": {
- "summary": "Fetch every custody receipt covering one asset.",
- "operationId": "get_asset_receipts",
- "parameters": [
- {
- "name": "asset_id",
- "in": "path",
- "description": "The asset id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "404": {
+ "description": "Channel not found",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -4458,18 +7642,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4478,28 +7678,63 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/AssetReceiptsResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "500": {
- "description": "Internal server error",
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4507,32 +7742,56 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
- },
- "security": [
+ }
+ },
+ "delete": {
+ "summary": "Close a channel and drop both mailboxes with it.",
+ "description": "**The initiator's, and authenticated.** A close is the one relay operation that is not\nidempotent from the other device's point of view — it ends the ceremony — so leaving it on\nthe handle alone would make an abandoned QR code a denial of service. The account is checked\nagainst the channel's recorded initiator, and a channel belonging to another account answers\nexactly as an unknown one does.",
+ "operationId": "close_enrollment_channel",
+ "parameters": [
{
- "bearer": []
- }
- ]
- }
- },
- "/v1/shares": {
- "post": {
- "summary": "Register a share link the caller's client has issued.",
- "operationId": "issue_share",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/IssueShareRequest"
- }
+ "name": "channel_id",
+ "in": "path",
+ "description": "The handle a redeemed code returned.",
+ "required": true,
+ "schema": {
+ "type": "string"
}
},
- "required": true
- },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
"responses": {
"401": {
"description": "Unauthorized",
@@ -4544,6 +7803,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -4556,6 +7839,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4566,6 +7875,32 @@
},
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4574,8 +7909,63 @@
}
}
},
- "415": {
- "description": "Unsupported Media Type",
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Channel not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4584,8 +7974,34 @@
}
}
},
- "422": {
- "description": "Unprocessable Entity",
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4594,18 +8010,63 @@
}
}
},
- "201": {
- "description": "The share link is registered and servable",
- "content": {
- "application/json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/IssueShareResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "500": {
- "description": "Internal server error",
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4613,9 +8074,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -4625,22 +8083,55 @@
]
}
},
- "/v1/shares/{opaque_id}": {
- "delete": {
- "summary": "Revoke one of the caller's links.",
- "description": "Idempotent from the caller's side and **indistinguishable**: a link that was never theirs, a\nlink that does not exist, and a link they already revoked are all `204`. Revocation is the\none operation where saying \"there was nothing to revoke\" would be a lookup.",
- "operationId": "revoke_share",
+ "/v1/albums": {
+ "post": {
+ "summary": "Bind an album id to the authenticated caller.",
+ "description": "Idempotent: the same id from a second device, or after a recovery, is a success that writes\nnothing.",
+ "operationId": "provision_album",
"parameters": [
{
- "name": "opaque_id",
- "in": "path",
- "description": "The opaque id.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProvisionAlbumRequest"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
"401": {
"description": "Unauthorized",
@@ -4652,6 +8143,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -4664,29 +8179,32 @@
},
"403": {
"description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4695,35 +8213,34 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/s/{opaque_id}": {
- "get": {
- "summary": "What a viewer needs to begin, for a live link.",
- "operationId": "share_metadata",
- "parameters": [
- {
- "name": "opaque_id",
- "in": "path",
- "description": "The opaque id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4732,18 +8249,34 @@
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/json": {
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/SharedMetadataResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "404": {
- "description": "Not found",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4752,18 +8285,34 @@
}
}
},
- "429": {
- "description": "Too many requests",
- "content": {
- "application/problem+json": {
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4772,148 +8321,272 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/s/{opaque_id}/wrapped-secret": {
- "get": {
- "summary": "The passphrase-wrapped scope material, when there is one.",
- "description": "A link with no passphrase answers `404` rather than `204` or an empty body: whether a link is\npassphrase-protected is already disclosed by the metadata record, and a *second* way to ask\nthe same question with a different shape is a second thing to keep consistent.",
- "operationId": "share_wrapped_secret",
- "parameters": [
- {
- "name": "opaque_id",
- "in": "path",
- "description": "The opaque id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "responses": {
- "400": {
- "description": "Bad Request",
+ "201": {
+ "description": "The album was created and bound to the caller",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/ProvisionAlbumResponse"
}
}
}
},
"200": {
- "description": "OK",
- "content": {
- "application/octet-stream": {
+ "description": "The album id was already provisioned to this account; nothing was written",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
"type": "string",
- "format": "binary"
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "429": {
- "description": "Too many requests",
+ },
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/ProvisionAlbumResponse"
}
}
}
},
"500": {
"description": "Internal server error",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
},
- "/s/{opaque_id}/blob/{hash}": {
+ "/v1/albums/{album_id}/upgrade": {
"get": {
- "summary": "Ciphertext for one of the link's blobs, ranged.",
- "description": "The membership check is the security property: a link serves the addresses its record\nenumerates and nothing else, so it cannot be walked sideways into the album's unstripped\nmetadata. A blob the link does not name is the same `404` as a link that does not exist.",
- "operationId": "share_blob",
+ "summary": "Read the ceremony's phase and the drain count.",
+ "description": "The one call a proposer polls between steps 2 and 4. `in_flight` reaching zero is the signal\nthat the tombstone may be committed.",
+ "operationId": "album_upgrade_phase",
"parameters": [
{
- "name": "opaque_id",
- "in": "path",
- "description": "The opaque id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "hash",
+ "name": "album_id",
"in": "path",
- "description": "The blob's content address.",
+ "description": "The album's id.",
"required": true,
"schema": {
"type": "string"
}
},
{
- "name": "Range",
+ "name": "X-Capsule-Protocol",
"in": "header",
- "description": "The part of the representation to transfer, per RFC 9110 section 14.2. A field this operation cannot apply is ignored and the whole representation is sent.",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
"schema": {
"type": "string",
- "pattern": "^bytes=(?:\\d+-\\d*|-\\d+)(?:\\s*,\\s*(?:\\d+-\\d*|-\\d+)){0,7}$"
- },
- "example": "bytes=0-1023"
- },
- {
- "name": "If-Range",
- "in": "header",
- "description": "The entity tag the client's partial copy came from, per RFC 9110 section 13.1.5. The `Range` is honoured only if it matches this representation under the strong comparison; otherwise the whole representation is sent.",
- "schema": {
- "type": "string"
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
{
- "name": "If-None-Match",
+ "name": "X-Capsule-Crypto-Suite",
"in": "header",
- "description": "The entity tag the client already holds, per RFC 9110 section 13.1.2",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
"schema": {
- "type": "string"
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
},
{
- "name": "If-Modified-Since",
+ "name": "X-Capsule-Sidecar-Schema",
"in": "header",
- "description": "The date the client's copy carries, per RFC 9110 section 13.1.3",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
"schema": {
- "type": "string"
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
"responses": {
- "400": {
- "description": "Bad Request",
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4922,101 +8595,142 @@
}
}
},
- "200": {
- "description": "the whole representation",
+ "403": {
+ "description": "Forbidden",
"headers": {
- "Accept-Ranges": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "ETag": {
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Last-Modified": {
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
},
"content": {
- "application/octet-stream": {
+ "application/problem+json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "206": {
- "description": "the part the request asked for",
+ "400": {
+ "description": "Bad Request",
"headers": {
- "Accept-Ranges": {
- "schema": {
- "type": "string"
- }
- },
- "ETag": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Last-Modified": {
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Content-Range": {
- "description": "The part of the representation enclosed, and its complete length, per RFC 9110 section 14.4.",
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
"required": true,
- "content": {
- "text/plain": {
- "schema": {
- "type": "string",
- "pattern": "^bytes \\d+-\\d+/\\d+$"
- }
- }
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
},
"content": {
- "application/octet-stream": {
+ "application/problem+json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "304": {
- "description": "the client's copy is current",
+ "200": {
+ "description": "OK",
"headers": {
- "ETag": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
},
- "Last-Modified": {
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpgradePhaseResponse"
}
}
}
},
"404": {
"description": "Not found",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "429": {
- "description": "Too many requests",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5027,77 +8741,32 @@
},
"500": {
"description": "Internal server error",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/v1/drops/links": {
- "post": {
- "summary": "Provision an upload link.",
- "operationId": "provision_link",
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/ProvisionLinkRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
- "401": {
- "description": "Unauthorized",
- "headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
- }
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
- "schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "415": {
- "description": "Unsupported Media Type",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5106,38 +8775,34 @@
}
}
},
- "422": {
- "description": "Unprocessable Entity",
- "content": {
- "application/problem+json": {
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "201": {
- "description": "The upload link is provisioned and accepting drops",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/ProvisionLinkResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -5145,24 +8810,65 @@
"bearer": []
}
]
- }
- },
- "/v1/drops/links/{opaque_id}": {
- "delete": {
- "summary": "Revoke one of the caller's links.",
- "description": "Indistinguishable and idempotent, for the same reason a share revocation is: saying \"there\nwas nothing to revoke\" would be a lookup.",
- "operationId": "revoke_link",
+ },
+ "post": {
+ "summary": "Put an album into upgrade quiescence.",
+ "description": "Idempotent under its own `intent_id`: versioning.md is explicit that the same `UpgradeIntent`\nnever produces two forks, and a proposer that lost an acknowledgement re-POSTs the same bytes.",
+ "operationId": "begin_album_upgrade",
"parameters": [
{
- "name": "opaque_id",
+ "name": "album_id",
"in": "path",
- "description": "The opaque id.",
+ "description": "The album's id.",
"required": true,
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
}
],
+ "requestBody": {
+ "content": {
+ "application/cbor": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
"401": {
"description": "Unauthorized",
@@ -5174,6 +8880,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -5186,29 +8916,32 @@
},
"403": {
"description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5217,46 +8950,34 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- },
- "/d/{opaque_id}": {
- "post": {
- "summary": "Open a drop session through a link.",
- "description": "Invariants 26–30 in order: the link admits the file and reserves its caps in one store\noperation, then the owner's quota is charged, then the declaration is checked.",
- "operationId": "create_drop",
- "parameters": [
- {
- "name": "opaque_id",
- "in": "path",
- "description": "The opaque id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/CreateDropRequest"
- }
- }
- },
- "required": true
- },
- "responses": {
"400": {
"description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5266,7 +8987,33 @@
}
},
"415": {
- "description": "Unsupported Media Type",
+ "description": "Unsupported media type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5275,28 +9022,70 @@
}
}
},
- "422": {
- "description": "Unprocessable Entity",
- "content": {
- "application/problem+json": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "201": {
- "description": "A drop session is open and accepting chunks",
+ },
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/CreateDropResponse"
+ "$ref": "#/components/schemas/UpgradePhaseResponse"
}
}
}
},
"404": {
"description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5305,8 +9094,34 @@
}
}
},
- "403": {
- "description": "Passphrase required",
+ "409": {
+ "description": "Upgrade in flight",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5315,8 +9130,34 @@
}
}
},
- "409": {
- "description": "Link capacity exhausted",
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5326,27 +9167,62 @@
}
},
"413": {
- "description": "File too large",
- "content": {
- "application/problem+json": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/FileTooLargeProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
}
},
- "429": {
- "description": "Too many requests",
- "content": {
- "application/problem+json": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "500": {
- "description": "Internal server error",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5355,82 +9231,106 @@
}
}
}
- }
- }
- },
- "/d/{opaque_id}/{upload_id}": {
- "patch": {
- "summary": "Append one chunk to a drop session.",
- "description": "The link is the credential: possession of the opaque id, plus a session that belongs to it.\nEverything after that is [`crate::upload::chunk::append`] — the album path's own function.",
- "operationId": "append_drop_chunk",
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ },
+ "delete": {
+ "summary": "Abort a ceremony, returning the album to normal operation.",
+ "description": "Named by `intent_id` in the path's own query so that aborting is a statement about *which*\nupgrade — a caller that does not hold the live id gets a `409` rather than the power to\ncancel somebody else's ceremony.",
+ "operationId": "abort_album_upgrade",
"parameters": [
{
- "name": "opaque_id",
+ "name": "album_id",
"in": "path",
- "description": "The opaque id of the link the session belongs to.",
+ "description": "The album's id.",
"required": true,
"schema": {
"type": "string"
}
},
{
- "name": "upload_id",
- "in": "path",
- "description": "The session id.",
+ "name": "intent_id",
+ "in": "query",
+ "description": "The ceremony to abort.",
"required": true,
"schema": {
"type": "string"
}
},
{
- "name": "X-Capsule-Offset",
+ "name": "X-Capsule-Protocol",
"in": "header",
- "description": "Where in the blob this chunk starts.",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
},
{
- "name": "X-Capsule-Checksum",
+ "name": "X-Capsule-Sidecar-Schema",
"in": "header",
- "description": "The chunk's SHA-256, bare lowercase hex. Required: the idempotency tuple is undefined\nwithout it.",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
"required": false,
"schema": {
- "type": [
- "string",
- "null"
- ]
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
- "requestBody": {
- "content": {
- "application/octet-stream": {
- "schema": {
- "type": "string",
- "format": "binary"
- }
- }
- },
- "required": true
- },
"responses": {
- "400": {
- "description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "415": {
- "description": "Unsupported media type",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5439,32 +9339,34 @@
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send",
+ "403": {
+ "description": "Forbidden",
"headers": {
- "X-Capsule-Offset": {
- "description": "Where the session is now.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "integer",
- "minimum": 0.0,
- "format": "uint64"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "404": {
- "description": "Not found",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "409": {
- "description": "Chunk refused",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5473,49 +9375,106 @@
}
}
},
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
"schema": {
"$ref": "#/components/schemas/CodedProblem"
}
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- }
- }
- },
- "/v1/drops": {
- "get": {
- "summary": "The caller's pending drops.",
- "operationId": "list_inbox",
- "responses": {
- "401": {
- "description": "Unauthorized",
+ "200": {
+ "description": "OK",
"headers": {
- "WWW-Authenticate": {
- "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
"required": true,
"schema": {
- "type": "string"
- },
- "example": "Bearer"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/UpgradePhaseResponse"
}
}
}
},
- "403": {
- "description": "Forbidden",
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5524,18 +9483,70 @@
}
}
},
- "200": {
- "description": "OK",
+ "409": {
+ "description": "Upgrade in flight",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
- "application/json": {
+ "application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/InboxResponse"
+ "$ref": "#/components/schemas/CodedProblem"
}
}
}
},
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5545,7 +9556,69 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
}
},
"security": [
@@ -5555,27 +9628,58 @@
]
}
},
- "/v1/drops/{drop_id}/adopt": {
- "post": {
- "summary": "Adopt a pending drop into an album.",
- "description": "Invariant 32. The manifest re-runs the create battery — a drop that skipped it would be the\none write on this server that entered an album unvalidated — and its `ciphertext_hash` must\nname a blob in **the caller's own inbox**, which is what stops an adoption from minting an\nasset over somebody else's bytes.\n\nThe row is **claimed, written, then settled**. Across two ports there is no transaction, and\nthe two failure directions are not equal: writing first and deleting after can duplicate a\nphoto, taking first and failing to write loses one. A claim leaves a crash visible in the\nowner's own inbox instead, marked `adopting`.",
- "operationId": "adopt_drop",
+ "/v1/albums/{album_id}/roster": {
+ "put": {
+ "summary": "Publish the caller's roster for one of their albums.",
+ "operationId": "publish_album_roster",
"parameters": [
{
- "name": "drop_id",
+ "name": "album_id",
"in": "path",
- "description": "The drop's identifier.",
+ "description": "The album's id.",
"required": true,
"schema": {
"type": "string"
}
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AdoptRequest"
+ "$ref": "#/components/schemas/RosterRequest"
}
}
},
@@ -5592,6 +9696,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -5604,6 +9732,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5614,46 +9768,176 @@
},
"400": {
"description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "415": {
- "description": "Unsupported Media Type",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "422": {
- "description": "Unprocessable Entity",
+ },
"content": {
"application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/RosterVersionLeapProblem"
}
}
}
},
- "200": {
- "description": "OK",
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/AdoptResponse"
+ "$ref": "#/components/schemas/RosterResponse"
}
}
}
},
"404": {
"description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5662,8 +9946,70 @@
}
}
},
+ "409": {
+ "description": "Roster stale",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/RosterStaleProblem"
+ }
+ }
+ }
+ },
"500": {
"description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5673,7 +10019,69 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
}
},
"security": [
@@ -5683,22 +10091,55 @@
]
}
},
- "/v1/drops/{drop_id}": {
- "delete": {
- "summary": "Discard a pending drop.",
- "description": "The bytes become unreferenced and the collector reclaims them; the link's cap is **not**\nrefunded, because the drop did happen — a guest deposited a file and the owner chose not to\nkeep it, which is not the same as a link slot never having been used.",
- "operationId": "discard_drop",
+ "/v1/upload": {
+ "post": {
+ "summary": "Open an upload session for one blob of an asset bundle.",
+ "description": "Runs the refuse-by-default envelope battery — invariants 1–8 and the top-level↔envelope\nconsistency family — **before** anything is written, then stages the session's file and\nrecords the session. A request whose `(owner, hash, album)` tuple already has an active\nsession gets that session back rather than a second one.",
+ "operationId": "create_upload",
"parameters": [
{
- "name": "drop_id",
- "in": "path",
- "description": "The drop's identifier.",
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
"required": true,
"schema": {
- "type": "string"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
}
}
],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateUploadRequest"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
"401": {
"description": "Unauthorized",
@@ -5710,6 +10151,30 @@
"type": "string"
},
"example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -5722,6 +10187,32 @@
},
"403": {
"description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5732,19 +10223,104 @@
},
"400": {
"description": "Bad Request",
- "content": {
- "application/problem+json": {
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
},
- "204": {
- "description": "the request succeeded and there is no content to send"
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
},
- "404": {
- "description": "Not found",
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5753,8 +10329,210 @@
}
}
},
- "500": {
- "description": "Internal server error",
+ "201": {
+ "description": "Upload session created",
+ "headers": {
+ "Location": {
+ "description": "Where the session lives.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "X-Capsule-Suggested-Chunk-Size": {
+ "description": "The starting chunk size.",
+ "required": false,
+ "schema": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Offset": {
+ "description": "The authoritative offset, on a resumed session.",
+ "required": false,
+ "schema": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateUploadResponse"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "The active session for these bytes, to resume",
+ "headers": {
+ "Location": {
+ "description": "Where the session lives.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "X-Capsule-Suggested-Chunk-Size": {
+ "description": "The starting chunk size.",
+ "required": false,
+ "schema": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Offset": {
+ "description": "The authoritative offset, on a resumed session.",
+ "required": false,
+ "schema": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateUploadResponse"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Album quiescing",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/DuplicateBlobProblem"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5764,1246 +10542,10710 @@
}
},
"413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
- }
- ]
- }
- }
- },
- "components": {
- "schemas": {
- "VersionResponse": {
- "properties": {
- "name": {
- "type": "string",
- "description": "The server package name."
- },
- "version": {
+ "description": "File too large",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/upload/{id}": {
+ "delete": {
+ "summary": "Cancel a session: its record, its accepted chunks and its staged bytes, together.",
+ "description": "Refused while finalization is running — it is not interruptible — and refused once the\nsession is terminal, because there is nothing left to cancel and the receipt is what a\nclient should read instead.",
+ "operationId": "cancel_upload",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "The session's identifier, as `POST /v1/upload` returned it.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Upload session not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Session not active",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ },
+ "head": {
+ "summary": "Report a session's progress and state.",
+ "description": "The resumption primitive: a client that lost a connection, an acknowledgement, or a process\nasks here and learns the authoritative offset, the declared length and the session's state.\nThe answer carries **no body** — HTTP forbids one on `HEAD`, which is why the protocol puts\nall three on headers.",
+ "operationId": "head_upload",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "The session's identifier, as `POST /v1/upload` returned it.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "Progress and state on X-Capsule-* headers, no body",
+ "headers": {
+ "X-Capsule-Offset": {
+ "description": "The next byte the server expects.",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Content-Length": {
+ "description": "The declared total, fixed at creation.",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Upload-Status": {
+ "description": "Where the session is in its state machine.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Cache-Control": {
+ "description": "`no-store`: progress is not cacheable.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Upload session not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ },
+ "patch": {
+ "summary": "Append a chunk, and finalize when it completes the declared size.",
+ "description": "Every rule the [chunk\ncontract](../../../capsule-docs/src/content/docs/design/import/upload-protocol.md) fixes is\nchecked before a byte is written, and the checksum is verified against the received bytes\n*first*, so a chunk corrupted in transit persists nothing.",
+ "operationId": "append_chunk",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "The session's identifier, as `POST /v1/upload` returned it.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Offset",
+ "in": "header",
+ "description": "Where in the blob this chunk starts.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ {
+ "name": "X-Capsule-Checksum",
+ "in": "header",
+ "description": "The chunk's SHA-256, bare lowercase hex. Required: the idempotency tuple is undefined\nwithout it.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported media type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Offset": {
+ "description": "The next byte the server expects.",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Upload session not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Offset mismatch",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/OffsetMismatchProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "Chunk too large",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/albums/{album_id}/ops": {
+ "post": {
+ "summary": "Apply one signed lifecycle manifest to an album's asset.",
+ "description": "The whole battery runs before anything is written, and a rejection writes nothing —\nincluding the blobs the bundle carries, which are stored only after the manifest has passed\nevery check the server can make without a key.",
+ "operationId": "album_lifecycle_op",
+ "parameters": [
+ {
+ "name": "album_id",
+ "in": "path",
+ "description": "The album's identifier.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/OpRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/OpResponse"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Upgrade required",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProtocolRangeProblem"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Stale revival",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/StaleRevivalProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/storage/verify": {
+ "post": {
+ "summary": "Confirm that the server holds the copies a client is about to stop holding.",
+ "description": "A pure read: it writes no blob, no index row and no verdict. Soundness against a racing\ncollection comes from the standing GC grace window rather than from a per-request lease,\nwhich is why nothing here takes one.",
+ "operationId": "verify_storage",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StorageVerifyRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StorageVerifyResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/shares": {
+ "post": {
+ "summary": "Register a share link the caller's client has issued.",
+ "operationId": "issue_share",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/IssueShareRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "201": {
+ "description": "The share link is registered and servable",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/IssueShareResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/shares/{opaque_id}": {
+ "delete": {
+ "summary": "Revoke one of the caller's links.",
+ "description": "Idempotent from the caller's side and **indistinguishable**: a link that was never theirs, a\nlink that does not exist, and a link they already revoked are all `204`. Revocation is the\none operation where saying \"there was nothing to revoke\" would be a lookup.",
+ "operationId": "revoke_share",
+ "parameters": [
+ {
+ "name": "opaque_id",
+ "in": "path",
+ "description": "The opaque id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/drops/links": {
+ "post": {
+ "summary": "Provision an upload link.",
+ "operationId": "provision_link",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProvisionLinkRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "201": {
+ "description": "The upload link is provisioned and accepting drops",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProvisionLinkResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/drops/links/{opaque_id}": {
+ "delete": {
+ "summary": "Revoke one of the caller's links.",
+ "description": "Indistinguishable and idempotent, for the same reason a share revocation is: saying \"there\nwas nothing to revoke\" would be a lookup.",
+ "operationId": "revoke_link",
+ "parameters": [
+ {
+ "name": "opaque_id",
+ "in": "path",
+ "description": "The opaque id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/drops/{drop_id}/adopt": {
+ "post": {
+ "summary": "Adopt a pending drop into an album.",
+ "description": "Invariant 32. The manifest re-runs the create battery — a drop that skipped it would be the\none write on this server that entered an album unvalidated — and its `ciphertext_hash` must\nname a blob in **the caller's own inbox**, which is what stops an adoption from minting an\nasset over somebody else's bytes.\n\nThe row is **claimed, written, then settled**. Across two ports there is no transaction, and\nthe two failure directions are not equal: writing first and deleting after can duplicate a\nphoto, taking first and failing to write loses one. A claim leaves a crash visible in the\nowner's own inbox instead, marked `adopting`.",
+ "operationId": "adopt_drop",
+ "parameters": [
+ {
+ "name": "drop_id",
+ "in": "path",
+ "description": "The drop's identifier.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AdoptRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AdoptResponse"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/drops/{drop_id}": {
+ "delete": {
+ "summary": "Discard a pending drop.",
+ "description": "The bytes become unreferenced and the collector reclaims them; the link's cap is **not**\nrefunded, because the drop did happen — a guest deposited a file and the owner chose not to\nkeep it, which is not the same as a link slot never having been used.",
+ "operationId": "discard_drop",
+ "parameters": [
+ {
+ "name": "drop_id",
+ "in": "path",
+ "description": "The drop's identifier.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "426": {
+ "description": "Protocol version unsupported",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/auth/devices": {
+ "get": {
+ "summary": "List the caller's live sessions and the cohorts they group under.",
+ "description": "Scoped by credential with no path parameter, for the same reason the escrow is: the only\naccount entitled to a session ledger is its own, and making that structural beats enforcing\nit.",
+ "operationId": "list_devices",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DevicesResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/auth/devices/directory/{user_id}": {
+ "get": {
+ "summary": "Fetch a user's signed device directory, verbatim.",
+ "description": "The response body is the exact bytes the owner signed. Re-encoding them would detach the\ndocument from its signature, and the failure would look like the *publisher's* bug.",
+ "operationId": "fetch_device_directory",
+ "parameters": [
+ {
+ "name": "user_id",
+ "in": "path",
+ "description": "The account id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/cbor": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/quota": {
+ "get": {
+ "summary": "Report the authenticated uploader's storage-quota snapshot.",
+ "description": "Scoped to the caller, and to nobody else: quota is accounted to the *uploader*, and one\naccount's storage use is not another's business.",
+ "operationId": "get_quota",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/QuotaResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/moderation/record": {
+ "get": {
+ "summary": "Serve the caller's own moderation record.",
+ "operationId": "moderation_record",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ModerationRecordResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/upload/sessions": {
+ "get": {
+ "summary": "Every upload the caller can resume.",
+ "description": "Oldest first, which is the order the store promises and the order a client wants: the oldest\nin-flight session is the one closest to eviction.",
+ "operationId": "list_upload_sessions",
+ "parameters": [
+ {
+ "name": "status",
+ "in": "query",
+ "description": "Return only sessions in this state.\n\nOne of `pending`, `uploading`, `waiting_for_processing`, `completed`,\n`failed_processing` — the same tokens the `X-Capsule-Upload-Status` header carries, so a\nclient filters on the value it was already given rather than on a second vocabulary.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SessionsResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/upload/{id}/receipt": {
+ "get": {
+ "summary": "Fetch the custody receipt for a finalized upload.",
+ "operationId": "get_upload_receipt",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "The session's identifier, as `POST /v1/upload` returned it.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/cbor": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Receipt not available",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/sync": {
+ "get": {
+ "summary": "Returns the changes in the caller's library after `cursor`.",
+ "description": "Read-only and idempotent: two calls with the same cursor return the same page, because the\ncursor names a position rather than consuming one. That is what makes a lost response\nharmless and a retry free.",
+ "operationId": "sync_feed",
+ "parameters": [
+ {
+ "name": "cursor",
+ "in": "query",
+ "description": "The opaque cursor a previous page returned. Absent means \"from the beginning\".",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ {
+ "name": "page_size",
+ "in": "query",
+ "description": "How many entries to return. Clamped into the range this server serves.\n\n`u32` and not `usize`: Kynos refuses to describe a platform-width integer, and it is\nright to — a schema whose bounds depend on the server's pointer size is a schema no\nclient can rely on.",
+ "required": false,
+ "schema": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "maximum": 4294967295.0,
+ "minimum": 0.0,
+ "format": "uint32"
+ }
+ },
+ {
+ "name": "album_id",
+ "in": "query",
+ "description": "One album's page rather than the caller's own feed (`S-C51`).\n\nFor the album's owner or any account on its current roster. Positions are the owner's\nsequence numbers filtered to the album, and the cursor is bound to `(caller, album)`, so\nit cannot be presented on the caller's own feed or on another album. Absent: the caller's\nown library, as before.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SyncPageResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/blob/{hash}": {
+ "get": {
+ "summary": "Fetch a ciphertext blob by its content address, ranged.",
+ "description": "Opaque octets: the server holds no key and this route never learns what it is serving. An\naccount fetches the blobs of its own assets and of the albums it is currently a member of; a\nformer member is told `403`, and everyone else is told what an unknown address is told —\nsee [`crate::serve`] for the boundary and its reasons.\n\nThe one answer that *is* account-scoped is the transient `409`: it reports the caller's own\nin-flight upload and nobody else's (`S-C40`).",
+ "operationId": "get_blob",
+ "parameters": [
+ {
+ "name": "hash",
+ "in": "path",
+ "description": "The blob's ciphertext content address, lowercase hex.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "Range",
+ "in": "header",
+ "description": "The part of the representation to transfer, per RFC 9110 section 14.2. A field this operation cannot apply is ignored and the whole representation is sent.",
+ "schema": {
+ "type": "string",
+ "pattern": "^bytes=(?:\\d+-\\d*|-\\d+)(?:\\s*,\\s*(?:\\d+-\\d*|-\\d+)){0,7}$"
+ },
+ "example": "bytes=0-1023"
+ },
+ {
+ "name": "If-Range",
+ "in": "header",
+ "description": "The entity tag the client's partial copy came from, per RFC 9110 section 13.1.5. The `Range` is honoured only if it matches this representation under the strong comparison; otherwise the whole representation is sent.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "If-None-Match",
+ "in": "header",
+ "description": "The entity tag the client already holds, per RFC 9110 section 13.1.2",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "If-Modified-Since",
+ "in": "header",
+ "description": "The date the client's copy carries, per RFC 9110 section 13.1.3",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "the whole representation",
+ "headers": {
+ "Accept-Ranges": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "ETag": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Last-Modified": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "206": {
+ "description": "the part the request asked for",
+ "headers": {
+ "Accept-Ranges": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "ETag": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Last-Modified": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Content-Range": {
+ "description": "The part of the representation enclosed, and its complete length, per RFC 9110 section 14.4.",
+ "required": true,
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string",
+ "pattern": "^bytes \\d+-\\d+/\\d+$"
+ }
+ }
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "304": {
+ "description": "the client's copy is current",
+ "headers": {
+ "ETag": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Last-Modified": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Upload in progress",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "410": {
+ "description": "Gone",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/assets/{asset_id}/receipts": {
+ "get": {
+ "summary": "Fetch every custody receipt covering one asset.",
+ "operationId": "get_asset_receipts",
+ "parameters": [
+ {
+ "name": "asset_id",
+ "in": "path",
+ "description": "The asset id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AssetReceiptsResponse"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/drops": {
+ "get": {
+ "summary": "The caller's pending drops.",
+ "operationId": "list_inbox",
+ "parameters": [
+ {
+ "name": "X-Capsule-Protocol",
+ "in": "header",
+ "description": "The `YYYY-MM-DD` protocol version this request is written against. Outside the server's `[X-Capsule-Protocol-Min, X-Capsule-Protocol-Max]` window the request is refused with `426`.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ {
+ "name": "X-Capsule-Crypto-Suite",
+ "in": "header",
+ "description": "The crypto suite id from the primitives inventory. Sent on writes; a suite this server does not implement is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ },
+ {
+ "name": "X-Capsule-Sidecar-Schema",
+ "in": "header",
+ "description": "The sidecar schema version declared at `sidecar_schema` field 0. Sent on metadata updates; a schema newer than this server indexes is refused with `400`.",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0
+ }
+ }
+ ],
+ "responses": {
+ "401": {
+ "description": "Unauthorized",
+ "headers": {
+ "WWW-Authenticate": {
+ "description": "The challenge the client must answer, per RFC 9110 section 11.6.1.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "example": "Bearer"
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Forbidden",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/InboxResponse"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Malformed handshake",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
+ }
+ },
+ "/v1/version": {
+ "get": {
+ "summary": "Reports the server's name and version.",
+ "description": "Unauthenticated and side-effect free. Clients use it as a reachability probe before\nattempting a protocol handshake, so it must stay cheap and must never fail for a reason\nthe caller could act on — there is no failure variant, and the return type says so.",
+ "operationId": "get_version",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/VersionResponse"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/.well-known/capsule/attestation-keys": {
+ "get": {
+ "summary": "Serve this server's storage-attestation keys and their append-only history.",
+ "description": "Cacheable and unauthenticated. It changes only when a key rotates, and a client that pinned\na stale copy still resolves every receipt signed before it fetched — which is the property\nthe append-only ordering buys.",
+ "operationId": "attestation_keys",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AttestationKeysResponse"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/.well-known/capsule/server-info": {
+ "get": {
+ "summary": "Serve this server's public, server-scoped facts.",
+ "description": "Unauthenticated by contract: a client deciding whether it can talk to this server at all has\nno credential yet, and a peer resolving the key that verifies a capability token must not\nneed one from the server whose claims it is checking.",
+ "operationId": "server_info",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ServerInfoResponse"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/.well-known/capsule/deprecation": {
+ "get": {
+ "summary": "Serve the announced deprecation cutoffs.",
+ "description": "The same announcements `server-info` carries, at their own path because that is the URL the\n`Warning:` header on a below-cutoff response points a human at, and because a client polling\nfor a cutoff should not have to refetch the whole discovery record to find one.",
+ "operationId": "deprecation_announcements",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeprecationsResponse"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/.well-known/capsule/revoked-jti": {
+ "get": {
+ "summary": "Serve the federation capability revocation list.",
+ "description": "Bounded by at most 24 hours of revocations, because an entry past the token's own `exp` is\npruned and a capability token cannot be minted to live longer than that. Public: a peer\nchecking whether a token it holds is still good is, by construction, not yet authenticated\nhere, and the record names no user — only opaque `jti`s.\n\n# Errors\n\nReturns `503` if the revocation list cannot be read. Deliberately *not* an empty list: an\nempty list is the strongest possible claim this endpoint can make — nothing is revoked — and\nserving it on a storage failure would turn an outage into a silent un-revocation of every\ntoken, which is exactly what the peer-side fail-closed rule exists to prevent.",
+ "operationId": "revoked_jti",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RevokedJtiResponse"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "Revocation list unavailable",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/s/{opaque_id}": {
+ "get": {
+ "summary": "What a viewer needs to begin, for a live link.",
+ "operationId": "share_metadata",
+ "parameters": [
+ {
+ "name": "opaque_id",
+ "in": "path",
+ "description": "The opaque id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SharedMetadataResponse"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too many requests",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/s/{opaque_id}/wrapped-secret": {
+ "get": {
+ "summary": "The passphrase-wrapped scope material, when there is one.",
+ "description": "A link with no passphrase answers `404` rather than `204` or an empty body: whether a link is\npassphrase-protected is already disclosed by the metadata record, and a *second* way to ask\nthe same question with a different shape is a second thing to keep consistent.",
+ "operationId": "share_wrapped_secret",
+ "parameters": [
+ {
+ "name": "opaque_id",
+ "in": "path",
+ "description": "The opaque id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "OK",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too many requests",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/s/{opaque_id}/blob/{hash}": {
+ "get": {
+ "summary": "Ciphertext for one of the link's blobs, ranged.",
+ "description": "The membership check is the security property: a link serves the addresses its record\nenumerates and nothing else, so it cannot be walked sideways into the album's unstripped\nmetadata. A blob the link does not name is the same `404` as a link that does not exist.",
+ "operationId": "share_blob",
+ "parameters": [
+ {
+ "name": "opaque_id",
+ "in": "path",
+ "description": "The opaque id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "hash",
+ "in": "path",
+ "description": "The blob's content address.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "Range",
+ "in": "header",
+ "description": "The part of the representation to transfer, per RFC 9110 section 14.2. A field this operation cannot apply is ignored and the whole representation is sent.",
+ "schema": {
+ "type": "string",
+ "pattern": "^bytes=(?:\\d+-\\d*|-\\d+)(?:\\s*,\\s*(?:\\d+-\\d*|-\\d+)){0,7}$"
+ },
+ "example": "bytes=0-1023"
+ },
+ {
+ "name": "If-Range",
+ "in": "header",
+ "description": "The entity tag the client's partial copy came from, per RFC 9110 section 13.1.5. The `Range` is honoured only if it matches this representation under the strong comparison; otherwise the whole representation is sent.",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "If-None-Match",
+ "in": "header",
+ "description": "The entity tag the client already holds, per RFC 9110 section 13.1.2",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "If-Modified-Since",
+ "in": "header",
+ "description": "The date the client's copy carries, per RFC 9110 section 13.1.3",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "200": {
+ "description": "the whole representation",
+ "headers": {
+ "Accept-Ranges": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "ETag": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Last-Modified": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "206": {
+ "description": "the part the request asked for",
+ "headers": {
+ "Accept-Ranges": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "ETag": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Last-Modified": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Content-Range": {
+ "description": "The part of the representation enclosed, and its complete length, per RFC 9110 section 14.4.",
+ "required": true,
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string",
+ "pattern": "^bytes \\d+-\\d+/\\d+$"
+ }
+ }
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "304": {
+ "description": "the client's copy is current",
+ "headers": {
+ "ETag": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "Last-Modified": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too many requests",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/d/{opaque_id}": {
+ "post": {
+ "summary": "Open a drop session through a link.",
+ "description": "Invariants 26–30 in order: the link admits the file and reserves its caps in one store\noperation, then the owner's quota is charged, then the declaration is checked.",
+ "operationId": "create_drop",
+ "parameters": [
+ {
+ "name": "opaque_id",
+ "in": "path",
+ "description": "The opaque id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateDropRequest"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported Media Type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Unprocessable Entity",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "201": {
+ "description": "A drop session is open and accepting chunks",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/CreateDropResponse"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "403": {
+ "description": "Passphrase required",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Link capacity exhausted",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "File too large",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/FileTooLargeProblem"
+ }
+ }
+ }
+ },
+ "429": {
+ "description": "Too many requests",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/d/{opaque_id}/{upload_id}": {
+ "patch": {
+ "summary": "Append one chunk to a drop session.",
+ "description": "The link is the credential: possession of the opaque id, plus a session that belongs to it.\nEverything after that is [`crate::upload::chunk::append`] — the album path's own function.",
+ "operationId": "append_drop_chunk",
+ "parameters": [
+ {
+ "name": "opaque_id",
+ "in": "path",
+ "description": "The opaque id of the link the session belongs to.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "upload_id",
+ "in": "path",
+ "description": "The session id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "X-Capsule-Offset",
+ "in": "header",
+ "description": "Where in the blob this chunk starts.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ {
+ "name": "X-Capsule-Checksum",
+ "in": "header",
+ "description": "The chunk's SHA-256, bare lowercase hex. Required: the idempotency tuple is undefined\nwithout it.",
+ "required": false,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "400": {
+ "description": "Bad Request",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "415": {
+ "description": "Unsupported media type",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "204": {
+ "description": "the request succeeded and there is no content to send",
+ "headers": {
+ "X-Capsule-Offset": {
+ "description": "Where the session is now.",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64"
+ }
+ },
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Not found",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "409": {
+ "description": "Chunk refused",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ },
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "413": {
+ "description": "the request body exceeds the configured limit",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "RegisterRequest": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "The address the account is identified by."
+ },
+ "password": {
+ "type": "string",
+ "description": "The password that will authenticate this account's **sessions**.\n\nNever the master key's input: the master key does not derive from it and is never visible\nto the credential verifier. Hashed by the registry adapter and never retained, logged, or\nechoed."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email",
+ "password"
+ ],
+ "description": "The `POST /v1/auth/register` body.\n\nDeliberately the *smallest* thing that can create an account: an address and a password. No\ndisplay name, no profile, no invitation code — every one of those would be a field the server\nstores about a person, and this server's whole posture is that it stores as little as it can."
+ },
+ "Problem": {
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "status": {
+ "type": "integer",
+ "maximum": 65535.0,
+ "minimum": 0.0,
+ "format": "uint16"
+ },
+ "detail": {
+ "type": "string"
+ },
+ "instance": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": true,
+ "type": "object",
+ "required": [
+ "type",
+ "status"
+ ],
+ "title": "Problem Details",
+ "description": "An RFC 9457 problem detail."
+ },
+ "TokenResponse": {
+ "properties": {
+ "access_token": {
"type": "string",
- "description": "The server package version."
+ "description": "The short-lived credential for ordinary requests."
+ },
+ "refresh_token": {
+ "type": "string",
+ "description": "The long-lived credential that buys new pairs from `POST /v1/auth/refresh`."
+ },
+ "token_type": {
+ "type": "string",
+ "description": "Always `Bearer`."
+ },
+ "expires_by": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64",
+ "description": "The **absolute** Unix-seconds instant `access_token` stops being honoured.\n\nAbsolute rather than a duration, which is what the field has always carried despite its\nname; the SDK depends on it."
}
},
"type": "object",
"required": [
- "name",
- "version"
+ "access_token",
+ "refresh_token",
+ "token_type",
+ "expires_by"
],
- "description": "Identifies the running server.\n\nDeliberately incurious: a name and a version, no build host, no commit, no uptime, no\nfeature list. This endpoint is unauthenticated, so everything it returns is public, and a\nkey-free server has no reason to hand an anonymous caller a fingerprint of its deployment.\nExact client build identification runs the other way (`S-D15`) — clients tell the server\nwhat they are, not the reverse."
+ "description": "A freshly issued token pair.\n\nThe field names and `expires_by`'s meaning are a live client contract — `capsule-sdk`'s\n`TokenResponseBody` reads exactly these — so they are preserved verbatim from the Salvo\nsurface. `Debug` is hand-written; both tokens are bearer credentials.\n\n`Deserialize` is derived so the suite reads the pair back through the same type the server\nwrote — a test that pulled `access_token` out of a `serde_json::Value` would still pass if\nthe field were renamed on the way out."
+ },
+ "LoginRequest": {
+ "properties": {
+ "email": {
+ "type": "string",
+ "description": "The account's email address."
+ },
+ "password": {
+ "type": "string",
+ "description": "The account's password.\n\nVerified by the account directory and never retained, logged, or echoed."
+ },
+ "cohort_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "An advisory device-cohort hash grouping one physical device's re-enrollments\n(slice `S-C13`).\n\nLegibility metadata only: no authorization path reads it, and an unusable value is\ndropped rather than refused — a sign-in must not fail over a field that gates nothing."
+ },
+ "device_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The directory device the client claims to be (slice `S-N3`), as a UUID.\n\nClient-asserted and unverified. Dropped, not refused, when it is not a usable UUID, for\nthe same reason as `cohort_hash`."
+ }
+ },
+ "type": "object",
+ "required": [
+ "email",
+ "password"
+ ],
+ "description": "Credentials, plus the two advisory identifiers a client may volunteer.\n\n`Debug` is hand-written. A derived one would print the password into any log line, panic\nmessage or `tracing` field that formatted the request — which is the single worst thing this\nfile could do, and is one `#[derive(Debug)]` away at all times.\nNo `#[schema(min_length = ...)]` on either credential, deliberately. Kynos 0.1.0 publishes a\nstring constraint into the document but does not enforce it on the request path — an empty\npassword reaches the handler — so declaring one would put a promise in the contract that the\nserver does not keep, which is the exact class of drift this rebuild exists to remove. Length\nis a body-size concern and belongs to a limits middleware; it is recorded as owed rather than\nasserted here."
+ },
+ "SecondFactorChallenge": {
+ "properties": {
+ "mfa_token": {
+ "type": "string",
+ "description": "The token to present alongside the code."
+ },
+ "expires_by": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64",
+ "description": "The **absolute** Unix-seconds instant the challenge stops being honoured.\n\nAbsolute rather than a duration, matching `TokenResponse::expires_by`, so a client has\none convention rather than two."
+ }
+ },
+ "type": "object",
+ "required": [
+ "mfa_token",
+ "expires_by"
+ ],
+ "description": "A half-finished sign-in.\n\n`Debug` is hand-written: the token is a credential, even though it authenticates nothing on\nits own."
+ },
+ "RefreshRequest": {
+ "properties": {
+ "refresh_token": {
+ "type": "string",
+ "description": "The refresh token issued by a previous login or refresh.\n\nUnconstrained in the schema for the reason [`LoginRequest`] records: an empty one is a\ntoken that does not verify, which is a 401 the handler already answers correctly."
+ }
+ },
+ "type": "object",
+ "required": [
+ "refresh_token"
+ ],
+ "description": "The refresh token being exchanged for a new pair.\n\n`Debug` is hand-written, for the same reason as [`LoginRequest`]: this field is a live\ncredential."
+ },
+ "RevokeChallengeResponse": {
+ "properties": {
+ "challenge": {
+ "type": "string",
+ "description": "The single-use token. Burned on the first attempt, successful or not."
+ },
+ "expires_at": {
+ "type": "string",
+ "description": "When it stops being redeemable, RFC 3339."
+ }
+ },
+ "type": "object",
+ "required": [
+ "challenge",
+ "expires_at"
+ ],
+ "description": "The challenge a global sign-out is signed over."
+ },
+ "RevokeAllRequest": {
+ "properties": {
+ "challenge": {
+ "type": "string",
+ "description": "The challenge that was issued."
+ },
+ "proof": {
+ "type": "string",
+ "description": "The account identity key's hybrid signature over\n[`revoke_all_signing_bytes`](capsule_core::crypto::revoke::revoke_all_signing_bytes),\ncanonical CBOR, base64."
+ }
+ },
+ "type": "object",
+ "required": [
+ "challenge",
+ "proof"
+ ],
+ "description": "A master-key proof over an issued challenge."
+ },
+ "RevokeAllResponse": {
+ "properties": {
+ "revoked": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64",
+ "description": "How many sessions were closed — the caller's own among them.\n\nCounted from the records the store actually removed, never from a separately maintained\nindex. The Salvo implementation read a per-user set that `revoke_session` did not clean\nup, so this number inflated by one for every prior refresh; `S-C29` made the record and\nits listing entry one fact, so there is nothing left to disagree."
+ }
+ },
+ "type": "object",
+ "required": [
+ "revoked"
+ ],
+ "description": "What a global sign-out closed."
+ },
+ "ReauthenticateRequest": {
+ "properties": {
+ "password": {
+ "type": "string",
+ "description": "The account's password."
+ }
+ },
+ "type": "object",
+ "required": [
+ "password"
+ ],
+ "description": "A password, re-presented on a session that already exists."
+ },
+ "ReauthenticateResponse": {
+ "properties": {
+ "authenticated_at": {
+ "type": "string",
+ "description": "The moment the credential was accepted, RFC 3339.\n\nReturned so a client can decide locally whether a gated operation will be admitted,\nrather than discovering it from a `403` in the middle of a ceremony."
+ }
+ },
+ "type": "object",
+ "required": [
+ "authenticated_at"
+ ],
+ "description": "When the re-authenticated session's freshness window last opened."
+ },
+ "PublishDirectoryResponse": {
+ "properties": {
+ "directory_version": {
+ "type": "integer",
+ "minimum": 0.0,
+ "format": "uint64",
+ "description": "The version now stored, which equals the submitted one."
+ }
+ },
+ "type": "object",
+ "required": [
+ "directory_version"
+ ],
+ "description": "The accepted version, echoed so a client knows what is now in force."
+ },
+ "StoreEscrowResponse": {
+ "properties": {
+ "stored_at": {
+ "type": "string",
+ "description": "When the server accepted it, RFC 3339.\n\nEchoed so a client can tell whether a cached copy is current — the stale-cache rule,\nwhich exists because a rotation from another device would otherwise manufacture false\nverification failures on this one."
+ },
+ "replaced": {
+ "type": "boolean",
+ "description": "Whether this displaced an earlier escrow.\n\nA rotation and a first escrow are different events for a client: one completes account\nsetup, and the other means the previous recovery secret has stopped working."
+ }
+ },
+ "type": "object",
+ "required": [
+ "stored_at",
+ "replaced"
+ ],
+ "description": "What storing an escrow did."
+ },
+ "UpdateProfileRequest": {
+ "properties": {
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The display name to set, clear (`null`), or leave alone (absent)."
+ }
+ },
+ "type": "object",
+ "description": "A partial edit of the caller's profile.\n\n`display_name` is a **doubly** optional field on the wire, and the two levels mean different\nthings: an absent key leaves the name alone, and an explicit `null` clears it. That is what\n`#[serde(default, deserialize_with = …)]` over an `Option