Peering: outbound send pipeline with authorization and status (#103) #126

Merged
Cordy merged 1 commit from feat/peering-send into main 2026-08-06 01:35:53 +00:00
Owner

The sender half of #103: authorize, then move one file to a peer in the background, recording what happened. Builds on the Client from the previous commit.

First change developed on a branch rather than on main, per the working rule added after ci.yml caught main transiently not compiling.

Authorization, all synchronous before Start returns

The caller gets a real error, not a job that fails a second later.

  • CanSend — §4.2's per-peering allow-list of users and IdP groups. Being logged in is not enough, and the global userSendEnabled toggle the issue originally proposed is superseded.
  • Legal holds — decided 2026-08-06. A held file cannot be sent. Worth stating why this needed writing at all: holds block delete, overwrite and rename but not reads, and sending is a read. Without an explicit check, a legal hold would be trivially defeated by peering the file to another instance — the opposite of what a hold is for.
  • Space write-membership — §4.2 requires it, since a read-only member reading a file is fine but sending it off-instance is exfiltration. Writable is a hook, and nil means space content cannot be sent at all: failing closed beats guessing when the wiring is incomplete.
  • Folders refused — files only in v1, mirroring the cross-scope rename limitation.

Status store

Transfers is atomic JSON, the same pattern as the registry and share store — no database, per the standing rule.

On open, anything still pending or streaming becomes failed with "interrupted by a restart; re-send to retry". v1 has no mid-flight resume, so a transfer that was in flight when the process died is not alive and must not look it. ForUser scopes the list to its owner: who sent what to whom is nobody else's business.

One cost worth naming

The receiver is told the plaintext hash before any bytes move, so it has to be computed up front — which means reading the file twice, once to hash and once to stream. The alternative is sending no hash at all, which would leave integrity to the AEAD alone; that detects corruption in transit but cannot detect the sender encrypting the wrong file. The double read is the honest price of declaring integrity up front, and it is documented in the code rather than left as a surprise.

Not in this PR

The HTTP surface — POST /api/v1/peering/send and GET /api/v1/peering/transfers — and the cmd/cairnd wiring that supplies Stat, Open, Held and Writable from the real stores. The pipeline underneath them is complete and tested.

10 tests, gofmt, vet, full suite and go build ./... all clean.

The sender half of #103: authorize, then move one file to a peer in the background, recording what happened. Builds on the `Client` from the previous commit. First change developed on a branch rather than on `main`, per the working rule added after `ci.yml` caught `main` transiently not compiling. ## Authorization, all synchronous before `Start` returns The caller gets a real error, not a job that fails a second later. - **`CanSend`** — §4.2's per-peering allow-list of users and IdP groups. Being logged in is not enough, and the global `userSendEnabled` toggle the issue originally proposed is superseded. - **Legal holds** — decided 2026-08-06. A held file cannot be sent. Worth stating why this needed writing at all: holds block delete, overwrite and rename but **not reads**, and sending is a read. Without an explicit check, a legal hold would be trivially defeated by peering the file to another instance — the opposite of what a hold is for. - **Space write-membership** — §4.2 requires it, since a read-only member reading a file is fine but sending it off-instance is exfiltration. `Writable` is a hook, and **nil means space content cannot be sent at all**: failing closed beats guessing when the wiring is incomplete. - **Folders refused** — files only in v1, mirroring the cross-scope rename limitation. ## Status store `Transfers` is atomic JSON, the same pattern as the registry and share store — no database, per the standing rule. **On open, anything still `pending` or `streaming` becomes `failed`** with "interrupted by a restart; re-send to retry". v1 has no mid-flight resume, so a transfer that was in flight when the process died is not alive and must not look it. `ForUser` scopes the list to its owner: who sent what to whom is nobody else's business. ## One cost worth naming The receiver is told the **plaintext** hash before any bytes move, so it has to be computed up front — which means reading the file twice, once to hash and once to stream. The alternative is sending no hash at all, which would leave integrity to the AEAD alone; that detects corruption in transit but cannot detect the sender encrypting the wrong file. The double read is the honest price of declaring integrity up front, and it is documented in the code rather than left as a surprise. ## Not in this PR The HTTP surface — `POST /api/v1/peering/send` and `GET /api/v1/peering/transfers` — and the `cmd/cairnd` wiring that supplies `Stat`, `Open`, `Held` and `Writable` from the real stores. The pipeline underneath them is complete and tested. 10 tests, `gofmt`, `vet`, full suite and `go build ./...` all clean.
feat(peering): outbound send pipeline with authorization and status (#103)
All checks were successful
ci / test-and-build (pull_request) Successful in 32s
f30c62a22e
Cordy merged commit b36cf8dcf0 into main 2026-08-06 01:35:53 +00:00
Cordy deleted branch feat/peering-send 2026-08-06 01:35:56 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#126
No description provided.