A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go

fix pagination on crew record check

evan.jarrett.net 482d921c c80b5b29

verified
Changed files
+56 -39
pkg
+56 -39
pkg/auth/hold_remote.go
··· 324 } 325 326 // isCrewMemberNoCache queries XRPC without caching (internal helper) 327 func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID, userDID string) (bool, error) { 328 // Resolve DID to URL 329 holdURL := atproto.ResolveHoldURL(holdDID) 330 331 - // Build XRPC request URL 332 - // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew 333 - xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s", 334 - holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) 335 336 - req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) 337 - if err != nil { 338 - return false, err 339 - } 340 341 - resp, err := a.httpClient.Do(req) 342 - if err != nil { 343 - return false, fmt.Errorf("XRPC request failed: %w", err) 344 - } 345 - defer resp.Body.Close() 346 347 - if resp.StatusCode != http.StatusOK { 348 - body, _ := io.ReadAll(resp.Body) 349 - return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) 350 - } 351 352 - // Parse response 353 - var xrpcResp struct { 354 - Records []struct { 355 - URI string `json:"uri"` 356 - CID string `json:"cid"` 357 - Value struct { 358 - Type string `json:"$type"` 359 - Member string `json:"member"` 360 - Role string `json:"role"` 361 - Permissions []string `json:"permissions"` 362 - AddedAt string `json:"addedAt"` 363 - } `json:"value"` 364 - } `json:"records"` 365 - } 366 367 - if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { 368 - return false, fmt.Errorf("failed to decode XRPC response: %w", err) 369 - } 370 371 - // Check if userDID is in the crew list 372 - for _, record := range xrpcResp.Records { 373 - if record.Value.Member == userDID { 374 - // TODO: Check expiration if set 375 - return true, nil 376 } 377 } 378 379 return false, nil
··· 324 } 325 326 // isCrewMemberNoCache queries XRPC without caching (internal helper) 327 + // Handles pagination to check all crew records, not just the first page 328 func (a *RemoteHoldAuthorizer) isCrewMemberNoCache(ctx context.Context, holdDID, userDID string) (bool, error) { 329 // Resolve DID to URL 330 holdURL := atproto.ResolveHoldURL(holdDID) 331 332 + // Paginate through all crew records 333 + cursor := "" 334 + for { 335 + // Build XRPC request URL with pagination 336 + // GET /xrpc/com.atproto.repo.listRecords?repo={did}&collection=io.atcr.hold.crew&limit=100 337 + xrpcURL := fmt.Sprintf("%s%s?repo=%s&collection=%s&limit=100", 338 + holdURL, atproto.RepoListRecords, url.QueryEscape(holdDID), url.QueryEscape(atproto.CrewCollection)) 339 + if cursor != "" { 340 + xrpcURL += "&cursor=" + url.QueryEscape(cursor) 341 + } 342 343 + req, err := http.NewRequestWithContext(ctx, "GET", xrpcURL, nil) 344 + if err != nil { 345 + return false, err 346 + } 347 348 + resp, err := a.httpClient.Do(req) 349 + if err != nil { 350 + return false, fmt.Errorf("XRPC request failed: %w", err) 351 + } 352 353 + if resp.StatusCode != http.StatusOK { 354 + body, _ := io.ReadAll(resp.Body) 355 + resp.Body.Close() 356 + return false, fmt.Errorf("XRPC request failed: status %d: %s", resp.StatusCode, string(body)) 357 + } 358 + 359 + // Parse response 360 + var xrpcResp struct { 361 + Cursor string `json:"cursor"` 362 + Records []struct { 363 + URI string `json:"uri"` 364 + CID string `json:"cid"` 365 + Value struct { 366 + Type string `json:"$type"` 367 + Member string `json:"member"` 368 + Role string `json:"role"` 369 + Permissions []string `json:"permissions"` 370 + AddedAt string `json:"addedAt"` 371 + } `json:"value"` 372 + } `json:"records"` 373 + } 374 375 + if err := json.NewDecoder(resp.Body).Decode(&xrpcResp); err != nil { 376 + resp.Body.Close() 377 + return false, fmt.Errorf("failed to decode XRPC response: %w", err) 378 + } 379 + resp.Body.Close() 380 381 + // Check if userDID is in this page of crew records 382 + for _, record := range xrpcResp.Records { 383 + if record.Value.Member == userDID { 384 + // TODO: Check expiration if set 385 + return true, nil 386 + } 387 + } 388 389 + // Check if there are more pages 390 + if xrpcResp.Cursor == "" || len(xrpcResp.Records) == 0 { 391 + break 392 } 393 + cursor = xrpcResp.Cursor 394 } 395 396 return false, nil