Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
20c4d81
feat(server): federation capabilities, the stores behind them, and th…
justin13888 Sep 6, 2026
0b7fe30
Merge branch 'feat/server-album-membership-405' at 710867a9 into feat…
justin13888 Sep 6, 2026
de48e9e
feat(server): serve one album's sync page to a federated peer's capab…
justin13888 Sep 9, 2026
d6987f8
feat(server): serve a federated peer's blob fetch within its capabili…
justin13888 Sep 9, 2026
5e065eb
feat(server): mint, revoke and refresh the federation capability
justin13888 Sep 9, 2026
b1861f9
feat(server): take signed federated moderation reports and cut a bloc…
justin13888 Sep 9, 2026
6eabbcd
feat(server): keep federation's capabilities, revocations and peers i…
justin13888 Sep 9, 2026
7dedbc1
feat(sdk): pull a shared album from a peer's home server under a capa…
justin13888 Sep 9, 2026
3ba6c8c
docs(federation): say what the federation layer now does, and what it…
justin13888 Sep 9, 2026
436c4f8
style(federation): clear the lints the check gate does not reach
justin13888 Sep 9, 2026
e0ad630
fix(server): bound a federation grant by an absolute deadline its ref…
justin13888 Sep 9, 2026
6ace9b8
fix(server): keep a federated report's signed bytes so the row can be…
justin13888 Sep 9, 2026
3e66695
fix(server): bound every field, account and budget on the federated r…
justin13888 Sep 10, 2026
f731cf1
docs(server): link the body-cap constraint to the issue that tracks it
justin13888 Sep 10, 2026
94b217e
docs(federation): say that the blocklist and report intake cannot be …
justin13888 Sep 10, 2026
b1b1786
fix(server): answer a peer's backup fetch as unrelated, and state wha…
justin13888 Sep 10, 2026
cc00855
fix: pin the epoch boundary every adapter must agree on, and thread t…
justin13888 Sep 10, 2026
a1591ed
docs(server): regenerate the document, and stop citing a bound that b…
justin13888 Sep 10, 2026
bb4c86e
test(server): pin that a capability is refused on every account-only …
justin13888 Sep 10, 2026
ae74ff5
Merge branch 'feat/server-album-membership-405' at 5fcd4a6c into feat…
justin13888 Sep 10, 2026
74ac803
fix(server): enforce the grant-lifetime ceiling in the store, not in …
justin13888 Sep 10, 2026
b8babdb
fix(server): accept a report for an unknown account rather than revea…
justin13888 Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 45 additions & 3 deletions SLICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,10 @@ row's remainder now lives.
| S-D29 | Local alert surface (`capsule-core::notify` + native delivery) | sdk/clients | S-Z11 | M | ACTIVE | ready | |
| S-D20 | CLI truthfulness pass (status/register/endpoints/flags) | sdk/clients | — | M | MIXED | done | |
| S-E1 | Share-link end-to-end serving | fed/sharing | S-C4 | M | MIXED | done\* | live-browser smoke → `S-Q5`; seeds → gates |
| S-E2 | Federation capabilities + pulls | fed/sharing | S-C2, S-A3 | L | RETIRED | ready | capability gate on the live read method → `S-E5` |
| S-E2 | Federation capabilities + pulls | fed/sharing | S-C2, S-A3 | L | RETIRED | part | the serving half ships: mint/refresh/revoke, the capability arm on `GET /v1/sync?album_id=` and `GET /v1/blob/{hash}`, per-peer events budget, Postgres ordinal 6; the receiving half (egress worker, re-validation, rejected-hash table) → #476 |
| S-E3 | LAN peering | fed/sharing | S-D2, S-C7 | L | RETIRED | ready | live mDNS → post-v1 (peering.md note) |
| S-E4 | Aggregated federated albums (album-group view) | fed/sharing | S-E2, S-D2 | L | MIXED | done | cover override rides post-v1 settings doc |
| S-E5 | Federation capability gate on the REST sync surface | fed/sharing | — | M-L | RETIRED | ready | |
| S-E5 | Federation capability gate on the REST sync surface | fed/sharing | — | M-L | RETIRED | done | one `bearer` component, two principals; the peer arm is bound to the capability's album and its member's granted epoch |
| S-F1 | uniffi consolidation (0.29 catalog vs 0.31 core) | platform/FFI | — | M | ACTIVE | done | |
| S-F2 | Secure Enclave / StrongBox hybrid composition | platform/FFI | S-A4, S-F1 | L | ACTIVE | done\* | Kotlin run → owed-CI |
| S-F3 | Xcode/Gradle binding wiring + on-device CI | platform/FFI | S-F2 | L | ACTIVE | done\* | first CI runs + device lanes → owed-CI |
Expand Down Expand Up @@ -3838,6 +3838,23 @@ them was incidental:
discovery and pinning, signed report intake with `S-C32`'s rate limit, the blocklist and its
enforcement point, and whatever admin authentication the above needs.
- **Blocked on:** the federation layer (`S-E2`'s territory) and `S-C32`. **Tier:** Unit + Smoke.
- **Status note (2026-09-09): both halves ship, one question stays open.** The
federation-capability layer landed with `S-E2`, and both of this slice's blocked deliverables
followed. **Report intake** is `POST /v1/federation/reports`: the report carries its own
Ed25519 signature over the canonical CBOR of its other fields, verified against the peer's
key, and the signature is verified **before** the `(reporting_server, reported_user)` budget
is charged, so a third party spoofing `reporting_server` cannot spend a real peer's allowance.
A report nobody signed for is dropped and never queued; an accepted one writes a row an
operator reads through `ModerationStore::pending_reports` and changes nothing about the
reported account. **The blocklist** is `blocked_at` on the peer row and is consulted at mint,
at every presentation, at refresh and at intake; blocking also cuts and publishes every live
grant the peer holds.
- **Still owed here.** Peer keys are **operator-pinned**: this server has no outbound HTTP
client, so nothing fetches or TOFU-pins another server's `server-info`, and the operator
command that would do the pinning cannot be written until the durable boot arm exists (`serve
--memory` forgets what it pinned). The **admin authentication model** this slice names first
is untouched: `pending_reports` is the queue, and reading it over HTTP is what waits.
Blocklist *exchange* stays v2 by the contract. Filed as #476.

### S-C50 — the share-link privacy strip is specified where it cannot run

Expand Down Expand Up @@ -4522,7 +4539,21 @@ Kynos server, which cannot be written until `S-C53` gives the server a way to cr
bullets pass; E2E case 4 lives. **Tier:** Unit + Smoke + E2E case 4.
- **Landed in retired code:** capabilities, budgets, and revocation state ship on the
Salvo server. **Re-scoped onto Kynos.**
- **Owed:** capability gate on the live method → `S-E5`.
- **Status note (2026-09-09).** The **serving half** ships on Kynos. `capsule-server::federation`
mints an EdDSA-JWT capability under the server's own operational key (the one `server-info`
publishes), records it, refreshes it idempotently on `(peer, jti)`, and revokes it — and the
store **is** the revocation list, so `/.well-known/capsule/revoked-jti` and "is this `jti`
revoked" have one answer. The pull is the existing reads: `GET /v1/sync?album_id=` and
`GET /v1/blob/{hash}` take the capability on the same `bearer` component a session token
rides (`S-E5`). Scope is enforced against the blob's server-visible role, the per-peer
events-per-hour budget rides `CounterStore`, and both stores have in-memory and Postgres
adapters passing one conformance suite (migration ordinal 6). `capsule-sdk::federation`
orchestrates a pull over generated calls only.
- **Owed:** the **receiving** half — the egress worker that fetches on a schedule, invariant-20
re-validation of what it pulls, per-`(receiving_user, source_peer)` quota, the breadcrumb
index and the soft-fail rejected-hash table — plus bytes/hour and CPU/hour budgets, the error
budget, the circuit breaker and the probation tier, which need a weighted counter this port
does not have. Filed as #476. `error.federation.circuit_open` stays unused until it lands.

