feat(membership): room discovery — GET /members/{endpoint}/rooms + ListMyRooms
A peer invited to an encrypted room needs to find it: the control plane is
pull-based (no server push of invitations), so add a discovery endpoint that
lists every room an endpoint belongs to, with the room's metadata and the
endpoint's role.
- store.ListRoomsForEndpoint: JOIN members+rooms, ordered by room id, empty
slice (not error) for an endpoint in no rooms.
- membershipd: GET /members/{endpoint}/rooms returns {room_id, subject, epoch,
policy, role}[].
- client.ListMyRooms + RoomRef: a bot polls this to discover and then Join +
Subscribe rooms it was invited to.
Tests: store-level (owner in N rooms, member in one, unknown endpoint → []) and
client-level e2e through the embedded harness (B discovers a room A invited it
to, without prior knowledge of the room id; owner sees role=owner).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ func (s *Server) routes() {
|
||||
s.mux.HandleFunc("POST /rooms/{id}/invite", s.handleInvite)
|
||||
s.mux.HandleFunc("GET /rooms/{id}/key", s.handleGetKey)
|
||||
s.mux.HandleFunc("GET /rooms/{id}/members", s.handleListMembers)
|
||||
s.mux.HandleFunc("GET /members/{endpoint}/rooms", s.handleListMemberRooms)
|
||||
s.mux.HandleFunc("POST /rooms/{id}/rekey", s.handleRekey)
|
||||
s.mux.HandleFunc("GET /rooms/{id}", s.handleGetRoom)
|
||||
s.mux.HandleFunc("POST /blobs", s.handlePutBlob)
|
||||
@@ -101,6 +102,14 @@ type roomResp struct {
|
||||
Policy policyJSON `json:"policy"`
|
||||
}
|
||||
|
||||
type memberRoomJSON struct {
|
||||
RoomID string `json:"room_id"`
|
||||
Subject string `json:"subject"`
|
||||
Epoch int `json:"epoch"`
|
||||
Policy policyJSON `json:"policy"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type rekeyKey struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
SealedKey []byte `json:"sealed_key"`
|
||||
@@ -262,6 +271,30 @@ func (s *Server) handleListMembers(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) handleListMemberRooms(w http.ResponseWriter, r *http.Request) {
|
||||
endpoint := r.PathValue("endpoint")
|
||||
if endpoint == "" {
|
||||
writeErr(w, http.StatusBadRequest, "endpoint required")
|
||||
return
|
||||
}
|
||||
rooms, err := s.store.ListRoomsForEndpoint(endpoint)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
out := make([]memberRoomJSON, 0, len(rooms))
|
||||
for _, rm := range rooms {
|
||||
out = append(out, memberRoomJSON{
|
||||
RoomID: rm.RoomID,
|
||||
Subject: rm.Subject,
|
||||
Epoch: rm.Epoch,
|
||||
Policy: policyJSON{Encrypt: rm.Encrypt, Persist: rm.Persist, SignMsgs: rm.SignMsgs},
|
||||
Role: rm.Role,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetRoom(w http.ResponseWriter, r *http.Request) {
|
||||
roomID := r.PathValue("id")
|
||||
info, err := s.store.GetRoom(roomID)
|
||||
|
||||
Reference in New Issue
Block a user