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.

clean up duplicate functions

+23 -52
+5 -12
pkg/appview/handlers/api.go
··· 1 1 package handlers 2 2 3 3 import ( 4 - "context" 5 4 "database/sql" 6 5 "encoding/json" 7 6 "errors" ··· 37 36 repository := chi.URLParam(r, "repository") 38 37 39 38 // Resolve owner's handle to DID 40 - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) 39 + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 41 40 if err != nil { 42 41 slog.Warn("Failed to resolve handle for star", "handle", handle, "error", err) 43 42 http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) ··· 95 94 repository := chi.URLParam(r, "repository") 96 95 97 96 // Resolve owner's handle to DID 98 - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) 97 + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 99 98 if err != nil { 100 99 slog.Warn("Failed to resolve handle for unstar", "handle", handle, "error", err) 101 100 http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) ··· 156 155 repository := chi.URLParam(r, "repository") 157 156 158 157 // Resolve owner's handle to DID 159 - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) 158 + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 160 159 if err != nil { 161 160 slog.Warn("Failed to resolve handle for check star", "handle", handle, "error", err) 162 161 http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) ··· 200 199 repository := chi.URLParam(r, "repository") 201 200 202 201 // Resolve owner's handle to DID 203 - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) 202 + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 204 203 if err != nil { 205 204 http.Error(w, "Failed to resolve handle", http.StatusBadRequest) 206 205 return ··· 231 230 digest := chi.URLParam(r, "digest") 232 231 233 232 // Resolve owner's handle to DID 234 - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) 233 + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) 235 234 if err != nil { 236 235 http.Error(w, "Failed to resolve handle", http.StatusBadRequest) 237 236 return ··· 253 252 w.Header().Set("Content-Type", "application/json") 254 253 json.NewEncoder(w).Encode(manifest) 255 254 } 256 - 257 - // resolveIdentityToDID is a helper function that resolves a handle or DID to a DID 258 - func resolveIdentityToDID(ctx context.Context, directory identity.Directory, identityStr string) (string, error) { 259 - // Resolve to DID via directory (handles both handles and DIDs) 260 - return atproto.ResolveHandleToDID(ctx, identityStr) 261 - }
+3 -1
pkg/appview/holdhealth/checker_test.go
··· 6 6 "net/http/httptest" 7 7 "testing" 8 8 "time" 9 + 10 + "atcr.io/pkg/atproto" 9 11 ) 10 12 11 13 func TestNewChecker(t *testing.T) { ··· 317 319 318 320 for _, tt := range tests { 319 321 t.Run(tt.name, func(t *testing.T) { 320 - result := normalizeHoldEndpoint(tt.input) 322 + result := atproto.ResolveHoldDIDFromURL(tt.input) 321 323 if result != tt.expected { 322 324 t.Errorf("normalizeHoldEndpoint(%q) = %q, want %q", tt.input, result, tt.expected) 323 325 }
+3 -28
pkg/appview/holdhealth/worker.go
··· 5 5 "database/sql" 6 6 "fmt" 7 7 "log/slog" 8 - "strings" 9 8 "sync" 10 9 "time" 10 + 11 + "atcr.io/pkg/atproto" 11 12 ) 12 13 13 14 // DBQuerier interface for database queries (allows mocking in tests) ··· 129 130 130 131 for _, endpoint := range endpoints { 131 132 // Normalize to canonical DID format 132 - normalizedDID := normalizeHoldEndpoint(endpoint) 133 + normalizedDID := atproto.ResolveHoldDIDFromURL(endpoint) 133 134 134 135 // Skip if we've already seen this normalized DID 135 136 if seen[normalizedDID] { ··· 219 220 220 221 return endpoints, nil 221 222 } 222 - 223 - // normalizeHoldEndpoint converts a hold endpoint (URL or DID) to canonical DID format 224 - // This ensures that different representations of the same hold are deduplicated: 225 - // - http://172.28.0.3:8080 → did:web:172.28.0.3:8080 226 - // - http://hold01.atcr.io → did:web:hold01.atcr.io 227 - // - https://hold01.atcr.io → did:web:hold01.atcr.io 228 - // - did:web:hold01.atcr.io → did:web:hold01.atcr.io (passthrough) 229 - func normalizeHoldEndpoint(endpoint string) string { 230 - // Strip protocol and trailing slashes 231 - normalized := endpoint 232 - normalized = strings.TrimPrefix(normalized, "http://") 233 - normalized = strings.TrimPrefix(normalized, "https://") 234 - normalized = strings.TrimSuffix(normalized, "/") 235 - 236 - // If already a DID, return as-is 237 - if strings.HasPrefix(endpoint, "did:") { 238 - return endpoint 239 - } 240 - 241 - // Extract hostname (remove path if present) 242 - parts := strings.Split(normalized, "/") 243 - hostname := parts[0] 244 - 245 - // Convert to did:web 246 - return "did:web:" + hostname 247 - }
+2 -2
pkg/appview/jetstream/processor.go
··· 25 25 // useCache: true for Worker (live streaming), false for Backfill (batch processing) 26 26 func NewProcessor(database *sql.DB, useCache bool) *Processor { 27 27 p := &Processor{ 28 - db: database, 29 - useCache: useCache, 28 + db: database, 29 + useCache: useCache, 30 30 } 31 31 32 32 if useCache {
-1
pkg/appview/ui_test.go
··· 525 525 526 526 // Test that all expected templates are loaded 527 527 expectedTemplates := []string{ 528 - "base.html", 529 528 "nav", 530 529 "repo-card", 531 530 "repository",
+6 -3
pkg/atproto/lexicon.go
··· 406 406 } 407 407 408 408 // ResolveHoldDIDFromURL converts a hold endpoint URL to a did:web DID 409 - // For did:web holds: https://hold01.atcr.io → did:web:hold01.atcr.io 410 - // If input is already a DID, returns it as-is 409 + // This ensures that different representations of the same hold are deduplicated: 410 + // - http://172.28.0.3:8080 → did:web:172.28.0.3:8080 411 + // - http://hold01.atcr.io → did:web:hold01.atcr.io 412 + // - https://hold01.atcr.io → did:web:hold01.atcr.io 413 + // - did:web:hold01.atcr.io → did:web:hold01.atcr.io (passthrough) 411 414 func ResolveHoldDIDFromURL(holdURL string) string { 412 415 // Handle empty URLs 413 416 if holdURL == "" { ··· 415 418 } 416 419 417 420 // If already a DID, return as-is 418 - if strings.HasPrefix(holdURL, "did:") { 421 + if IsDID(holdURL) { 419 422 return holdURL 420 423 } 421 424
+2 -3
pkg/auth/oauth/client.go
··· 20 20 type App struct { 21 21 clientApp *oauth.ClientApp 22 22 baseURL string 23 - directory identity.Directory 24 23 } 25 24 26 25 // NewApp creates a new OAuth app for ATCR with default scopes ··· 32 31 func NewAppWithScopes(baseURL string, store oauth.ClientAuthStore, scopes []string) (*App, error) { 33 32 config := NewClientConfigWithScopes(baseURL, scopes) 34 33 clientApp := oauth.NewClientApp(&config, store) 34 + clientApp.Dir = atproto.GetDirectory() 35 35 36 36 return &App{ 37 37 clientApp: clientApp, 38 38 baseURL: baseURL, 39 - directory: atproto.GetDirectory(), 40 39 }, nil 41 40 } 42 41 ··· 102 101 103 102 // Directory returns the identity directory used by the OAuth app 104 103 func (a *App) Directory() identity.Directory { 105 - return a.directory 104 + return a.clientApp.Dir 106 105 } 107 106 108 107 // ClientIDWithScopes generates a client ID with custom scopes
+2 -2
pkg/hold/pds/xrpc.go
··· 263 263 264 264 // Normalize actor to DID 265 265 actorDID := actor 266 - if !strings.HasPrefix(actor, "did:") { 266 + if !atproto.IsDID(actor) { 267 267 // It's a handle, resolve to DID 268 268 expectedHandle := strings.TrimPrefix(h.pds.DID(), "did:web:") 269 269 if actor == expectedHandle { ··· 306 306 for _, actor := range actors { 307 307 // Normalize actor to DID 308 308 actorDID := actor 309 - if !strings.HasPrefix(actor, "did:") { 309 + if !atproto.IsDID(actor) { 310 310 // It's a handle, check if it matches 311 311 if actor == expectedHandle { 312 312 actorDID = h.pds.DID()