### S-E3 — LAN peering

Expand Down Expand Up @@ -4576,6 +4607,17 @@ Kynos server, which cannot be written until `S-C53` gives the server a way to cr
- **Note:** the verifier itself (`federation::pull::authorize`) is `ACTIVE` core and does
not change — this slice is purely about giving it a production caller on the new
transport.
- **Status note (2026-09-09): done.** The verifier was rebuilt on Kynos rather than called
from the retired tree, because the retired one has no store behind it. `federation::scheme`
registers a second security scheme under the **same** `bearer` component name and description
as the session scheme — one entry in the document, one credential key in the generated SDK —
and hands the handler a `Principal::{Session, Peer}`. The authenticator asks the session
module first and only then the capability codec, so every existing bearer path is byte-for-byte
what it was; a capability that verifies must also be one this server **recorded**. Coded
refusals are the route's, from the admitted credential: revoked, wrong album, insufficient
scope, blocked peer, over budget. Peer identity is grounded in `federation_peers`, closing
`S-C8`'s note. E2E case 4's server half runs in `capsule-server/tests/federation.rs`, and its
SDK-over-a-socket half in `capsule-server/tests/sdk_client.rs`.

## Lane F — platform / FFI

Expand Down
7 changes: 7 additions & 0 deletions capsule-android/src/androidMain/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1881,15 +1881,22 @@
<string name="error_escrow_malformed">The recovery backup could not be saved.</string>
<string name="error_escrow_not_stored">No recovery backup is saved for this account.</string>
<string name="error_escrow_unavailable">Capsule couldn\'t reach the recovery backup. Please try again.</string>
<string name="error_federation_album_not_found">That album couldn\'t be found.</string>
<string name="error_federation_audience_mismatch">This access grant is for a different album.</string>
<string name="error_federation_capability_expired">This shared album\'s access has expired.</string>
<string name="error_federation_capability_invalid">This shared album\'s access could not be verified.</string>
<string name="error_federation_capability_malformed">That sharing request isn\'t valid.</string>
<string name="error_federation_capability_revoked">Access to this shared album has been revoked.</string>
<string name="error_federation_circuit_open">This source is temporarily backed off after repeated errors.</string>
<string name="error_federation_member_not_on_roster">That person isn\'t on this album\'s member list.</string>
<string name="error_federation_not_configured">This server doesn\'t share albums with other servers.</string>
<string name="error_federation_peer_unknown">That server isn\'t one this server knows.</string>
<string name="error_federation_rate_budget_exceeded">This source has reached its request limit. Please wait and try again.</string>
<string name="error_federation_revocations_unavailable">Capsule couldn\'t read the revocation list. Please try again.</string>
<string name="error_federation_scope_insufficient">This access grant does not cover the requested content.</string>
<string name="error_federation_unavailable">Capsule couldn\'t reach the federation records. Please try again.</string>
<string name="error_moderation_account_suspended">Your account is suspended. You can\'t upload or share until it\'s reinstated.</string>
<string name="error_moderation_report_malformed">That report isn\'t valid.</string>
<string name="error_moderation_report_rate_limited">Too many reports from this source. Please wait and try again.</string>
<string name="error_moderation_report_unsigned">The moderation report could not be verified.</string>
<string name="error_moderation_server_blocked">This server is blocked from federating with us.</string>
Expand Down
1 change: 0 additions & 1 deletion capsule-docs/planned-modules.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,3 @@
capsule-core::media The Capsule-side owner of decode, metadata extraction and derivative generation, which will consume Rawshift once Rawshift stabilizes. Rawshift is a pinned submodule today and is not a workspace dependency, so nothing consumes it and this module has no body to write yet. Lane B in SLICES.md.
capsule-core::notify Alert classes and their trigger predicates, so every platform evaluates one shared decision function rather than reimplementing the taxonomy. Contract: design/notifications.md. Tier 0 has no server half, so this is client-only work.
capsule-core::import::camera The PTP/IP tethered-camera source adapter (S-B9). Post-v1; the contract exists so the adapter seam is fixed before anything implements it.
capsule-server::federation Server-to-server federation pull. The whole surface is post-v1 — `capsule-server` has no federation route, no capability-token verifier and no per-peer budget enforcement.
11 changes: 9 additions & 2 deletions capsule-docs/src/content/docs/design/api-surfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ the gate that keeps it current — is [Developer Documentation](/design/develope
| Album roster publish (`PUT /v1/albums/{album_id}/roster`) | REST | `capsule-server::membership` | [Threat Model — Validation](/design/threat-model/validation/) (invariant 33) |
| Blob fetch (`GET /v1/blob/{hash}`, HTTP `Range`) | REST | `capsule-server::blob` | [Download & Sync](/design/import/download-sync/) |
| Sync feed (change discovery after a cursor) | REST | `capsule-server::sync` | [Download & Sync](/design/import/download-sync/) |
| Federation pull | REST | `capsule-server::federation` | [Federation](/design/federation/) |
| Federation capability lifecycle (`POST /v1/albums/{album_id}/capabilities`, `DELETE /v1/albums/{album_id}/capabilities/{jti}`, `POST /v1/federation/capabilities/refresh`) and signed report intake (`POST /v1/federation/reports`) | REST | `capsule-server::federation` | [Federation](/design/federation/) |
| Federation **pull** — no route of its own: a peer reads `GET /v1/sync?album_id=` and `GET /v1/blob/{hash}` with a capability in the `bearer` slot | REST | `capsule-server::federation` (the credential and its admission) over `::sync` / `::serve` | [Federation](/design/federation/) |
| Share serving (`/s/{opaque_id}`) | REST | `capsule-server::share` | [Share Links](/design/share-links/) |
| Guest drops (`POST /d/{opaque_id}`, inbox, adoption) | REST | `capsule-server::drop` | [Web Upload](/design/web-upload/) |
| Storage verification (`POST /v1/storage/verify`) | REST | `capsule-server::verify` | [Storage Verification](/design/import/storage-verification/) |
Expand Down Expand Up @@ -167,7 +168,13 @@ 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
carriage.
carriage. **The document carries one `bearer` component for both.** The two read primitives a peer
pulls through — `GET /v1/sync` and `GET /v1/blob/{hash}` — register a second Kynos security scheme
under the same component name and a byte-identical description, so every operation's `security` is
the one requirement it always was and the generated client attaches either token type under the one
credential key it knows. A second key would have split one carriage into two for a difference the
wire does not have. `capsule-server/tests/conformance.rs` pins the component set at exactly one
entry.

## Rejection Mapping

Expand Down
Loading
Loading