Peering: send into a Space's shared inbox (Space as a peering recipient) #244
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Feature request from the #234/#243 discussion. Distinct from #243 — recording why, plus the design questions to answer before building. Not started; parked for scheduling.
Motivation
A Cairn Space is already a group with members (owner-defined) and a shared folder. That makes it the natural Cairn-native equivalent of Nextcloud's
group@domainfederated target: instead of the sender enumerating members and delivering N copies to N personal inboxes, the sender addresses the Space, and one copy lands in the Space's shared inbox where members read it through their existing space membership.This is the clean answer to the #234 privacy problem: no member usernames are ever disclosed to the sender — the group is an opaque destination the receiver resolves internally.
Why this is separate from #243
/home/<member>/Inbox/Grounding: two Space backends already exist
internal/spacestore, ADR 0001):{name, owner, members[]}age-encrypted in the state-backend, no IdP, works under every auth mode + key custody. This is what makes openbao/app-owned instances able to group users at all.internal/spaces,Manager): a pair of Keycloak groups (<name>/<name>-ro) + owner attribute; membership in Keycloak;Members()via the group-members API.Both expose owner + members + roles (write/read). A space-as-recipient feature sits on top of whichever backend the instance uses.
Design questions to resolve first
Receivers.Spaces, or a marker on the existing list). The peer's admin decides which Spaces may receive from a given peer, same shape as the user/group allow-lists./Spaces/<name>/Inbox/<peer>/) re-encrypted through the normal stack, so quota/audit/trash/retention/holds apply with no new code (same principle as #104/#237 personal delivery). Confirm the space folder path convention (#64 scope).CanReceive-equivalent for spaces: the Space must be allow-listed for this peer AND exist on this instance. No member-level check needed at delivery (delivery is to the folder, not a person), which is what makes it privacy-clean.Relationship to existing issues
Milestone: v0.7.
Refs: #234, #243, #104, #237, #64, #211, #226;
internal/spacestore,internal/spaces.Design Q3 (shared-space encryption) — RESOLVED, no prerequisite. Verified against
internal/storage/encrypt.Shared spaces already encrypt to a per-space identity, not to each member's key and not by fan-out. The encrypt driver derives the group scope from the path (
GroupFromPath("/spaces/<name>/…")→WithGroup), and the KeyProvider resolves the space's age identity:keysFor→groupKeysFor), mirror of per-user..../groups/<space>(keysFor:rel = "groups/"+g), mirror of per-user.So the crypto exists in every custody mode. Two consequences that make delivery trivial:
/spaces/<name>/Inbox/<peer>/auto-encrypts to the space key.group.go: join = full history, leave = revoked, no re-encryption).Revised Q3 verdict: not a prerequisite and not load-bearing after all — the shared-space encrypt path carries it. The feature reduces to: address a Space as a recipient, allow-list it per peer, and write into its inbox path.
Minor corner to confirm at build time (not a blocker): app-owned spaces (
spacestore) pair with openbao/static custody (group key in KV / deployment key). On a keycloak-custody instance the group-key path expects a real Keycloak group, so an app-owned space there would need that path checked — unusual combination, orthogonal to the feature.Design confirmed — build-ready spec
All six questions resolved (Q3/Q4 by the encryption finding above; Q1/Q2/Q5/Q6 ratified). Recording the final shape.
Confirmed decisions
Receivers.Spacesallow-list on the peer entry, same shape/UX as the user/group rows./spaces/<name>/Inbox/<peer>/, re-encrypted through the normal stack (quota/audit/trash/retention/holds apply free).group.gobehavior).CanReceive-equivalent for spaces: the Space is allow-listed for this peer AND exists on this instance. No member-level check — a space existing implies its membership already exists and is managed locally.ListReceiverSpaces-style RPC. Space names the admin allow-listed for this peer are not member PII, so this is safe to surface.Work breakdown
Receivers.Spaces []stringonpeering.Peer;ReceiverSpaces(peer)accessor;CanReceiveSpace(peer, space)= allow-listed ∩ exists. Peering-card UI to edit the space allow-list (mirrors the send/recv rows).Spaces.Exists(name) boolresolver (spacestore for app-owned,spaces.Managerfor IdP-group), wired like theRecipientsinterface.PrepareTransfer(user vs space); aListReceiverSpacesRPC returning allow-listed spaces ∩ exist (one audited event per call, likeListRecipients).CanReceiveSpacefor spaces, existingRecipients.Exists/CanReceivefor users.deliver.gowrites into/spaces/<name>/Inbox/<peer>/<file>; the encrypt driver's path-derived group scope handles encryption to the space key automatically (no recipient-user injection needed). Confirm the inbox path against the #64 scope convention.Receivers.Spaceseditor.Design is complete; ready to implement when scheduled.
Done end to end and dogfood-verified (v0.6.26–v0.6.29):
Receivers.Spaces,CanReceiveSpace,ReceiverSpaces) + admin API (v0.6.26); protorecipient_kind+ListReceiverSpacesRPC, PrepareTransfer space/user dispatch, delivery into/spaces/<name>/Inbox/<peer>/reusing the space's own path-derived group key,peerSpacesexistence resolver over the app-owned store + IdP-group manager (v0.6.27); Kind threaded through the send path +GET /peering/peers/{peer}/spaces(v0.6.28).recvSpaceseditor (v0.6.28), add-peer form inbound Spaces field (v0.6.29).Closing as complete.