diff --git a/Cargo.lock b/Cargo.lock
index 575bda5e..b3ea930b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -761,7 +761,10 @@ dependencies = [
"jiff",
"jsonwebtoken",
"kynos",
+ "rcgen",
+ "reqwest",
"ring",
+ "rustls",
"secrecy",
"serde",
"serde_json",
@@ -769,6 +772,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.20",
"tokio",
+ "tokio-rustls",
"totp-rs",
"tracing",
"tracing-subscriber",
diff --git a/Cargo.toml b/Cargo.toml
index ffdbdaf4..6ed21c01 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -73,6 +73,14 @@ jiff = { version = "0.2", features = ["serde"] }
jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] }
nanoid = "0.4.0"
redis = { version = "1.2.2", features = ["tokio-comp", "connection-manager"] }
+# The HTTP client. `capsule-sdk` is the sanctioned client network path and `capsule-server`'s
+# OIDC relying party (slice `S-N1`) is the one server egress: discovery, JWKS and the token
+# exchange against an identity provider. rustls only, per the TLS row in design/dependencies.md;
+# `json` for the provider's documents. The SDK enables `stream` and `multipart` on top.
+reqwest = { version = "0.12.28", default-features = false, features = [
+ "json",
+ "rustls-tls",
+] }
ring = "0.17.14"
sea-orm = { version = "1.1.20" }
sea-orm-migration = { version = "1.1.20", features = [
diff --git a/SLICES.md b/SLICES.md
index ce7d3266..8e83616c 100644
--- a/SLICES.md
+++ b/SLICES.md
@@ -349,8 +349,8 @@ row's remainder now lives.
| S-I6 | Android ships raw ICU to users; the guard never fires | i18n | — | M | ACTIVE | done | `aapt2` unverified — owed-CI |
| S-I7 | The Rust runtime formatter cannot do ICU plurals | i18n | — | M | ACTIVE | done\* | refuses now; evaluating plurals still owed |
| S-I8 | clap `--help` text is unreachable from the catalogs | i18n | — | S | ACTIVE | ready | found widening `i18n-guard` |
-| S-N1 | OIDC relying party (server) | auth | — | L | RETIRED | ready | |
-| S-N2 | SDK/CLI OIDC login flows | auth | S-N1 | M | MIXED | blocked | |
+| S-N1 | OIDC relying party (server) | auth | — | L | RETIRED | done\* | in-process mock IdP stands in for the testcontainer one; durable adapters owed (#460) |
+| S-N2 | SDK/CLI OIDC login flows | auth | S-N1 | M | MIXED | part | SDK half landed with `S-N1`; CLI loopback listener + device grant are #461 |
| S-N3 | `device_id` on session listing + ceremony cohorts | auth | — | S | RETIRED | done | the wire half lands with `S-C13`; the TOTP ceremony with `S-C55`; passkeys retire on `S-C56` |
| S-P1 | `capsule_sdk` FFI workspace verbs | iOS path | S-A10 | L | MIXED | done | feed `manifest_cbor` shape → `S-C30` |
| S-P2 | Swift auth service + Keychain + login screen | iOS path | S-P1 | L | MIXED | ready | |
@@ -4887,6 +4887,22 @@ lands on Kynos rather than on Salvo.
green. **Tier:** Unit + Smoke. **Blocks:** S-N2.
- **Rebuild note:** unstarted, so there is nothing to re-scope — write it against Kynos
directly rather than adding routes to a server that is being replaced.
+- **Landed (issue #407):** `capsule-server::auth::oidc` — a pure ID-token validator
+ (`claims`), discovery with the issuer mix-up defence, a JWKS cache refetched on an
+ unknown `kid` and floored at one fetch a minute, the `IdentityProvider` port with its
+ HTTP adapter and a `Disabled` null object, `FederatedAccounts` keyed on
+ `(issuer, subject)` with no linking by address, and a typed `OidcAuthorizationStore`
+ ceremony port (single-use `state`, ten-minute TTL). `POST /v1/auth/oidc/authorize` and
+ `POST /v1/auth/oidc/callback` mount inside the protocol gate and mint sessions through
+ the password path's `open_session_for`, second factor included; `server-info` publishes
+ `auth.oidc` or `null`. **Two deviations, recorded:** the testcontainer IdP is an
+ in-process mock provider on loopback, because `test-rust` runs offline (dex in
+ `capsule-server/compose.yaml`, `--profile oidc`, is the manual run); and the Valkey
+ ceremony-store and Postgres federated-account adapters are owed (#460), so
+ `OIDC_ISSUER` under the durable backends is refused by name and the development
+ profile's federated accounts hold their own rows. Hand-written over `jsonwebtoken`
+ rather than `openidconnect` — see the OIDC row in
+ [Dependencies](capsule-docs/src/content/docs/design/dependencies.md).
### S-N2 — SDK/CLI OIDC login flows
@@ -4898,6 +4914,12 @@ lands on Kynos rather than on Salvo.
`cohort_hash` rides the ceremony. **Depends on:** S-N1 (**live block**).
- **Done when:** `capsule auth login --oidc` round-trips against the dev IdP;
mocked-HTTP tests per flow. **Tier:** Unit + Smoke.
+- **Part landed (issue #407):** `capsule_sdk::auth::AuthClient::begin_oidc_login` /
+ `complete_oidc_login` — the two server legs, answering the same `LoginOutcome` a
+ password login does, with the cohort riding the completing request and the
+ `error.auth.oidc_*` refusals typed on `AuthError`. **Remainder (#461):** the CLI's
+ loopback listener and `--oidc` arm, the browser-open policy the docs do not carry, and
+ the device authorization grant (RFC 8628) with its own ceremony store.
### S-N3 — `device_id` on session listing + ceremony cohorts
diff --git a/capsule-android/src/androidMain/res/values/strings.xml b/capsule-android/src/androidMain/res/values/strings.xml
index 4c874840..75c2c626 100644
--- a/capsule-android/src/androidMain/res/values/strings.xml
+++ b/capsule-android/src/androidMain/res/values/strings.xml
@@ -1827,6 +1827,14 @@
This account is locked after too many failed sign-in attempts.
That is not your current password.
Invalid email or password.
+ An account with that email address already exists here. Sign in with its password instead.
+ Too many sign-ins are already in progress. Please try again in a moment.
+ Your identity provider didn\'t accept that sign-in. Try again.
+ Single sign-on isn\'t set up on this server.
+ That sign-in can\'t return to this app.
+ That sign-in has expired. Start again.
+ Your identity provider\'s answer couldn\'t be verified.
+ Capsule couldn\'t reach your identity provider just now. Please try again.
That password cannot be used.
That display name cannot be used.
That account no longer exists.
@@ -1856,6 +1864,7 @@
That device list couldn\'t be read.
This device list is out of date. Capsule will refresh it before continuing.
That upload could not be added to the album.
+ This server is handling too many upload links right now. Please try again shortly.
This upload link is full.
This upload link is full. Ask for a new one.
That part of the upload could not be accepted. It will be retried.
@@ -1867,6 +1876,7 @@
This upload link needs its passphrase.
Too many attempts. Please wait and try again.
Capsule couldn\'t reach the upload service. Please try again.
+ This server is handling too many enrollment attempts right now. Please try again shortly.
This device-add session has ended. Start again.
That device code didn\'t work. Generate a new one and try again.
Confirm it\'s you on this device to add another device.
@@ -1902,6 +1912,7 @@
Please sign in again.
Some of that request didn\'t make sense.
Capsule couldn\'t read that content type.
+ This server is handling too many shared links right now. Please try again shortly.
That share link could not be created.
Too many attempts. Please wait and try again.
Capsule couldn\'t reach that share. Please 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-docs/src/content/docs/design/api-surfaces.md b/capsule-docs/src/content/docs/design/api-surfaces.md
index b3d664da..9a29b1fe 100644
--- a/capsule-docs/src/content/docs/design/api-surfaces.md
+++ b/capsule-docs/src/content/docs/design/api-surfaces.md
@@ -123,12 +123,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/authentication.md b/capsule-docs/src/content/docs/design/authentication.md
index 96f066aa..b741e545 100644
--- a/capsule-docs/src/content/docs/design/authentication.md
+++ b/capsule-docs/src/content/docs/design/authentication.md
@@ -79,6 +79,28 @@ Both paths mint the same Capsule [sessions](#session-and-access-tokens) and bind
A deployment may enable either or both. Neither path weakens the cryptographic binding: the IdP (or password) authenticates the *session*; the master key never derives from, and is never visible to, the credential verifier.
+### Signing In Through an Identity Provider
+
+Slice `S-N1` (the server) and the SDK half of `S-N2` (`capsule-sdk`'s `begin_oidc_login` / `complete_oidc_login`). Authorization code + PKCE, and nothing else: no implicit flow, no hybrid flow, and — until the CLI's loopback listener and the device grant land (issue #461) — no device authorization grant.
+
+- **Two requests, one ceremony.** `POST /v1/auth/oidc/authorize` takes the client's own `redirect_uri` and answers the provider's authorization URL, a `state`, and the ceremony's deadline (ten minutes). The client sends the person there and receives the provider's redirect itself — a web app's callback route, a CLI's loopback listener, `ASWebAuthenticationSession` on iOS. `POST /v1/auth/oidc/callback` takes the redirect's `state` and `code` and answers exactly what `POST /v1/auth/login` answers: a token pair, or a `202` second-factor challenge. The session is opened by the same code the password path uses, so a federated sign-in is in every respect the same session.
+- **The redirect URI is client-supplied and allow-listed.** Admitted if it equals `OIDC_REDIRECT_URL` exactly, or — when `OIDC_ALLOW_LOOPBACK_REDIRECT` is on, which it is **not** by default — is an `http` URI whose host is the loopback IP literal `127.0.0.1` or `[::1]` on **any** port (RFC 8252 §7.3; `localhost` is deliberately not admitted, per §8.3). The loopback arm is opt-in because it is the one knob that widens where the server will send a person back to; a deployment with a CLI or desktop client turns it on, and the CLI flow (issue #461) tells the operator so. The admitted value is stored with the ceremony and replayed byte for byte to the token endpoint, as RFC 6749 §4.1.3 requires. This one field is what lets a native client complete the flow without a second server surface. A refused URI is `400 error.auth.oidc_redirect_invalid`.
+- **Beginning a ceremony is bounded twice**, because it is an unauthenticated write into a store: sixty a minute per redirect host (`429 error.auth.rate_limited`), and the pending-ceremony store's own capacity — ten thousand in memory, expired records purged on every write — answered as `503 error.auth.oidc_at_capacity` when reached. That code is its own, not the `500`'s `error.auth.unavailable`: [the API surfaces contract](/design/api-surfaces/) has clients switch on the code and never on status alone, so "the server said not now, retry in a moment" and "a store could not answer at all" may not share one.
+- **The rate-limit key is picked after the redirect is validated, not before.** The redirect URI is caller-supplied, so keying the limiter on its host before the policy has admitted it would let an unauthenticated caller add one row per request to the counter store — refused every time, and counted forever. An admitted redirect is keyed on its host, at most three; every refusal shares one deployment-wide bucket on its own budget, so refusals stay throttled without being able to mint keys. The counter store purges lapsed windows and holds a ceiling besides, **one per key kind rather than one for everything**, because several other limiters on the surface key on something a caller sent and must do so *before* they resolve it — throttling only real ids would make the limiter a free existence oracle. A single shared ceiling would have let a flood against the cheapest of those surfaces deny a first-time key to all the others, single sign-on included; partitioned, it denies only its own. The enrollment redemption additionally shape-checks the presented code before charging, on the same reasoning as the redirect here: a shape check is not an existence check, so it bounds the key without reopening the oracle.
+- **The `state` is burned on the first callback, successful or not.** The nonce, the PKCE verifier and the redirect URI live in a single-use ceremony store between the two legs; a replayed `state` — and therefore a stolen code arriving on it — finds nothing. Unknown, spent and expired are one answer, `401 error.auth.oidc_state_invalid`, so the callback is not an oracle.
+- **Every ID-token refusal is one code on the wire.** The relying party checks the header algorithm (RS256, ES256 or EdDSA; never `none`, never HMAC), the signature against the provider's published keys, `iss` for exact string equality with `OIDC_ISSUER`, `aud` containing the client id, `azp` when present, `exp`/`nbf`/`iat` with a sixty-second skew, the `nonce` against the one this ceremony issued, and a bounded `sub`. Which check failed reaches the server log; the wire says `401 error.auth.oidc_token_invalid` for all of them. A provider that refuses the exchange is `401 error.auth.oidc_exchange_failed`; a provider that cannot be reached is `500 error.auth.oidc_unavailable`, distinct from `error.auth.unavailable` because "your identity provider is down" and "our session store is down" are different operator actions.
+- **Discovery is lazy, and a provider that names another issuer is refused.** The provider's metadata is fetched on first use and cached for a day; nothing is resolved at boot, so an identity provider that is down does not stop a server from serving local auth. A discovery document whose `issuer` is not the configured one is refused (the mix-up defence), and every endpoint must be `https` unless the issuer itself is a loopback IP literal — the development carve-out — under which every plain-HTTP endpoint must itself be loopback, so a provider on this machine cannot send the code off-box in the clear. Signing keys are refetched on an unknown `kid`, at most once a minute, so a stream of forged key ids cannot make the server hammer the provider — and re-read after an hour regardless, because a key the provider *revoked* never produces that evidence; the ceiling is what stops it being honoured. A provider behind a private CA is reached with `OIDC_CA_BUNDLE`, a PEM bundle of additional trust anchors read at boot.
+- **Accounts are keyed on `(issuer, subject)`, and never linked by address.** The first sign-in for an unknown pair creates a password-less account. An IdP-asserted `email` that already belongs to an account is `409 error.auth.oidc_address_taken`, never a link: the address is a claim the provider controls, and honouring it as a link key would hand the matching account to anyone who can set an email at the provider — the same class of takeover the [profile surface](#the-profile-surface) refuses when it fixes the login address. The disclosure the `409` makes is the one registration already makes. **Only a verified address counts**, both ways: an address the provider asserts without `email_verified` reserves nothing and collides with nothing, or a person could register somebody else's address at the provider, unverified, and hold its owner out. Deliberately linking an existing account to a provider identity is a separate, authenticated ceremony, out of scope.
+- **Deviation, named rather than substituted (issue #460).** Two of the properties above are owed rather than shipped, because the account port has no nullable credential yet and the federated rows do not share the password directory's table: the `409` is checked against *federated* accounts' verified addresses, not yet against password accounts' — and a password-less OIDC account is one no password row exists for, not yet one whose null credential `authenticate` refuses structurally. The test fixture's double encodes the intended contract; the Postgres adapter delivers it.
+- **The OIDC door does not consult the password lockout.** The lockout counts failed *credential presentations* against the local directory, and a federated sign-in presents none — the provider already authenticated the person. Refusing single sign-on on a locked local account would let anyone who can guess passwords at `POST /v1/auth/login` lock a person out of the other door too.
+- **The second factor is honoured, not bypassed.** A confirmed TOTP enrollment turns the callback into the same `202` challenge the password path issues, completed at `POST /v1/auth/login/verify-totp` with the advisory `cohort_hash` and `device_id` riding that completing request. Bypassing it would let an account that enrolled a factor be signed into without one through a second door.
+- **Scopes are `openid email`, with no knob.** The address is the one claim the relying party reads, for the one decision it makes with it. `profile` is not requested: the display name is something the person sets, and asking the provider for it would have the server store a fact it declined to collect at registration.
+- **`server-info` publishes `auth.oidc: { authorize, callback }`, or `null`.** Endpoints only — never the issuer, never the client id, never anything user-scoped. The presence of the record is how a login chooser decides whether to offer the path; without `OIDC_ISSUER` the authorize answers `404 error.auth.oidc_not_configured`.
+
+**Configuration** is six variables, read with the rest in `capsule-server/src/config.rs`: `OIDC_ISSUER` (absent means the path is off; `https`, or `http` on a loopback IP literal for development), `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET` (optional — absent is a public client, PKCE-only, which is what RFC 8252 §8.5 requires of a native app), `OIDC_REDIRECT_URL` (optional; held to the issuer's scheme rule), `OIDC_ALLOW_LOOPBACK_REDIRECT` (default off) and `OIDC_CA_BUNDLE` (optional; a PEM path, read at boot and refused by name if unusable). Half a relying party — an issuer with no client id, or the reverse — is a startup fault. Under the durable backends `OIDC_ISSUER` is refused by name until the Valkey ceremony store and the Postgres federated-account adapter land (issue #460); the development profile runs it on the in-memory adapters, and `capsule-server/compose.yaml` ships a dex service (`--profile oidc`) with a public `capsule` client to run it against.
+
+**Deviation from the validation plan, named rather than substituted.** [Validation](#validation) asks for a testcontainer IdP. The suite uses an in-process mock provider on loopback instead, because `mise run test-rust` runs offline and container-free; it speaks the identical wire — discovery JSON, a JWK Set, a form-encoded token `POST`, a signed compact JWS — and exercises key rotation, the refetch floor, a wrong PKCE verifier at the token endpoint, and every claim refusal. The dex service is the manual run against a real provider.
+
## Identity and Discovery
Patterns borrowed from Matrix 2.0, with one critical departure: **`.well-known/` never enumerates the user list**. A federated setting where a peer can list every user on a server is unacceptable — both from an abuse-surface perspective (spam, harassment-target discovery, account-enumeration attacks) and a privacy perspective.
diff --git a/capsule-docs/src/content/docs/design/dependencies.md b/capsule-docs/src/content/docs/design/dependencies.md
index 548962bc..ba1698bb 100644
--- a/capsule-docs/src/content/docs/design/dependencies.md
+++ b/capsule-docs/src/content/docs/design/dependencies.md
@@ -25,14 +25,15 @@ Mechanically, every Rust version is pinned once in the root `Cargo.toml` `[works
| Error handling | `thiserror` in libraries; `eyre` + `color-eyre` in binaries | Libraries define typed error enums; binaries (CLI, server `main`, xtask) wrap them in reports. | `anyhow` is not used. |
| Logging | `tracing` (facade) + `tracing-subscriber` (binaries) | All crates; structured fields and hot-path spans per the traceability rule in `AGENTS.md`. The `log` facade is forbidden in new code. | Remaining `log::` call sites in `capsule-core` / `capsule-core-ffi` migrate in slice S-F6. |
| TLS implementation | `rustls` (with `tokio-rustls` as the async adapter) | Wherever Capsule code holds a TLS stack: the SDK's HTTP client, [LAN-peering](/design/peering/) mutual TLS (`tokio-rustls`), server egress, sea-orm's `runtime-tokio-rustls`. The `ring` provider is pinned for the peering stack so it never depends on an ambiguous process-default `CryptoProvider`. Never native-tls/openssl. | **None.** The one exception this row used to carry — `openssl` as a transitive dependency of `webauthn-rs` attestation-certificate verification — goes with passkeys (`S-C56`) and with the `capsule-server` tree that holds them. |
-| X.509 leaf generation | `rcgen` | The per-connection self-signed leaf the [LAN-peering](/design/peering/) mTLS handshake presents. The certificate carries no trust of its own — peering is CA-less and identity is decided by the application-layer hybrid check — so the leaf is ephemeral. `ring` provider, matching the rustls pin above. | Server-facing certificates are operator-provisioned, not minted in-process. |
+| X.509 leaf generation | `rcgen` | The per-connection self-signed leaf the [LAN-peering](/design/peering/) mTLS handshake presents. The certificate carries no trust of its own — peering is CA-less and identity is decided by the application-layer hybrid check — so the leaf is ephemeral. `ring` provider, matching the rustls pin above. Also a **dev-dependency** of `capsule-server`, with `rustls` and `tokio-rustls` at the SDK's exact pins, for the one test that serves a mock identity provider behind a private CA (`OIDC_CA_BUNDLE`, slice `S-N1`). | Server-facing certificates are operator-provisioned, not minted in-process. |
| LAN service discovery | mocked seam (`capsule-sdk::peering::Discovery`) | Peering's mDNS advertisement/browse is behind a trait seam; the opaque, rotating descriptor is pure and unit-tested. A live responder (pure-Rust `mdns-sd`) is the sanctioned implementation to plug in — added by a follow-up slice with its own row, since a live multicast responder is non-deterministic and untestable in CI. | — |
| Identifiers | `uuid` — **UUIDv7 for every newly introduced identifier** | Time-ordered v7 is the default (index locality); the assignment of existing ids is owned by [Metadata — Identifiers](/design/metadata/#identifiers). | UUIDv4 where an id must not leak creation time (e.g. `device_id`). Capability-bearing opaque ids (share links, drops) are not UUIDs at all — they carry their own ≥128-bit entropy per their owner docs. |
| Async runtime | `tokio` | All async code. | — |
| HTTP server | Kynos | All `capsule-server` REST/OpenAPI surfaces, including sync and federation. | No secondary public transport. |
| Kynos sourcing | `kynos = { version = "0.1.0", features = ["openapi32"] }` — from **crates.io** | Kynos published 0.1.0 on 2026-08-29, which discharges the repin-on-publish exit this row previously carried: the git dependency and its pinned rev are gone, so a bump is an ordinary reviewed version change rather than a rev audit. Tokio-only, MSRV **1.85**, edition 2024. `openapi32` is a strict, purely additive superset of the default `openapi31`. **Enabling the feature does not by itself make the document 3.2**: `Router::openapi()` emits the *lowest* version expressing the API without loss, deliberately not keyed on the feature, because Cargo unifies features across a dependency graph and a document's version must not follow a flag an unrelated crate turned on. Capsule therefore pins the version explicitly with `openapi_as(SpecVersion::V3_2)` — the case Kynos names as *"a consumer's toolchain pins a version"*, and one that targets rather than downgrades, so an unexpressible construct is an error naming what blocks it, never a document with operations quietly missing. | Master has moved past the 0.1.0 tag (`512adbc7`) — the delta is docs/CI plus `Accept-Language` negotiation, which Capsule does not adopt (error codes are localized client-side, offline). Repin at 0.2 if a released feature is needed. |
| HTTP body | `http-body-util` | `capsule-server`'s coded-problem interceptor (`S-C36`) only: it reads a rendered RFC 9457 body back before putting it down again, and Kynos's `Body` is an `http_body::Body` with no inherent collector. Already in the lock file through Kynos and hyper, so it adds nothing to the tree. | Not a general HTTP abstraction: nothing else in Capsule touches a body outside a typed extractor, and a second use is a sign something is bypassing one. |
-| HTTP client | `reqwest` (`default-features = false`, `rustls-tls`) | `capsule-sdk` — the sanctioned network path. | — |
+| HTTP client | `reqwest` (`default-features = false`, `rustls-tls`, `json`) | `capsule-sdk` — the sanctioned client network path — and `capsule-server`'s OIDC relying party (slice `S-N1`), the one server egress: the discovery document, the JWK Set and the form-encoded token exchange against the configured identity provider, and nothing else. Pinned once in the workspace manifest; the SDK adds `stream` and `multipart`. The `rustls-tls` feature selects rustls's `ring` provider, the same one the SDK and the peering stack pin, so the two crates that hold a TLS stack agree. | A second server egress is a sign something is bypassing the relying party's adapter. |
+| OIDC relying party | `jsonwebtoken` (existing, `aws_lc_rs`) + hand-written discovery, JWKS cache and token exchange in `capsule-server::auth::oidc` | Slice `S-N1`. Signature verification against a JWK Set is the workspace's JWT crate (`JwkSet::find`, `DecodingKey::from_jwk`); the discovery fetch, the key cache with its unknown-`kid` refetch floor, the form `POST` and every claim check are Capsule's, because each of those is a security decision this repository wants legible ([Authentication — OIDC](/design/authentication/#signing-in-through-an-identity-provider)). No crate enters the lock file. | `openidconnect` 4.0.1 was priced and rejected: its manifest declares `chrono` (banned; the exception list above does not include it), the `log` facade (banned; S-F6 removes it), `rsa 0.9.2` carrying RUSTSEC-2023-0071 with no fixed release — which `deny.toml` would not catch, since only the licence check is wired — and duplicate majors of `base64` and `thiserror`. |
| REST client codegen | `spargen` **0.4.0** (in-house, OpenAPI 3.1.x **and 3.2.x**) | `capsule-sdk` **build-dependency only**: `build.rs` lowers the committed `capsule-sdk/openapi.json` — emitted deterministically from the Kynos server's own OpenAPI document — into the typed `rest::Client`, wrapped by `client::AuthenticatedClient` (slice `S-D8`). Its runtime support is embedded into the generated module, so spargen never enters the SDK's runtime tree. Since 0.3.0 it enforces **runtime dependency contracts**, which set the floors on `bytes`, `reqwest`, `serde` and `serde_json` in the root manifest — bump those together or generation fails. Its API changed in 0.3: `Config` split into `Spec`/`Build`, and `Report::outcome` is a method (a `Cached` outcome is a success, not a failure). | Progenitor is gone. **The two exclusions this row used to carry are lifted**: object-typed query params and binary bodies both lower correctly as of 0.2.2, so the byte-serving surface is generated rather than hand-written. What *stays* hand-written is orchestration, not parsing — the resumable upload state machine (`S-D1`), token refresh, sync, recovery and protocol-version negotiation. Four Salvo-emitted operations are narrowed with `spargen::omit!` because they are structurally invalid; see the gates table in `SLICES.md`. |
| Second factor | `totp-rs` (`otpauth`, `gen_secret`) | The RFC 6238 codes of the local auth path's second factor (slice `S-C55`), in `capsule-server`'s `auth::totp` alone. The parameters are Capsule's and are published as constants — SHA-1, six digits, a thirty-second step, one step of drift — because an authenticator app assumes all four and a deployment that changed one would issue provisioning URIs that silently mis-generate. What the crate does **not** own is replay: a code is accepted at most once, and that is a compare-and-set in the enrollment store, not an algorithm. | The crate's own `skew` is deliberately unused: Capsule walks the drift window itself because it needs to know *which* step matched, and `check` reports only that one did. |
| Constant-time comparison | `subtle` | The one place a secret-derived value is compared byte for byte: the second factor's code check (`S-C55`). A hand-rolled fold is what an optimizer is free to short-circuit, and the resulting code looks correct forever. Already in the tree under `aes-gcm`, so this promotes a transitive dependency rather than adding one. | Password and manifest comparisons do not use it — a password never rises above its adapter, and signature verification is the signature crate's own constant-time path. |
diff --git a/capsule-i18n/src/bundles/en.json b/capsule-i18n/src/bundles/en.json
index 318b9021..e9223171 100644
--- a/capsule-i18n/src/bundles/en.json
+++ b/capsule-i18n/src/bundles/en.json
@@ -1836,6 +1836,14 @@
"error.auth.account_locked": "This account is locked after too many failed sign-in attempts.",
"error.auth.current_password_invalid": "That is not your current password.",
"error.auth.invalid_credentials": "Invalid email or password.",
+ "error.auth.oidc_address_taken": "An account with that email address already exists here. Sign in with its password instead.",
+ "error.auth.oidc_at_capacity": "Too many sign-ins are already in progress. Please try again in a moment.",
+ "error.auth.oidc_exchange_failed": "Your identity provider didn't accept that sign-in. Try again.",
+ "error.auth.oidc_not_configured": "Single sign-on isn't set up on this server.",
+ "error.auth.oidc_redirect_invalid": "That sign-in can't return to this app.",
+ "error.auth.oidc_state_invalid": "That sign-in has expired. Start again.",
+ "error.auth.oidc_token_invalid": "Your identity provider's answer couldn't be verified.",
+ "error.auth.oidc_unavailable": "Capsule couldn't reach your identity provider just now. Please try again.",
"error.auth.password_invalid": "That password cannot be used.",
"error.auth.profile_invalid": "That display name cannot be used.",
"error.auth.profile_not_found": "That account no longer exists.",
@@ -1865,6 +1873,7 @@
"error.directory.unsupported_media_type": "That device list couldn't be read.",
"error.directory.version_conflict": "This device list is out of date. Capsule will refresh it before continuing.",
"error.drop.adoption_refused": "That upload could not be added to the album.",
+ "error.drop.at_capacity": "This server is handling too many upload links right now. Please try again shortly.",
"error.drop.cap_exceeded": "This upload link is full.",
"error.drop.cap_exhausted": "This upload link is full. Ask for a new one.",
"error.drop.chunk_refused": "That part of the upload could not be accepted. It will be retried.",
@@ -1876,6 +1885,7 @@
"error.drop.passphrase_required": "This upload link needs its passphrase.",
"error.drop.rate_limited": "Too many attempts. Please wait and try again.",
"error.drop.unavailable": "Capsule couldn't reach the upload service. Please try again.",
+ "error.enrollment.at_capacity": "This server is handling too many enrollment attempts right now. Please try again shortly.",
"error.enrollment.channel_not_found": "This device-add session has ended. Start again.",
"error.enrollment.code_refused": "That device code didn't work. Generate a new one and try again.",
"error.enrollment.local_auth_required": "Confirm it's you on this device to add another device.",
@@ -1911,6 +1921,7 @@
"error.request.unauthenticated": "Please sign in again.",
"error.request.unprocessable": "Some of that request didn't make sense.",
"error.request.unsupported_media_type": "Capsule couldn't read that content type.",
+ "error.share.at_capacity": "This server is handling too many shared links right now. Please try again shortly.",
"error.share.malformed": "That share link could not be created.",
"error.share.rate_limited": "Too many attempts. Please wait and try again.",
"error.share.unavailable": "Capsule couldn't reach that share. Please try again.",
diff --git a/capsule-i18n/src/generated.rs b/capsule-i18n/src/generated.rs
index a71cd1ac..1813048d 100644
--- a/capsule-i18n/src/generated.rs
+++ b/capsule-i18n/src/generated.rs
@@ -62,6 +62,30 @@ pub mod error_codes {
/// `error.auth.invalid_credentials`
pub const AUTH_INVALID_CREDENTIALS: &str = "error.auth.invalid_credentials";
+ /// `error.auth.oidc_address_taken`
+ pub const AUTH_OIDC_ADDRESS_TAKEN: &str = "error.auth.oidc_address_taken";
+
+ /// `error.auth.oidc_at_capacity`
+ pub const AUTH_OIDC_AT_CAPACITY: &str = "error.auth.oidc_at_capacity";
+
+ /// `error.auth.oidc_exchange_failed`
+ pub const AUTH_OIDC_EXCHANGE_FAILED: &str = "error.auth.oidc_exchange_failed";
+
+ /// `error.auth.oidc_not_configured`
+ pub const AUTH_OIDC_NOT_CONFIGURED: &str = "error.auth.oidc_not_configured";
+
+ /// `error.auth.oidc_redirect_invalid`
+ pub const AUTH_OIDC_REDIRECT_INVALID: &str = "error.auth.oidc_redirect_invalid";
+
+ /// `error.auth.oidc_state_invalid`
+ pub const AUTH_OIDC_STATE_INVALID: &str = "error.auth.oidc_state_invalid";
+
+ /// `error.auth.oidc_token_invalid`
+ pub const AUTH_OIDC_TOKEN_INVALID: &str = "error.auth.oidc_token_invalid";
+
+ /// `error.auth.oidc_unavailable`
+ pub const AUTH_OIDC_UNAVAILABLE: &str = "error.auth.oidc_unavailable";
+
/// `error.auth.password_invalid`
pub const AUTH_PASSWORD_INVALID: &str = "error.auth.password_invalid";
@@ -149,6 +173,9 @@ pub mod error_codes {
/// `error.drop.adoption_refused`
pub const DROP_ADOPTION_REFUSED: &str = "error.drop.adoption_refused";
+ /// `error.drop.at_capacity`
+ pub const DROP_AT_CAPACITY: &str = "error.drop.at_capacity";
+
/// `error.drop.cap_exceeded`
pub const DROP_CAP_EXCEEDED: &str = "error.drop.cap_exceeded";
@@ -182,6 +209,9 @@ pub mod error_codes {
/// `error.drop.unavailable`
pub const DROP_UNAVAILABLE: &str = "error.drop.unavailable";
+ /// `error.enrollment.at_capacity`
+ pub const ENROLLMENT_AT_CAPACITY: &str = "error.enrollment.at_capacity";
+
/// `error.enrollment.channel_not_found`
pub const ENROLLMENT_CHANNEL_NOT_FOUND: &str = "error.enrollment.channel_not_found";
@@ -287,6 +317,9 @@ pub mod error_codes {
/// `error.request.unsupported_media_type`
pub const REQUEST_UNSUPPORTED_MEDIA_TYPE: &str = "error.request.unsupported_media_type";
+ /// `error.share.at_capacity`
+ pub const SHARE_AT_CAPACITY: &str = "error.share.at_capacity";
+
/// `error.share.malformed`
pub const SHARE_MALFORMED: &str = "error.share.malformed";
diff --git a/capsule-sdk/src/albums.rs b/capsule-sdk/src/albums.rs
index 5e969203..4067a94d 100644
--- a/capsule-sdk/src/albums.rs
+++ b/capsule-sdk/src/albums.rs
@@ -99,6 +99,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,
diff --git a/capsule-sdk/src/auth.rs b/capsule-sdk/src/auth.rs
index 6c7db931..f8b8259c 100644
--- a/capsule-sdk/src/auth.rs
+++ b/capsule-sdk/src/auth.rs
@@ -24,6 +24,13 @@
//! ladder lands with `S-D10`, but the `401`-retry-once and pre-flight refresh here
//! are the parts the session store owns.
//!
+//! The OIDC legs (slice `S-N2`: [`AuthClient::begin_oidc_login`] and
+//! [`AuthClient::complete_oidc_login`]) are **not** hand-rolled: neither is token
+//! orchestration, so the exemption above does not cover them, and they call the generated
+//! [`rest::Client`] — every body and every response parsed by generated code, with only the
+//! mapping into [`LoginOutcome`] and [`AuthError`] written here. The browser leg between the
+//! two is the platform's: a loopback listener on the CLI, `ASWebAuthenticationSession` on iOS.
+//!
//! ## Testing
//!
//! The wire flows (login/refresh/logout, `401` recovery, error mapping) are proven
@@ -36,6 +43,7 @@
use std::sync::Arc;
+use capsule_core::crypto::primitives::PROTOCOL_VERSION;
use capsule_i18n::error_codes;
use jiff::Timestamp;
use secrecy::{ExposeSecret, SecretString};
@@ -43,6 +51,8 @@ use serde::{Deserialize, Serialize};
use tokio::sync::{Mutex, RwLock};
use tracing::instrument;
+use crate::rest;
+
/// Default pre-flight refresh window: refresh once the access token is within this
/// many seconds of expiry, so an in-flight request never races the boundary.
const DEFAULT_REFRESH_SKEW_SECS: i64 = 30;
@@ -112,6 +122,42 @@ pub enum AuthError {
/// [`LoginOutcome`], because a second factor is the system working rather than a failure.
#[error("this account requires a second factor; complete the sign-in with a code")]
SecondFactorRequired,
+
+ /// The server has no identity provider (`error.auth.oidc_not_configured`).
+ ///
+ /// A client that read `auth.oidc: null` from `server-info` never sees this; one that offered
+ /// the option anyway does.
+ #[error("single sign-on is not configured on this server")]
+ OidcNotConfigured,
+ /// The redirect URI this client asked for is not one the server admits
+ /// (`error.auth.oidc_redirect_invalid`). A client or deployment misconfiguration.
+ #[error("the server will not send a person back to this redirect URI")]
+ OidcRedirectInvalid,
+ /// The callback was refused: the ceremony expired or was replayed, the provider refused the
+ /// exchange, or the ID token failed a check. Every one means "start the sign-in again", so
+ /// they are one variant; `code` says which for a log line.
+ #[error("the sign-in through the identity provider was refused ({})", code.as_deref().unwrap_or("no code"))]
+ OidcRejected {
+ /// The server's `error.auth.oidc_*` code, when it sent one.
+ code: Option,
+ },
+ /// The identity provider asserted an address that already has a local account
+ /// (`error.auth.oidc_address_taken`). Never linked: the person signs in with that account's
+ /// password instead.
+ #[error("an account with that address already exists; sign in with its password")]
+ OidcAddressTaken,
+ /// The generated client could not reach the server, or could not build the request.
+ ///
+ /// The generated client classifies its transport failures itself (DNS, connection, TLS,
+ /// timeout, redirect policy) and they are not `reqwest::Error`s, so they cannot ride
+ /// [`AuthError::Transport`]; the class and the endpoint are what a caller acts on.
+ #[error("could not reach {endpoint}: {detail}")]
+ Network {
+ /// Which auth endpoint was being reached.
+ endpoint: &'static str,
+ /// The generated client's own description.
+ detail: String,
+ },
/// A server response the client does not model.
#[error("unexpected {status} response from {endpoint}: {detail}")]
Unexpected {
@@ -144,7 +190,10 @@ impl AuthError {
match self {
Self::InvalidCredentials => Some(error_codes::AUTH_INVALID_CREDENTIALS),
Self::RateLimited { .. } => Some(error_codes::AUTH_RATE_LIMITED),
- Self::Unexpected { code, .. } => code.as_deref(),
+ Self::OidcNotConfigured => Some(error_codes::AUTH_OIDC_NOT_CONFIGURED),
+ Self::OidcRedirectInvalid => Some(error_codes::AUTH_OIDC_REDIRECT_INVALID),
+ Self::OidcAddressTaken => Some(error_codes::AUTH_OIDC_ADDRESS_TAKEN),
+ Self::OidcRejected { code } | Self::Unexpected { code, .. } => code.as_deref(),
_ => None,
}
}
@@ -158,6 +207,8 @@ enum Endpoint {
VerifyTotp,
Refresh,
Logout,
+ OidcAuthorize,
+ OidcCallback,
}
impl Endpoint {
@@ -168,11 +219,13 @@ impl Endpoint {
Self::VerifyTotp => "login/verify-totp",
Self::Refresh => "refresh",
Self::Logout => "logout",
+ Self::OidcAuthorize => "oidc/authorize",
+ Self::OidcCallback => "oidc/callback",
}
}
/// What a `401` from this endpoint means.
- fn unauthorized_error(self) -> AuthError {
+ fn unauthorized_error(self, code: Option) -> AuthError {
match self {
// Registration does not authenticate an existing session, so a `401` from
// it is not a real ceremony outcome; treat it as a credential rejection.
@@ -183,6 +236,32 @@ impl Endpoint {
// the password — which is what `SessionExpired` says. Neither is a *credential*
// rejection, because the password already verified to get this far.
Self::VerifyTotp | Self::Refresh | Self::Logout => AuthError::SessionExpired,
+ // The callback's three `401`s — a spent state, a refused exchange, a refused token —
+ // all mean "start again"; the code is kept for the log. The authorize declares no
+ // `401`, so one from it is a server this client does not model.
+ Self::OidcCallback => AuthError::OidcRejected { code },
+ Self::OidcAuthorize => AuthError::Unexpected {
+ status: 401,
+ endpoint: self.name(),
+ detail: String::new(),
+ code,
+ },
+ }
+ }
+
+ /// The typed refusals only the OIDC endpoints make, matched on the catalog code.
+ fn oidc_refusal(self, status: u16, code: Option<&str>) -> Option {
+ match (self, status, code) {
+ (Self::OidcAuthorize, 404, Some(error_codes::AUTH_OIDC_NOT_CONFIGURED)) => {
+ Some(AuthError::OidcNotConfigured)
+ }
+ (Self::OidcAuthorize, 400, Some(error_codes::AUTH_OIDC_REDIRECT_INVALID)) => {
+ Some(AuthError::OidcRedirectInvalid)
+ }
+ (Self::OidcCallback, 409, Some(error_codes::AUTH_OIDC_ADDRESS_TAKEN)) => {
+ Some(AuthError::OidcAddressTaken)
+ }
+ _ => None,
}
}
}
@@ -304,6 +383,10 @@ struct AuthEndpoints {
verify_totp: String,
refresh: String,
logout: String,
+ /// The server root the generated client is built on, for the operations that go through
+ /// it: the auth base with its `/v1/auth` suffix removed, or the base itself when it carries
+ /// none (the in-crate mock serves the generated paths at its root).
+ server_root: String,
}
impl AuthEndpoints {
@@ -321,10 +404,30 @@ impl AuthEndpoints {
verify_totp: format!("{trimmed}/login/verify-totp"),
refresh: format!("{trimmed}/refresh"),
logout: format!("{trimmed}/logout"),
+ server_root: trimmed
+ .strip_suffix("/v1/auth")
+ .unwrap_or(trimmed)
+ .to_owned(),
})
}
}
+/// A begun sign-in through the identity provider (`S-N2`).
+///
+/// The platform sends the person to `authorization_url`, receives the provider's redirect at
+/// the `redirect_uri` it named, and hands the redirect's `code` with this `state` to
+/// [`AuthClient::complete_oidc_login`]. Good once, and until `expires_by`.
+///
+/// No `Debug`: the state is the key to the pending ceremony and the URL carries it.
+pub struct OidcAuthorization {
+ /// Where to send the person. Carries the whole authorization request in its query.
+ pub authorization_url: String,
+ /// The `state` the provider's redirect will echo.
+ pub state: SecretString,
+ /// The absolute Unix-seconds instant the ceremony stops being redeemable.
+ pub expires_by: u64,
+}
+
/// What a password login answered with (`S-C55`).
///
/// Two variants because the server has two outcomes and says so with a status: `200` with a
@@ -378,6 +481,9 @@ impl LoginOutcome {
pub struct AuthClient {
http: reqwest::Client,
base: Arc,
+ /// The generated client over the same transport, for the operations that are not token
+ /// orchestration (the OIDC legs).
+ rest: Arc,
clock: Arc,
refresh_skew_secs: i64,
/// The advisory device-cohort hash to ride every session-creation request
@@ -391,9 +497,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,15 +511,26 @@ 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,
http: reqwest::Client,
refresh_skew_secs: i64,
) -> Result {
+ let base = AuthEndpoints::from_base(base_url)?;
+ let rest = rest::Client::with_client(http.clone(), &base.server_root).map_err(|e| {
+ AuthError::InvalidBaseUrl {
+ url: base_url.to_string(),
+ reason: e.to_string(),
+ }
+ })?;
Ok(Self {
http,
- base: Arc::new(AuthEndpoints::from_base(base_url)?),
+ base: Arc::new(base),
+ rest: Arc::new(rest),
clock,
refresh_skew_secs,
cohort_hash: None,
@@ -460,29 +578,160 @@ impl AuthClient {
.send()
.await?;
- // `202 Accepted` — the password verified and the sign-in is not finished. Read from the
- // **status**, which is where the server puts the distinction; a body flag would be a
- // second place for the two to disagree. Before `S-C63` this fell through to
- // `read_tokens` and surfaced as `MalformedResponse`, which told a user with a second
- // factor that their server was broken.
+ let outcome = self.read_login_outcome(Endpoint::Login, response).await?;
+ tracing::info!("login answered");
+ Ok(outcome)
+ }
+
+ /// Begin a sign-in through the server's identity provider (`S-N2`).
+ ///
+ /// `redirect_uri` is where the provider will send the person back — this client's own
+ /// callback, which the server admits if it is the deployment's configured one or a loopback
+ /// IP literal on any port (the shape a CLI's or desktop app's listener has). What comes back
+ /// is where to send the person and the `state` to present with the resulting `code`.
+ ///
+ /// # Errors
+ ///
+ /// [`AuthError::OidcNotConfigured`] when the server has no provider,
+ /// [`AuthError::OidcRedirectInvalid`] when it refuses the redirect.
+ #[instrument(skip_all)]
+ pub async fn begin_oidc_login(
+ &self,
+ redirect_uri: &str,
+ ) -> Result {
+ tracing::info!("beginning a sign-in through the identity provider");
+ let body = self
+ .rest
+ .begin_oidc_login(
+ PROTOCOL_VERSION.to_owned(),
+ None,
+ &rest::types::OidcAuthorizeRequest {
+ redirect_uri: redirect_uri.to_owned(),
+ },
+ )
+ .await
+ .map_err(|error| {
+ map_rest_error(Endpoint::OidcAuthorize, error, |refused| match refused {
+ rest::BeginOidcLoginError::Status400(problem)
+ | rest::BeginOidcLoginError::Status404(problem)
+ | rest::BeginOidcLoginError::Status415(problem)
+ | rest::BeginOidcLoginError::Status422(problem)
+ | rest::BeginOidcLoginError::Status426(problem)
+ | rest::BeginOidcLoginError::Status429(problem)
+ | rest::BeginOidcLoginError::Status500(problem)
+ | rest::BeginOidcLoginError::Status503(problem) => Some(*problem),
+ rest::BeginOidcLoginError::Status413 => None,
+ })
+ })?
+ .into_inner();
+ Ok(OidcAuthorization {
+ authorization_url: body.authorization_url,
+ state: SecretString::from(body.state),
+ expires_by: u64::try_from(body.expires_by).unwrap_or(0),
+ })
+ }
+
+ /// Finish a sign-in through the identity provider with what its redirect carried (`S-N2`).
+ ///
+ /// Answers a [`LoginOutcome`] exactly as [`login`](AuthClient::login) does — a session, or a
+ /// second-factor challenge for an account that enrolled one — and the configured cohort hash
+ /// rides this request, because this is the one that opens the session.
+ ///
+ /// # Errors
+ ///
+ /// [`AuthError::OidcRejected`] for a spent or expired `state`, a refused exchange or a
+ /// refused ID token (start again); [`AuthError::OidcAddressTaken`] when the provider asserted
+ /// an address that already has a local account.
+ #[instrument(skip_all)]
+ pub async fn complete_oidc_login(
+ &self,
+ state: &SecretString,
+ code: &str,
+ ) -> Result {
+ tracing::info!(
+ cohort_emitted = self.cohort().is_some(),
+ "completing a sign-in through the identity provider"
+ );
+ let answer = self
+ .rest
+ .complete_oidc_login(
+ PROTOCOL_VERSION.to_owned(),
+ None,
+ &rest::types::OidcCallbackRequest {
+ state: state.expose_secret().to_owned(),
+ code: code.to_owned(),
+ cohort_hash: self.cohort().map(str::to_owned),
+ device_id: None,
+ },
+ )
+ .await
+ .map_err(|error| {
+ map_rest_error(Endpoint::OidcCallback, error, |refused| match refused {
+ rest::CompleteOidcLoginError::Status400(problem)
+ | rest::CompleteOidcLoginError::Status401(problem)
+ | rest::CompleteOidcLoginError::Status409(problem)
+ | rest::CompleteOidcLoginError::Status415(problem)
+ | rest::CompleteOidcLoginError::Status422(problem)
+ | rest::CompleteOidcLoginError::Status426(problem)
+ | rest::CompleteOidcLoginError::Status500(problem) => Some(*problem),
+ rest::CompleteOidcLoginError::Status413 => None,
+ })
+ })?
+ .into_inner();
+ // The status is the discriminator, as on the password login; the generated enum is
+ // exactly that status made a type.
+ let outcome = match answer {
+ rest::CompleteOidcLoginResponse::Status202(challenge) => {
+ tracing::info!("the identity provider sign-in needs a second factor");
+ LoginOutcome::SecondFactorRequired {
+ mfa_token: SecretString::from(challenge.mfa_token),
+ expires_by: u64::try_from(challenge.expires_by).unwrap_or(0),
+ }
+ }
+ rest::CompleteOidcLoginResponse::Status200(pair) => {
+ let tokens = TokenSet::from_wire(
+ Endpoint::OidcCallback,
+ TokenResponseBody {
+ access_token: pair.access_token,
+ refresh_token: pair.refresh_token,
+ expires_by: u64::try_from(pair.expires_by).unwrap_or(0),
+ },
+ )?;
+ tracing::info!("the identity provider sign-in succeeded; session established");
+ LoginOutcome::Session(self.session_with_tokens(tokens))
+ }
+ };
+ Ok(outcome)
+ }
+
+ /// A `200` is a session and a `202` is a challenge, on every route that opens a session.
+ ///
+ /// Read from the **status**, which is where the server puts the distinction; a body flag
+ /// would be a second place for the two to disagree. Before `S-C63` the `202` fell through to
+ /// `read_tokens` and surfaced as `MalformedResponse`, which told a user with a second factor
+ /// that their server was broken.
+ async fn read_login_outcome(
+ &self,
+ endpoint: Endpoint,
+ response: reqwest::Response,
+ ) -> Result {
if response.status() == reqwest::StatusCode::ACCEPTED {
let challenge: SecondFactorChallengeBody =
response
.json()
.await
.map_err(|e| AuthError::MalformedResponse {
- endpoint: Endpoint::Login.name(),
+ endpoint: endpoint.name(),
reason: e.to_string(),
})?;
- tracing::info!("login needs a second factor");
+ tracing::info!("the sign-in needs a second factor");
return Ok(LoginOutcome::SecondFactorRequired {
mfa_token: SecretString::from(challenge.mfa_token),
expires_by: challenge.expires_by,
});
}
-
- let tokens = read_tokens(Endpoint::Login, response).await?;
- tracing::info!("login succeeded; session established");
+ let tokens = read_tokens(endpoint, response).await?;
+ tracing::info!("the sign-in succeeded; session established");
Ok(LoginOutcome::Session(self.session_with_tokens(tokens)))
}
@@ -799,18 +1048,35 @@ async fn read_tokens(
/// Map a non-success response to a typed [`AuthError`], capturing the server's
/// `error.*` code and `Retry-After` where present.
async fn error_from_response(endpoint: Endpoint, response: reqwest::Response) -> AuthError {
- let status = response.status();
- let retry_after = response
- .headers()
- .get(reqwest::header::RETRY_AFTER)
- .and_then(|value| value.to_str().ok())
- .and_then(|raw| raw.trim().parse::().ok());
+ let status = response.status().as_u16();
+ let retry_after = retry_after_of(response.headers());
let api_error = response.json::().await.ok();
let code = api_error.as_ref().and_then(|body| body.code.clone());
let detail = api_error.map_or_else(String::new, |body| body.error);
+ error_for(endpoint, status, code, detail, retry_after)
+}
- match status.as_u16() {
- 401 => endpoint.unauthorized_error(),
+/// The `Retry-After` seconds a response carries, if it carries one.
+fn retry_after_of(headers: &reqwest::header::HeaderMap) -> Option {
+ headers
+ .get(reqwest::header::RETRY_AFTER)
+ .and_then(|value| value.to_str().ok())
+ .and_then(|raw| raw.trim().parse::().ok())
+}
+
+/// One status → variant mapping for both the hand-rolled and the generated paths.
+fn error_for(
+ endpoint: Endpoint,
+ status: u16,
+ code: Option,
+ detail: String,
+ retry_after: Option,
+) -> AuthError {
+ if let Some(refusal) = endpoint.oidc_refusal(status, code.as_deref()) {
+ return refusal;
+ }
+ match status {
+ 401 => endpoint.unauthorized_error(code),
423 => AuthError::AccountLocked,
429 => AuthError::RateLimited {
retry_after_secs: retry_after.unwrap_or(0),
@@ -824,6 +1090,49 @@ async fn error_from_response(endpoint: Endpoint, response: reqwest::Response) ->
}
}
+/// Map a generated-client failure to a typed [`AuthError`].
+///
+/// `problem` extracts the coded problem a documented refusal carries, so the status → variant
+/// mapping is the one the hand-rolled path uses; the generated client's own classes — transport,
+/// timeout, protocol, redirect, construction — become [`AuthError::Network`], an undocumented
+/// status [`AuthError::Unexpected`], and a body that did not decode [`AuthError::MalformedResponse`].
+fn map_rest_error(
+ endpoint: Endpoint,
+ error: rest::Error,
+ problem: impl FnOnce(E) -> Option,
+) -> AuthError {
+ match error {
+ rest::Error::Api(refused) => {
+ let status = refused.status().as_u16();
+ let retry_after = retry_after_of(refused.headers());
+ match problem(refused.into_inner()) {
+ Some(problem) => error_for(
+ endpoint,
+ status,
+ Some(problem.code),
+ problem.detail.unwrap_or_default(),
+ retry_after,
+ ),
+ None => error_for(endpoint, status, None, String::new(), retry_after),
+ }
+ }
+ rest::Error::UnexpectedStatus { status, body, .. } => AuthError::Unexpected {
+ status: status.as_u16(),
+ endpoint: endpoint.name(),
+ detail: String::from_utf8_lossy(&body).into_owned(),
+ code: None,
+ },
+ rest::Error::Decode { path, .. } => AuthError::MalformedResponse {
+ endpoint: endpoint.name(),
+ reason: path,
+ },
+ other => AuthError::Network {
+ endpoint: endpoint.name(),
+ detail: other.to_string(),
+ },
+ }
+}
+
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@@ -1533,6 +1842,194 @@ mod tests {
assert!(matches!(error, AuthError::NotAuthenticated));
}
+ // ── OIDC (S-N2) ───────────────────────────────────────────────────────────
+
+ /// An RFC 9457 problem carrying the stable code, as the server's coded-problem interceptor
+ /// renders one — the generated client parses refusals into this shape.
+ fn problem(status: u16, code: &str) -> MockResponse {
+ MockResponse::json(
+ status,
+ serde_json::json!({
+ "type": "about:blank",
+ "title": "refused",
+ "status": status,
+ "detail": "the double refuses on purpose",
+ "code": code,
+ })
+ .to_string(),
+ )
+ }
+
+ /// A server with an identity provider, answering the two OIDC routes and recording what
+ /// the callback received.
+ fn oidc_handler(captured: Arc>>) -> Handler {
+ Arc::new(move |req: MockRequest| {
+ let captured = captured.clone();
+ Box::pin(async move {
+ match req.path.as_str() {
+ "/v1/auth/oidc/authorize" => MockResponse::json(
+ 200,
+ serde_json::json!({
+ "authorization_url": "https://idp.test/authorize?state=state-1",
+ "state": "state-1",
+ "expires_by": 1_893_456_000,
+ })
+ .to_string(),
+ ),
+ "/v1/auth/oidc/callback" => {
+ // The generated client sends the handshake header on every request.
+ assert_eq!(
+ req.headers.get("x-capsule-protocol").map(String::as_str),
+ Some(PROTOCOL_VERSION)
+ );
+ *captured.lock().unwrap() = serde_json::from_str(&req.body).ok();
+ MockResponse::json(200, token_json("access-1", "refresh-1", far_future()))
+ }
+ _ => MockResponse::json(404, r#"{"error":"x"}"#),
+ }
+ })
+ })
+ }
+
+ /// The two legs round-trip, and the cohort rides the callback rather than the authorize.
+ #[tokio::test]
+ async fn an_oidc_login_begins_completes_and_carries_the_cohort_on_the_callback() {
+ let captured = Arc::new(std::sync::Mutex::new(None));
+ let server = start_mock(oidc_handler(captured.clone())).await;
+ let client = AuthClient::new(&server.base_url)
+ .unwrap()
+ .with_cohort_hash("a-particular-machine".to_owned());
+
+ let begun = client
+ .begin_oidc_login("http://127.0.0.1:4242/callback")
+ .await
+ .unwrap();
+ assert_eq!(
+ begun.authorization_url,
+ "https://idp.test/authorize?state=state-1"
+ );
+ assert_eq!(begun.state.expose_secret(), "state-1");
+ assert_eq!(begun.expires_by, 1_893_456_000);
+
+ let session = finished(
+ client
+ .complete_oidc_login(&begun.state, "code-1")
+ .await
+ .unwrap(),
+ );
+ assert!(session.is_authenticated().await);
+
+ let body = captured
+ .lock()
+ .unwrap()
+ .clone()
+ .expect("callback body captured");
+ assert_eq!(body["state"], "state-1");
+ assert_eq!(body["code"], "code-1");
+ assert_eq!(body["cohort_hash"], "a-particular-machine");
+ }
+
+ /// The callback's `202` is a second-factor challenge, as the password login's is.
+ #[tokio::test]
+ async fn an_oidc_callback_can_answer_a_second_factor_challenge() {
+ let handler: Handler = Arc::new(move |req| {
+ Box::pin(async move {
+ match req.path.as_str() {
+ "/v1/auth/oidc/callback" => MockResponse::json(
+ 202,
+ r#"{"mfa_token":"challenge-1","expires_by":1893456000}"#,
+ ),
+ _ => MockResponse::json(404, r#"{"error":"x"}"#),
+ }
+ })
+ });
+ let server = start_mock(handler).await;
+ let client = AuthClient::new(&server.base_url).unwrap();
+ let outcome = client
+ .complete_oidc_login(&SecretString::from("state-1"), "code-1")
+ .await
+ .unwrap();
+ assert!(matches!(outcome, LoginOutcome::SecondFactorRequired { .. }));
+ }
+
+ /// Each OIDC refusal maps to its typed variant on the catalog code, and the `401`s to one.
+ #[tokio::test]
+ async fn oidc_refusals_map_to_typed_errors_on_their_codes() {
+ let handler: Handler = Arc::new(move |req| {
+ Box::pin(async move {
+ match (req.path.as_str(), req.body.contains("evil")) {
+ ("/v1/auth/oidc/authorize", true) => {
+ problem(400, "error.auth.oidc_redirect_invalid")
+ }
+ ("/v1/auth/oidc/authorize", false) => {
+ problem(404, "error.auth.oidc_not_configured")
+ }
+ ("/v1/auth/oidc/callback", _) if req.body.contains("taken") => {
+ problem(409, "error.auth.oidc_address_taken")
+ }
+ ("/v1/auth/oidc/callback", _) => problem(401, "error.auth.oidc_state_invalid"),
+ _ => MockResponse::json(404, r#"{"error":"x"}"#),
+ }
+ })
+ });
+ let server = start_mock(handler).await;
+ let client = AuthClient::new(&server.base_url).unwrap();
+
+ let error = client
+ .begin_oidc_login("https://evil.example.test/cb")
+ .await
+ .err()
+ .expect("refused");
+ assert!(matches!(error, AuthError::OidcRedirectInvalid), "{error:?}");
+ assert_eq!(
+ error.error_code(),
+ Some(error_codes::AUTH_OIDC_REDIRECT_INVALID)
+ );
+
+ let error = client
+ .begin_oidc_login("http://127.0.0.1:4242/cb")
+ .await
+ .err()
+ .expect("refused");
+ assert!(matches!(error, AuthError::OidcNotConfigured), "{error:?}");
+ assert_eq!(
+ error.error_code(),
+ Some(error_codes::AUTH_OIDC_NOT_CONFIGURED)
+ );
+
+ let error = expect_login_err(
+ client
+ .complete_oidc_login(&SecretString::from("state-1"), "taken")
+ .await,
+ );
+ assert!(matches!(error, AuthError::OidcAddressTaken), "{error:?}");
+ assert_eq!(
+ error.error_code(),
+ Some(error_codes::AUTH_OIDC_ADDRESS_TAKEN)
+ );
+
+ let error = expect_login_err(
+ client
+ .complete_oidc_login(&SecretString::from("state-1"), "code-1")
+ .await,
+ );
+ assert!(matches!(error, AuthError::OidcRejected { .. }), "{error:?}");
+ assert_eq!(
+ error.error_code(),
+ Some(error_codes::AUTH_OIDC_STATE_INVALID)
+ );
+ }
+
+ /// The generated client is built on the server root: the auth base minus `/v1/auth`.
+ #[test]
+ fn the_server_root_is_the_auth_base_without_its_suffix() {
+ let endpoints = AuthEndpoints::from_base("https://api.example.test/v1/auth/").unwrap();
+ assert_eq!(endpoints.server_root, "https://api.example.test");
+ assert_eq!(endpoints.login, "https://api.example.test/v1/auth/login");
+ let bare = AuthEndpoints::from_base("http://127.0.0.1:4242").unwrap();
+ assert_eq!(bare.server_root, "http://127.0.0.1:4242");
+ }
+
#[test]
fn rejects_invalid_base_url() {
assert!(matches!(
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 8fa7239c..21f85e51 100644
--- a/capsule-server/.env.example
+++ b/capsule-server/.env.example
@@ -111,12 +111,47 @@ VALKEY_URL=redis://127.0.0.1:6379
# base64, 32 or 64 bytes; 32 is expanded to 64, domain-separated.
# ATTESTATION_KEY_SEED=replace-with-your-own-base64-seed
+# ── Single sign-on (OIDC relying party) ──────────────────────────────────────────────────────
+#
+# Absent means the path is off: `server-info` publishes `auth.oidc: null` and the authorize
+# answers 404. Set an issuer and a client id together — half a relying party is refused. The
+# issuer is `https`, or `http` on a loopback IP literal for development (never `localhost`);
+# the dex service in compose.yaml (`--profile oidc`) is the local one.
+# OIDC_ISSUER=http://127.0.0.1:5556/dex
+# OIDC_CLIENT_ID=capsule
+#
+# Optional. Absent is a public client, PKCE-only — what a CLI or desktop app is (RFC 8252 §8.5).
+# OIDC_CLIENT_SECRET=
+#
+# The web client's callback, admitted exactly. Held to the issuer's scheme rule.
+# OIDC_REDIRECT_URL=https://app.capsule.example/oidc/callback
+#
+# Admit a redirect to `http://127.0.0.1:{any port}/…` or `http://[::1]:{any port}/…` — what a
+# CLI's or desktop client's loopback listener needs (RFC 8252 §7.3). **Off by default**: it is
+# the one knob that widens where the server will send a person back to. Turn it on for a
+# deployment with such a client; the `capsule auth login --oidc` flow (issue #461) will say so.
+# OIDC_ALLOW_LOOPBACK_REDIRECT=false
+#
+# A PEM bundle of additional trust anchors for reaching a provider behind a private CA. Read
+# once at boot and refused by name — never by content — if it is missing, not a certificate
+# bundle, or empty. Added to the public roots, never replacing them.
+# OIDC_CA_BUNDLE=/etc/capsule/idp-ca.pem
+
# ── 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/Cargo.toml b/capsule-server/Cargo.toml
index c60c08da..68ff30a8 100644
--- a/capsule-server/Cargo.toml
+++ b/capsule-server/Cargo.toml
@@ -124,6 +124,12 @@ subtle = { workspace = true }
# parameter set rather than the escrow KDF's tiered one — see `auth::credential` for why those
# are unrelated numbers. The Postgres adapter (#402) uses the same helper.
argon2 = { workspace = true }
+# The OIDC relying party's egress (slice `S-N1`): the discovery document, the JWK Set and the
+# form-encoded token exchange, against the configured identity provider and nothing else. The
+# one outbound HTTP client in this crate. Already in the lock file through `capsule-sdk`, so this
+# promotes an edge rather than adding a crate; the HTTP-client row in design/dependencies.md
+# carries the scope. rustls-tls only, like everywhere else Capsule holds a TLS stack.
+reqwest = { workspace = true }
# The `capsule-server` binary: subcommand parsing, and the error report a startup failure prints.
# `clap` and `color-eyre` were already here for the `gen_openapi` binary this replaces; the
# binary is now one `capsule-server` with `serve | gc | purge | scrub | gen-openapi`
@@ -158,9 +164,19 @@ capsule-sdk = { path = "../capsule-sdk" }
# to look at them. Test-only, and the only place this crate touches the type.
secrecy = { workspace = true }
# `macros` and `rt-multi-thread` on top of what the blob store's adapter already needs: the
-# `#[tokio::test]` attribute and a runtime to drive it. Test-only, so the served binary carries
-# neither.
-tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
+# `#[tokio::test]` attribute and a runtime to drive it. `net` for the in-process mock identity
+# provider `tests/support/idp.rs` binds on loopback, so the OIDC relying party is exercised
+# over a real socket. Test-only, so the served binary carries none of the three.
+tokio = { workspace = true, features = ["macros", "net", "rt-multi-thread"] }
+# The TLS half of the mock identity provider, for the `OIDC_CA_BUNDLE` case: a private CA and
+# a leaf it signs (`rcgen`), served by `tokio-rustls`, so "the relying party trusts the operator's
+# CA and nothing else" is proven over a real handshake. The same three crates, versions and
+# features `capsule-sdk` pins for the peering stack - `ring` provider throughout, matching the
+# rustls `reqwest` links - so nothing new enters the lock file. Dev-only: the served binary
+# terminates no TLS (design/cryptography/failure-modes.md).
+rcgen = { version = "0.13", default-features = false, features = ["ring", "crypto"] }
+rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
+tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
# The feed's manifest bytes arrive base64-encoded, and a test that asserts byte equality has to
# decode them. A normal dependency of the crate since `S-C2`; listed here because the suite
# uses it directly.
diff --git a/capsule-server/compose.yaml b/capsule-server/compose.yaml
index 60013ef1..8fbb9b4e 100644
--- a/capsule-server/compose.yaml
+++ b/capsule-server/compose.yaml
@@ -71,6 +71,65 @@ services:
volumes:
- valkey_data:/data:Z,U
+ # A development identity provider for the OIDC relying party (slice `S-N1`, issue #407). Not a
+ # dependency of the server — a deployment without `OIDC_ISSUER` never talks to it — so it is a
+ # separate profile: `podman compose -f capsule-server/compose.yaml --profile oidc up -d dex`.
+ #
+ # Point the server at it with
+ #
+ # OIDC_ISSUER=http://127.0.0.1:5556/dex
+ # OIDC_CLIENT_ID=capsule
+ # OIDC_ALLOW_LOOPBACK_REDIRECT=true
+ #
+ # and no client secret: `capsule` is a **public** client, which is what a CLI or desktop app
+ # is (RFC 8252 §8.5), and dex admits `http://127.0.0.1:*` and `http://localhost:*` redirects
+ # for public clients without listing them — so the CLI's ephemeral loopback listener works
+ # unconfigured at dex, and `OIDC_ALLOW_LOOPBACK_REDIRECT=true` (off by default) admits the
+ # same on Capsule's side. The issuer is plain `http` on loopback, the one carve-out
+ # `auth::oidc::discovery` makes; a real deployment's issuer is `https`.
+ #
+ # Sign in as `admin@example.com` / `password` (the bcrypt below is dex's own documented hash
+ # for that word). Development credentials in a checked-in file, like Postgres's above; the
+ # port is loopback-only for the same reason.
+ #
+ # The dex configuration is inline (`configs.*.content`) rather than a file beside this one,
+ # so the stack stays one file. docker compose ≥ 2.23.1 and podman-compose ≥ 1.2.0 read it.
+ dex:
+ image: ghcr.io/dexidp/dex:v2.44.0
+ profiles: ["oidc"]
+ command: ["dex", "serve", "/etc/dex/config.yaml"]
+ ports:
+ - "127.0.0.1:5556:5556"
+ configs:
+ - source: dex_config
+ target: /etc/dex/config.yaml
+ healthcheck:
+ test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:5556/dex/healthz || exit 1"]
+ interval: 5s
+ timeout: 3s
+ retries: 12
+
+configs:
+ dex_config:
+ content: |
+ issuer: http://127.0.0.1:5556/dex
+ storage:
+ type: memory
+ web:
+ http: 0.0.0.0:5556
+ oauth2:
+ skipApprovalScreen: true
+ staticClients:
+ - id: capsule
+ name: Capsule (development)
+ public: true
+ enablePasswordDB: true
+ staticPasswords:
+ - email: admin@example.com
+ hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
+ username: admin
+ userID: 08a8684b-db88-4b73-90a9-3cd1661f5466
+
volumes:
postgres_data:
valkey_data:
diff --git a/capsule-server/openapi.json b/capsule-server/openapi.json
index 3e80f9f5..cb055eca 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,90 +6503,86 @@
}
}
}
- },
- "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/oidc/authorize": {
+ "post": {
+ "summary": "Begin a sign-in through the identity provider.",
+ "description": "Unauthenticated: this is how a person *becomes* a session. Nothing about the account is\nknown yet — the ceremony carries fresh random `state`, `nonce` and PKCE material and the\nadmitted redirect URI, and the record behind the `state` lives for ten minutes.\n\nBounded twice, because it is an unauthenticated write into a store: a budget per redirect\nhost ([`budgets::OIDC_AUTHORIZE`]) answers `429` before anything is done, and the store's\nown ceiling answers `503` when it is nevertheless full.",
+ "operationId": "begin_oidc_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.",
+ "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": {
+ "application/json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/OidcAuthorizeRequest"
}
}
},
"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"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -3545,18 +6593,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "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/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": {
@@ -3565,8 +6629,34 @@
}
}
},
- "415": {
- "description": "Unsupported media type",
+ "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": {
@@ -3575,62 +6665,70 @@
}
}
},
- "204": {
- "description": "the request succeeded and there is no content to send",
+ "200": {
+ "description": "OK",
"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": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/ProtocolRangeProblem"
+ "$ref": "#/components/schemas/OidcAuthorizationResponse"
}
}
}
},
"404": {
- "description": "Upload session not found",
- "content": {
- "application/problem+json": {
+ "description": "Single sign-on not configured",
+ "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}$"
}
- }
- }
- },
- "409": {
- "description": "Offset mismatch",
- "content": {
- "application/problem+json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest 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-Min-Client-Build": {
+ "description": "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": {
@@ -3638,45 +6736,33 @@
}
}
}
- }
- },
- "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"
- ]
- }
- }
- ],
- "responses": {
- "401": {
- "description": "Unauthorized",
+ },
+ "429": {
+ "description": "Too many sign-ins",
"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": {
@@ -3687,18 +6773,34 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "content": {
- "application/problem+json": {
+ "503": {
+ "description": "Sign-in capacity reached",
+ "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": {
@@ -3707,18 +6809,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/SessionsResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will 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": {
@@ -3728,105 +6846,62 @@
}
},
"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",
+ "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"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "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}$"
}
- }
- }
- },
- "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]+$"
}
}
}
},
- "200": {
- "description": "OK",
- "content": {
- "application/cbor": {
+ "426": {
+ "description": "Protocol version unsupported",
+ "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": {
"schema": {
@@ -3834,31 +6909,46 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
}
- ]
+ }
}
},
- "/v1/albums/{album_id}/ops": {
+ "/v1/auth/oidc/callback": {
"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",
+ "summary": "Finish a sign-in with what the provider's redirect carried.",
+ "description": "The `state` is burned first and whatever happens next: a ceremony that survived a failed\ncallback would be a ceremony an attacker could retry a stolen code against. Then the code is\nexchanged and the ID token verified by the provider adapter, the identity is resolved to an\naccount — created on first sight, keyed on `(issuer, subject)`, never linked by address — and\nthe session is opened exactly as a password sign-in opens one, second factor included.",
+ "operationId": "complete_oidc_login",
"parameters": [
{
- "name": "album_id",
- "in": "path",
- "description": "The album'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
}
}
],
@@ -3866,45 +6956,41 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/OpRequest"
+ "$ref": "#/components/schemas/OidcCallbackRequest"
}
}
},
"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": {
@@ -3915,6 +7001,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": {
@@ -3925,6 +7037,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": {
@@ -3934,37 +7072,105 @@
}
},
"200": {
- "description": "OK",
+ "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": {
- "$ref": "#/components/schemas/OpResponse"
+ "$ref": "#/components/schemas/TokenResponse"
}
}
}
},
- "426": {
- "description": "Upgrade required",
- "content": {
- "application/problem+json": {
+ "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": {
- "$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]+$"
}
}
- }
- },
- "409": {
- "description": "Stale revival",
+ },
"content": {
- "application/problem+json": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/StaleRevivalProblem"
+ "$ref": "#/components/schemas/SecondFactorChallenge"
}
}
}
},
- "500": {
- "description": "Internal server error",
+ "401": {
+ "description": "Sign-in expired",
+ "headers": {
+ "X-Capsule-Protocol-Min": {
+ "description": "The oldest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.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": {
@@ -3973,62 +7179,32 @@
}
}
},
- "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",
+ "409": {
+ "description": "Address already registered",
"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": {
@@ -4039,18 +7215,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": {
@@ -4059,18 +7251,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/SyncPageResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will 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": {
@@ -4078,65 +7315,46 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "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. 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",
+ "/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": "hash",
- "in": "path",
- "description": "The blob's ciphertext content address, lowercase hex.",
- "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
}
}
],
@@ -4151,6 +7369,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": {
@@ -4163,16 +7405,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": {
@@ -4182,100 +7440,134 @@
}
},
"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/EnrollmentCodeResponse"
}
}
}
},
- "206": {
- "description": "the part the request asked for",
+ "500": {
+ "description": "Internal server error",
"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"
- }
- },
- "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+$"
- }
- }
+ "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",
+ "413": {
+ "description": "the request body exceeds the configured limit",
"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": {
+ "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]+$"
}
}
- }
- },
- "409": {
- "description": "Upload in progress",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4284,18 +7576,34 @@
}
}
},
- "410": {
- "description": "Gone",
- "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": {
@@ -4303,9 +7611,6 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
}
},
"security": [
@@ -4315,54 +7620,84 @@
]
}
},
- "/v1/storage/verify": {
+ "/v1/auth/devices/enroll/redeem": {
"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",
+ "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": "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"
+ "$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": {
@@ -4373,6 +7708,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 +7744,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": {
@@ -4393,61 +7780,66 @@
},
"200": {
"description": "OK",
- "content": {
- "application/json": {
+ "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}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will 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/ChannelResponse"
}
}
}
},
- "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": "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": {
@@ -4458,92 +7850,68 @@
}
}
},
- "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}$"
}
- }
- }
- },
- "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/AssetReceiptsResponse"
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "404": {
- "description": "Not found",
+ },
"content": {
"application/problem+json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/EnrollmentRateLimitedProblem"
}
}
}
},
"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/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"
- }
- }
- },
- "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"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -4554,58 +7922,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}$"
}
- }
- }
- },
- "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}$"
}
- }
- }
- },
- "201": {
- "description": "The share link is registered and servable",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest 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-Min-Client-Build": {
+ "description": "The semver client build below which this server will 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": {
@@ -4613,45 +7986,94 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "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",
+ "/v1/auth/devices/enroll/channel/{channel_id}": {
+ "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",
"parameters": [
{
- "name": "opaque_id",
+ "name": "channel_id",
"in": "path",
- "description": "The opaque id.",
+ "description": "The handle a redeemed code returned.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "direction",
+ "in": "query",
+ "description": "`to_initiator` or `to_enrollee`.",
"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",
+ "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"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -4662,98 +8084,70 @@
}
}
},
- "403": {
- "description": "Forbidden",
- "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}$"
}
- }
- }
- },
- "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": {
+ "application/json": {
"schema": {
- "$ref": "#/components/schemas/CodedProblem"
+ "$ref": "#/components/schemas/DrainResponse"
}
}
}
},
- "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",
- "content": {
- "application/problem+json": {
+ "404": {
+ "description": "Channel not found",
+ "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/SharedMetadataResponse"
+ "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]+$"
}
}
- }
- },
- "429": {
- "description": "Too many requests",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4764,60 +8158,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"
- }
- }
- }
- },
- "/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",
- "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/octet-stream": {
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
"schema": {
"type": "string",
- "format": "binary"
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
}
}
- }
- },
- "404": {
- "description": "Not found",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -4826,94 +8192,123 @@
}
}
},
- "429": {
- "description": "Too many requests",
- "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}$"
}
- }
- }
- },
- "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"
}
}
- }
- },
- "/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",
+ },
+ "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": "opaque_id",
- "in": "path",
- "description": "The opaque id.",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "hash",
+ "name": "channel_id",
"in": "path",
- "description": "The blob's content address.",
+ "description": "The handle a redeemed code returned.",
"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
}
}
],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RelayRequest"
+ }
+ }
+ },
+ "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": {
@@ -4922,101 +8317,135 @@
}
}
},
- "200": {
- "description": "the whole representation",
+ "415": {
+ "description": "Unsupported Media Type",
"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",
+ "422": {
+ "description": "Unprocessable Entity",
"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"
- }
- },
- "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+$"
- }
- }
+ "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",
+ "204": {
+ "description": "the request succeeded and there is no content to send",
"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": {
+ "description": "Channel not found",
+ "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,45 +8456,30 @@
},
"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"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -5076,58 +8490,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}$"
}
- }
- }
- },
- "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}$"
}
- }
- }
- },
- "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}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will 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": {
@@ -5135,32 +8554,54 @@
}
}
}
- },
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "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",
+ "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": "opaque_id",
+ "name": "channel_id",
"in": "path",
- "description": "The opaque id.",
+ "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
+ }
}
],
"responses": {
@@ -5174,6 +8615,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,6 +8651,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": {
@@ -5196,6 +8687,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": {
@@ -5205,98 +8722,62 @@
}
},
"204": {
- "description": "the request succeeded and there is no content to send"
- },
- "500": {
- "description": "Internal server error",
- "content": {
- "application/problem+json": {
+ "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/CodedProblem"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
}
- }
- }
- },
- "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",
- "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": {
+ "404": {
+ "description": "Channel not found",
+ "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": "A drop session is open and accepting chunks",
- "content": {
- "application/json": {
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
"schema": {
- "$ref": "#/components/schemas/CreateDropResponse"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will 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": {
@@ -5305,18 +8786,34 @@
}
}
},
- "403": {
- "description": "Passphrase required",
- "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]+$"
}
}
- }
- },
- "409": {
- "description": "Link capacity exhausted",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5326,27 +8823,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 +8887,100 @@
}
}
}
- }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ]
}
},
- "/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",
+ "/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 of the link the session belongs to.",
- "required": true,
- "schema": {
- "type": "string"
- }
- },
- {
- "name": "upload_id",
- "in": "path",
- "description": "The session 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-Offset",
+ "name": "X-Capsule-Crypto-Suite",
"in": "header",
- "description": "Where in the blob this chunk starts.",
+ "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": {
+ "application/json": {
"schema": {
- "type": "string",
- "format": "binary"
+ "$ref": "#/components/schemas/ProvisionAlbumRequest"
}
}
},
"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,22 +8989,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}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will 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": {
@@ -5463,8 +9025,34 @@
}
}
},
- "409": {
- "description": "Chunk refused",
+ "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": {
@@ -5473,8 +9061,34 @@
}
}
},
- "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": {
@@ -5483,27 +9097,32 @@
}
}
},
- "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",
+ "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"
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. Advisory: `0.0.0` when no cutoff has been announced.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"
+ }
}
},
"content": {
@@ -5514,28 +9133,106 @@
}
}
},
- "403": {
- "description": "Forbidden",
+ "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",
+ "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",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Protocol-Max": {
+ "description": "The newest protocol version this server accepts.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
+ }
+ },
+ "X-Capsule-Min-Client-Build": {
+ "description": "The semver client build below which this server will stop answering. 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"
+ "$ref": "#/components/schemas/ProvisionAlbumResponse"
}
}
}
},
"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 +9242,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,32 +9314,54 @@
]
}
},
- "/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}/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": "drop_id",
+ "name": "album_id",
"in": "path",
- "description": "The drop's identifier.",
+ "description": "The album's id.",
"required": true,
"schema": {
"type": "string"
}
- }
- ],
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/AdoptRequest"
- }
+ },
+ {
+ "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",
@@ -5592,28 +9373,32 @@
"type": "string"
},
"example": "Bearer"
- }
- },
- "content": {
- "application/problem+json": {
+ },
+ "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}$"
}
- }
- }
- },
- "403": {
- "description": "Forbidden",
- "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]+$"
}
}
- }
- },
- "400": {
- "description": "Bad Request",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5622,8 +9407,34 @@
}
}
},
- "415": {
- "description": "Unsupported Media Type",
+ "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": {
@@ -5632,8 +9443,34 @@
}
}
},
- "422": {
- "description": "Unprocessable Entity",
+ "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": {
@@ -5644,16 +9481,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/AdoptResponse"
+ "$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": {
@@ -5664,6 +9553,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": {
@@ -5673,7 +9588,33 @@
}
},
"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]+$"
+ }
+ }
+ }
}
},
"security": [
@@ -5681,24 +9622,65 @@
"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",
+ },
+ "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": "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/cbor": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ },
+ "required": true
+ },
"responses": {
"401": {
"description": "Unauthorized",
@@ -5710,6 +9692,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 +9728,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 +9764,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]+$"
}
}
- }
- },
- "204": {
- "description": "the request succeeded and there is no content to send"
- },
- "404": {
- "description": "Not found",
+ },
"content": {
"application/problem+json": {
"schema": {
@@ -5753,8 +9798,34 @@
}
}
},
- "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": {
@@ -5763,38 +9834,10345 @@
}
}
},
- "413": {
- "description": "the request body exceeds the configured limit"
- }
- },
- "security": [
- {
- "bearer": []
+ "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"
+ }
+ }
+ }
+ },
+ "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": "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": {
+ "$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": []
+ }
+ ]
+ },
+ "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": "album_id",
+ "in": "path",
+ "description": "The album's id.",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "intent_id",
+ "in": "query",
+ "description": "The ceremony to abort.",
+ "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/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": {
+ "$ref": "#/components/schemas/CodedProblem"
+ }
+ }
+ }
+ },
+ "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": {
+ "$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/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": "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/CreateUploadRequest"
+ }
+ }
+ },
+ "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": "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": {
+ "$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/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": "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. 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",
+ "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/ShareMetadataRateLimitedProblem"
+ }
+ }
+ }
+ },
+ "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/ShareSecretRateLimitedProblem"
+ }
+ }
+ }
+ },
+ "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/ShareBlobRateLimitedProblem"
+ }
+ }
+ }
+ },
+ "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/DropRateLimitedProblem"
+ }
+ }
+ }
+ },
+ "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": {
- "VersionResponse": {
- "properties": {
- "name": {
- "type": "string",
- "description": "The server package name."
- },
- "version": {
- "type": "string",
- "description": "The server package version."
- }
- },
- "type": "object",
- "required": [
- "name",
- "version"
- ],
- "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."
- },
"RegisterRequest": {
"properties": {
"email": {
@@ -5989,113 +20367,31 @@
],
"description": "What a global sign-out closed."
},
- "SessionView": {
+ "ReauthenticateRequest": {
"properties": {
- "session_id": {
- "type": "string",
- "description": "The session's identifier — what a revoke names."
- },
- "created_at": {
- "type": "string",
- "description": "When this session *record* was minted, RFC 3339.\n\nA refresh rotates the session, so after one this is the rotation time and not the\nsign-in. `authenticated_at` is the field that answers \"when did you last sign in\"."
- },
- "authenticated_at": {
- "type": "string",
- "description": "When the user last proved a credential on this session's lineage, RFC 3339.\n\nCarried forward across refreshes, so it is the one timestamp here that means what a\nuser reading a devices list expects \"signed in\" to mean. It is also what the\ncross-device add's freshness gate reads (`S-C7`), so a client can show why an add is\nabout to ask for a password again."
- },
- "last_active_at": {
+ "password": {
"type": "string",
- "description": "When it was last seen, RFC 3339.\n\nEqual to `created_at` until `S-C48` puts the session ledger on the request path. A\nclient must not label this \"last used\" before then."
- },
- "user_agent": {
- "type": [
- "string",
- "null"
- ],
- "description": "The `User-Agent` the opening ceremony carried, if any."
- },
- "ip_address": {
- "type": [
- "string",
- "null"
- ],
- "description": "The address the opening ceremony came from, if any."
- },
- "cohort_hash": {
- "type": [
- "string",
- "null"
- ],
- "description": "The advisory cohort this session asserted, if any. Grouping only."
- },
- "device_id": {
- "type": [
- "string",
- "null"
- ],
- "description": "The directory device the client claimed to be (`S-N3`), if any.\n\nA different identifier space from `cohort_hash`: this names one directory device, the\ncohort groups re-enrollments of one physical device. Both are client-asserted; neither\ngates anything."
- },
- "current": {
- "type": "boolean",
- "description": "Whether this is the session making the request.\n\nSo a client can label \"this device\" without comparing tokens it should not be handling,\nand so revoking the current session is a deliberate act rather than an accident."
+ "description": "The account's password."
}
},
"type": "object",
"required": [
- "session_id",
- "created_at",
- "authenticated_at",
- "last_active_at",
- "current"
+ "password"
],
- "description": "One live session."
+ "description": "A password, re-presented on a session that already exists."
},
- "CohortView": {
+ "ReauthenticateResponse": {
"properties": {
- "cohort_hash": {
- "type": "string",
- "description": "The advisory hash."
- },
- "first_seen": {
- "type": "string",
- "description": "The first time this account was seen under it, RFC 3339.\n\nWhat lets a client say *\"a device you've used before\"* about a session whose own\n`device_id` is new — which is the entire reason the map is durable."
- },
- "last_seen": {
+ "authenticated_at": {
"type": "string",
- "description": "The most recent time, RFC 3339."
- }
- },
- "type": "object",
- "required": [
- "cohort_hash",
- "first_seen",
- "last_seen"
- ],
- "description": "One cohort this account has been seen under."
- },
- "DevicesResponse": {
- "properties": {
- "sessions": {
- "items": {
- "$ref": "#/components/schemas/SessionView"
- },
- "type": "array",
- "description": "Every live session, oldest first."
- },
- "cohorts": {
- "items": {
- "$ref": "#/components/schemas/CohortView"
- },
- "type": "array",
- "description": "Every cohort this account has ever been seen under, oldest first sighting first.\n\nServed **beside** the sessions rather than folded into them, because a cohort outlives\nthe sessions that carried it: a reinstall's new session groups with a cohort whose other\nsessions expired months ago, and a client that only had per-session cohorts could not\nsay \"you have used this device before\"."
+ "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": [
- "sessions",
- "cohorts"
+ "authenticated_at"
],
- "description": "The session ledger."
+ "description": "When the re-authenticated session's freshness window last opened."
},
"PublishDirectoryResponse": {
"properties": {
@@ -6130,31 +20426,18 @@
],
"description": "What storing an escrow did."
},
- "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": {
+ "UpdateProfileRequest": {
"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."
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The display name to set, clear (`null`), or leave alone (absent)."
}
},
"type": "object",
- "required": [
- "authenticated_at"
- ],
- "description": "When the re-authenticated session's freshness window last opened."
+ "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