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

implement com.atproto.sync.getRepoStatus

evan.jarrett.net 771cd439 8201d997

verified
Changed files
+154
pkg
atproto
hold
+1
CLAUDE.md
··· 405 405 Standard ATProto sync endpoints: 406 406 - `GET /xrpc/com.atproto.sync.getRepo?did={did}` - Download full repository as CAR file 407 407 - `GET /xrpc/com.atproto.sync.getRepo?did={did}&since={rev}` - Download repository diff since revision 408 + - `GET /xrpc/com.atproto.sync.getRepoStatus?did={did}` - Get repository hosting status and current revision 408 409 - `GET /xrpc/com.atproto.sync.subscribeRepos` - WebSocket firehose for real-time events 409 410 - `GET /xrpc/com.atproto.sync.listRepos` - List all repositories (single-user PDS) 410 411 - `GET /xrpc/com.atproto.sync.getBlob?did={did}&cid={digest}` - Get blob or presigned download URL
+6
pkg/atproto/endpoints.go
··· 98 98 // Response: Stream of #commit events 99 99 SyncSubscribeRepos = "/xrpc/com.atproto.sync.subscribeRepos" 100 100 101 + // SyncGetRepoStatus gets the hosting status for a repository. 102 + // Method: GET 103 + // Query: did={did} 104 + // Response: {"did": "...", "active": true, "rev": "..."} 105 + SyncGetRepoStatus = "/xrpc/com.atproto.sync.getRepoStatus" 106 + 101 107 // SyncRequestCrawl requests a relay to crawl a PDS. 102 108 // Method: POST 103 109 // Request: {"hostname": "hold01.atcr.io"}
+42
pkg/hold/pds/xrpc.go
··· 160 160 r.Get(atproto.SyncListRepos, h.HandleListRepos) 161 161 r.Get(atproto.SyncGetRecord, h.HandleSyncGetRecord) 162 162 r.Get(atproto.SyncGetRepo, h.HandleGetRepo) 163 + r.Get(atproto.SyncGetRepoStatus, h.HandleGetRepoStatus) 163 164 r.Get(atproto.SyncSubscribeRepos, h.HandleSubscribeRepos) 164 165 165 166 // DID document and handle resolution ··· 1100 1101 1101 1102 response := map[string]any{ 1102 1103 "repos": repos, 1104 + } 1105 + 1106 + w.Header().Set("Content-Type", "application/json") 1107 + json.NewEncoder(w).Encode(response) 1108 + } 1109 + 1110 + // HandleGetRepoStatus returns the hosting status for a repository 1111 + // Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status 1112 + func (h *XRPCHandler) HandleGetRepoStatus(w http.ResponseWriter, r *http.Request) { 1113 + // Get required 'did' parameter 1114 + did := r.URL.Query().Get("did") 1115 + if did == "" { 1116 + http.Error(w, "missing required parameter: did", http.StatusBadRequest) 1117 + return 1118 + } 1119 + 1120 + // Validate DID matches this PDS (single-user PDS only hosts one repo) 1121 + if did != h.pds.DID() { 1122 + http.Error(w, "repo not found", http.StatusNotFound) 1123 + return 1124 + } 1125 + 1126 + // Get current repo revision to verify repo is initialized 1127 + rev, err := h.pds.repomgr.GetRepoRev(r.Context(), h.pds.uid) 1128 + if err != nil || rev == "" { 1129 + // Repo exists (DID matches) but no commits yet 1130 + // Per ATProto spec, return active=true even if empty 1131 + response := map[string]any{ 1132 + "did": did, 1133 + "active": true, 1134 + } 1135 + w.Header().Set("Content-Type", "application/json") 1136 + json.NewEncoder(w).Encode(response) 1137 + return 1138 + } 1139 + 1140 + // Return status with revision 1141 + response := map[string]any{ 1142 + "did": did, 1143 + "active": true, 1144 + "rev": rev, 1103 1145 } 1104 1146 1105 1147 w.Header().Set("Content-Type", "application/json")
+105
pkg/hold/pds/xrpc_test.go
··· 981 981 t.Skip("Method validation is now handled by chi router, not individual handlers") 982 982 } 983 983 984 + // Tests for HandleGetRepoStatus 985 + 986 + // TestHandleGetRepoStatus tests com.atproto.sync.getRepoStatus with a valid DID 987 + // Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status 988 + func TestHandleGetRepoStatus(t *testing.T) { 989 + handler, _ := setupTestXRPCHandler(t) 990 + holdDID := "did:web:hold.example.com" 991 + 992 + req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{ 993 + "did": holdDID, 994 + }) 995 + w := httptest.NewRecorder() 996 + 997 + handler.HandleGetRepoStatus(w, req) 998 + 999 + result := assertJSONResponse(t, w, http.StatusOK) 1000 + 1001 + // Verify required fields per spec 1002 + if did, ok := result["did"].(string); !ok || did != holdDID { 1003 + t.Errorf("Expected did=%s, got %v", holdDID, result["did"]) 1004 + } 1005 + 1006 + if active, ok := result["active"].(bool); !ok { 1007 + t.Error("Expected active boolean") 1008 + } else if !active { 1009 + t.Error("Expected active to be true") 1010 + } 1011 + 1012 + // rev is optional but should be present for initialized repo 1013 + if rev, ok := result["rev"].(string); ok && rev == "" { 1014 + t.Error("Expected non-empty rev string when present") 1015 + } 1016 + } 1017 + 1018 + // TestHandleGetRepoStatus_EmptyRepo tests getRepoStatus with no commits 1019 + // Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status 1020 + func TestHandleGetRepoStatus_EmptyRepo(t *testing.T) { 1021 + pds, ctx := setupTestPDS(t) // Don't bootstrap 1022 + mockClient := &mockPDSClient{} 1023 + mockS3 := s3.S3Service{} 1024 + handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient) 1025 + holdDID := "did:web:hold.example.com" 1026 + 1027 + // Initialize repo but don't add any records 1028 + err := pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "") 1029 + if err != nil { 1030 + t.Fatalf("Failed to initialize repo: %v", err) 1031 + } 1032 + 1033 + req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{ 1034 + "did": holdDID, 1035 + }) 1036 + w := httptest.NewRecorder() 1037 + 1038 + handler.HandleGetRepoStatus(w, req) 1039 + 1040 + result := assertJSONResponse(t, w, http.StatusOK) 1041 + 1042 + // Even with no commits, repo is active 1043 + if did, ok := result["did"].(string); !ok || did != holdDID { 1044 + t.Errorf("Expected did=%s, got %v", holdDID, result["did"]) 1045 + } 1046 + 1047 + if active, ok := result["active"].(bool); !ok || !active { 1048 + t.Error("Expected active=true even for empty repo") 1049 + } 1050 + 1051 + // rev may not be present for empty repo (no commits) 1052 + if rev, ok := result["rev"].(string); ok && rev != "" { 1053 + t.Logf("Note: Empty repo has rev=%s (acceptable)", rev) 1054 + } 1055 + } 1056 + 1057 + // TestHandleGetRepoStatus_MissingDID tests missing did parameter 1058 + // Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status 1059 + func TestHandleGetRepoStatus_MissingDID(t *testing.T) { 1060 + handler, _ := setupTestXRPCHandler(t) 1061 + 1062 + req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, nil) 1063 + w := httptest.NewRecorder() 1064 + 1065 + handler.HandleGetRepoStatus(w, req) 1066 + 1067 + if w.Code != http.StatusBadRequest { 1068 + t.Errorf("Expected status 400, got %d", w.Code) 1069 + } 1070 + } 1071 + 1072 + // TestHandleGetRepoStatus_InvalidDID tests invalid DID 1073 + // Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status 1074 + func TestHandleGetRepoStatus_InvalidDID(t *testing.T) { 1075 + handler, _ := setupTestXRPCHandler(t) 1076 + 1077 + req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{ 1078 + "did": "did:plc:wrongdid", 1079 + }) 1080 + w := httptest.NewRecorder() 1081 + 1082 + handler.HandleGetRepoStatus(w, req) 1083 + 1084 + if w.Code != http.StatusNotFound { 1085 + t.Errorf("Expected status 404, got %d", w.Code) 1086 + } 1087 + } 1088 + 984 1089 // Tests for HandleSyncGetRecord 985 1090 986 1091 // TestHandleSyncGetRecord tests com.atproto.sync.getRecord