+1
CLAUDE.md
+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
+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
+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
+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