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

Configure Feed

Select the types of activity you want to include in your feed.

implement com.atproto.sync.getRepoStatus

+154
+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