···405405Standard ATProto sync endpoints:
406406- `GET /xrpc/com.atproto.sync.getRepo?did={did}` - Download full repository as CAR file
407407- `GET /xrpc/com.atproto.sync.getRepo?did={did}&since={rev}` - Download repository diff since revision
408408+- `GET /xrpc/com.atproto.sync.getRepoStatus?did={did}` - Get repository hosting status and current revision
408409- `GET /xrpc/com.atproto.sync.subscribeRepos` - WebSocket firehose for real-time events
409410- `GET /xrpc/com.atproto.sync.listRepos` - List all repositories (single-user PDS)
410411- `GET /xrpc/com.atproto.sync.getBlob?did={did}&cid={digest}` - Get blob or presigned download URL
+6
pkg/atproto/endpoints.go
···9898 // Response: Stream of #commit events
9999 SyncSubscribeRepos = "/xrpc/com.atproto.sync.subscribeRepos"
100100101101+ // SyncGetRepoStatus gets the hosting status for a repository.
102102+ // Method: GET
103103+ // Query: did={did}
104104+ // Response: {"did": "...", "active": true, "rev": "..."}
105105+ SyncGetRepoStatus = "/xrpc/com.atproto.sync.getRepoStatus"
106106+101107 // SyncRequestCrawl requests a relay to crawl a PDS.
102108 // Method: POST
103109 // Request: {"hostname": "hold01.atcr.io"}
+42
pkg/hold/pds/xrpc.go
···160160 r.Get(atproto.SyncListRepos, h.HandleListRepos)
161161 r.Get(atproto.SyncGetRecord, h.HandleSyncGetRecord)
162162 r.Get(atproto.SyncGetRepo, h.HandleGetRepo)
163163+ r.Get(atproto.SyncGetRepoStatus, h.HandleGetRepoStatus)
163164 r.Get(atproto.SyncSubscribeRepos, h.HandleSubscribeRepos)
164165165166 // DID document and handle resolution
···1100110111011102 response := map[string]any{
11021103 "repos": repos,
11041104+ }
11051105+11061106+ w.Header().Set("Content-Type", "application/json")
11071107+ json.NewEncoder(w).Encode(response)
11081108+}
11091109+11101110+// HandleGetRepoStatus returns the hosting status for a repository
11111111+// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
11121112+func (h *XRPCHandler) HandleGetRepoStatus(w http.ResponseWriter, r *http.Request) {
11131113+ // Get required 'did' parameter
11141114+ did := r.URL.Query().Get("did")
11151115+ if did == "" {
11161116+ http.Error(w, "missing required parameter: did", http.StatusBadRequest)
11171117+ return
11181118+ }
11191119+11201120+ // Validate DID matches this PDS (single-user PDS only hosts one repo)
11211121+ if did != h.pds.DID() {
11221122+ http.Error(w, "repo not found", http.StatusNotFound)
11231123+ return
11241124+ }
11251125+11261126+ // Get current repo revision to verify repo is initialized
11271127+ rev, err := h.pds.repomgr.GetRepoRev(r.Context(), h.pds.uid)
11281128+ if err != nil || rev == "" {
11291129+ // Repo exists (DID matches) but no commits yet
11301130+ // Per ATProto spec, return active=true even if empty
11311131+ response := map[string]any{
11321132+ "did": did,
11331133+ "active": true,
11341134+ }
11351135+ w.Header().Set("Content-Type", "application/json")
11361136+ json.NewEncoder(w).Encode(response)
11371137+ return
11381138+ }
11391139+11401140+ // Return status with revision
11411141+ response := map[string]any{
11421142+ "did": did,
11431143+ "active": true,
11441144+ "rev": rev,
11031145 }
1104114611051147 w.Header().Set("Content-Type", "application/json")
+105
pkg/hold/pds/xrpc_test.go
···981981 t.Skip("Method validation is now handled by chi router, not individual handlers")
982982}
983983984984+// Tests for HandleGetRepoStatus
985985+986986+// TestHandleGetRepoStatus tests com.atproto.sync.getRepoStatus with a valid DID
987987+// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
988988+func TestHandleGetRepoStatus(t *testing.T) {
989989+ handler, _ := setupTestXRPCHandler(t)
990990+ holdDID := "did:web:hold.example.com"
991991+992992+ req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{
993993+ "did": holdDID,
994994+ })
995995+ w := httptest.NewRecorder()
996996+997997+ handler.HandleGetRepoStatus(w, req)
998998+999999+ result := assertJSONResponse(t, w, http.StatusOK)
10001000+10011001+ // Verify required fields per spec
10021002+ if did, ok := result["did"].(string); !ok || did != holdDID {
10031003+ t.Errorf("Expected did=%s, got %v", holdDID, result["did"])
10041004+ }
10051005+10061006+ if active, ok := result["active"].(bool); !ok {
10071007+ t.Error("Expected active boolean")
10081008+ } else if !active {
10091009+ t.Error("Expected active to be true")
10101010+ }
10111011+10121012+ // rev is optional but should be present for initialized repo
10131013+ if rev, ok := result["rev"].(string); ok && rev == "" {
10141014+ t.Error("Expected non-empty rev string when present")
10151015+ }
10161016+}
10171017+10181018+// TestHandleGetRepoStatus_EmptyRepo tests getRepoStatus with no commits
10191019+// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
10201020+func TestHandleGetRepoStatus_EmptyRepo(t *testing.T) {
10211021+ pds, ctx := setupTestPDS(t) // Don't bootstrap
10221022+ mockClient := &mockPDSClient{}
10231023+ mockS3 := s3.S3Service{}
10241024+ handler := NewXRPCHandler(pds, mockS3, nil, nil, mockClient)
10251025+ holdDID := "did:web:hold.example.com"
10261026+10271027+ // Initialize repo but don't add any records
10281028+ err := pds.repomgr.InitNewActor(ctx, pds.uid, "", pds.did, "", "", "")
10291029+ if err != nil {
10301030+ t.Fatalf("Failed to initialize repo: %v", err)
10311031+ }
10321032+10331033+ req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{
10341034+ "did": holdDID,
10351035+ })
10361036+ w := httptest.NewRecorder()
10371037+10381038+ handler.HandleGetRepoStatus(w, req)
10391039+10401040+ result := assertJSONResponse(t, w, http.StatusOK)
10411041+10421042+ // Even with no commits, repo is active
10431043+ if did, ok := result["did"].(string); !ok || did != holdDID {
10441044+ t.Errorf("Expected did=%s, got %v", holdDID, result["did"])
10451045+ }
10461046+10471047+ if active, ok := result["active"].(bool); !ok || !active {
10481048+ t.Error("Expected active=true even for empty repo")
10491049+ }
10501050+10511051+ // rev may not be present for empty repo (no commits)
10521052+ if rev, ok := result["rev"].(string); ok && rev != "" {
10531053+ t.Logf("Note: Empty repo has rev=%s (acceptable)", rev)
10541054+ }
10551055+}
10561056+10571057+// TestHandleGetRepoStatus_MissingDID tests missing did parameter
10581058+// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
10591059+func TestHandleGetRepoStatus_MissingDID(t *testing.T) {
10601060+ handler, _ := setupTestXRPCHandler(t)
10611061+10621062+ req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, nil)
10631063+ w := httptest.NewRecorder()
10641064+10651065+ handler.HandleGetRepoStatus(w, req)
10661066+10671067+ if w.Code != http.StatusBadRequest {
10681068+ t.Errorf("Expected status 400, got %d", w.Code)
10691069+ }
10701070+}
10711071+10721072+// TestHandleGetRepoStatus_InvalidDID tests invalid DID
10731073+// Spec: https://docs.bsky.app/docs/api/com-atproto-sync-get-repo-status
10741074+func TestHandleGetRepoStatus_InvalidDID(t *testing.T) {
10751075+ handler, _ := setupTestXRPCHandler(t)
10761076+10771077+ req := makeXRPCGetRequest(atproto.SyncGetRepoStatus, map[string]string{
10781078+ "did": "did:plc:wrongdid",
10791079+ })
10801080+ w := httptest.NewRecorder()
10811081+10821082+ handler.HandleGetRepoStatus(w, req)
10831083+10841084+ if w.Code != http.StatusNotFound {
10851085+ t.Errorf("Expected status 404, got %d", w.Code)
10861086+ }
10871087+}
10881088+9841089// Tests for HandleSyncGetRecord
98510909861091// TestHandleSyncGetRecord tests com.atproto.sync.getRecord