From 482d921cc81dd901cf68887807b43333e19ab4a1 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Tue, 6 Jan 2026 09:29:37 -0600 Subject: [PATCH] fix pagination on crew record check --- pkg/auth/hold_remote.go | 107 +++++++++++++++++++++++----------------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/pkg/auth/hold_remote.go b/pkg/auth/hold_remote.go index e7cc14d..93c3276 100644 --- a/pkg/auth/hold_remote.go +++ b/pkg/auth/hold_remote.go @@ -324,56 +324,73 @@ func (a *RemoteHoldAuthorizer) IsCrewMember(ctx context.Context, holdDID, userDI } // isCrewMemberNoCache queries XRPC without caching (internal helper) +// Handles pagination to check all crew records, not just the first page func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID, userDID string) (bool, error) { // Resolve DID to URL holdURL := atproto.ResolveHoldURL(holdDID) - // Build XRPC request URL - // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew - xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s", - holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) - - req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) - if err != nil { - return false, err - } - - resp, err := a.httpClient.Do(req) - if err != nil { - return false, fmt.Errorf("XRPC request failed: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) - } - - // Parse response - var xrpcResp struct { - Records []struct { - URI string `json:"uri"` - CID string `json:"cid"` - Value struct { - Type string `json:"$type"` - Member string `json:"member"` - Role string `json:"role"` - Permissions []string `json:"permissions"` - AddedAt string `json:"addedAt"` - } `json:"value"` - } `json:"records"` - } - - if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { - return false, fmt.Errorf("failed to decode XRPC response: %w", err) - } - - // Check if userDID is in the crew list - for _, record := range xrpcResp.Records { - if record.Value.Member == userDID { - // TODO: Check expiration if set - return true, nil + // Paginate through all crew records + cursor := "" + for { + // Build XRPC request URL with pagination + // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew&limit=100 + xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s&limit=100", + holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) + if cursor != "" { + xrpcURL += "&cursor=" + url.QueryEscape(cursor) } + + req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) + if err != nil { + return false, err + } + + resp, err := a.httpClient.Do(req) + if err != nil { + return false, fmt.Errorf("XRPC request failed: %w", err) + } + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) + } + + // Parse response + var xrpcResp struct { + Cursor string `json:"cursor"` + Records []struct { + URI string `json:"uri"` + CID string `json:"cid"` + Value struct { + Type string `json:"$type"` + Member string `json:"member"` + Role string `json:"role"` + Permissions []string `json:"permissions"` + AddedAt string `json:"addedAt"` + } `json:"value"` + } `json:"records"` + } + + if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { + resp.Body.Close() + return false, fmt.Errorf("failed to decode XRPC response: %w", err) + } + resp.Body.Close() + + // Check if userDID is in this page of crew records + for _, record := range xrpcResp.Records { + if record.Value.Member == userDID { + // TODO: Check expiration if set + return true, nil + } + } + + // Check if there are more pages + if xrpcResp.Cursor == "" || len(xrpcResp.Records) == 0 { + break + } + cursor = xrpcResp.Cursor } return false, nil