diff --git a/pkg/appview/handlers/api.go b/pkg/appview/handlers/api.go index 5ec0626..8f42d05 100644 --- a/pkg/appview/handlers/api.go +++ b/pkg/appview/handlers/api.go @@ -1,7 +1,6 @@ package handlers import ( - "context" "database/sql" "encoding/json" "errors" @@ -37,7 +36,7 @@ func (h *StarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request repository := chi.URLParam(r, "repository") // Resolve owner's handle to DID - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) if err != nil { slog.Warn("Failed to resolve handle for star", "handle", handle, "error", err) http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) @@ -95,7 +94,7 @@ func (h *UnstarRepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque repository := chi.URLParam(r, "repository") // Resolve owner's handle to DID - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) if err != nil { slog.Warn("Failed to resolve handle for unstar", "handle", handle, "error", err) http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) @@ -156,7 +155,7 @@ func (h *CheckStarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { repository := chi.URLParam(r, "repository") // Resolve owner's handle to DID - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) if err != nil { slog.Warn("Failed to resolve handle for check star", "handle", handle, "error", err) http.Error(w, fmt.Sprintf("Failed to resolve handle: %v", err), http.StatusBadRequest) @@ -200,7 +199,7 @@ func (h *GetStatsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { repository := chi.URLParam(r, "repository") // Resolve owner's handle to DID - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) if err != nil { http.Error(w, "Failed to resolve handle", http.StatusBadRequest) return @@ -231,7 +230,7 @@ func (h *ManifestDetailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request digest := chi.URLParam(r, "digest") // Resolve owner's handle to DID - ownerDID, err := resolveIdentityToDID(r.Context(), h.Directory, handle) + ownerDID, err := atproto.ResolveHandleToDID(r.Context(), handle) if err != nil { http.Error(w, "Failed to resolve handle", http.StatusBadRequest) return @@ -253,9 +252,3 @@ func (h *ManifestDetailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(manifest) } - -// resolveIdentityToDID is a helper function that resolves a handle or DID to a DID -func resolveIdentityToDID(ctx context.Context, directory identity.Directory, identityStr string) (string, error) { - // Resolve to DID via directory (handles both handles and DIDs) - return atproto.ResolveHandleToDID(ctx, identityStr) -} diff --git a/pkg/appview/holdhealth/checker_test.go b/pkg/appview/holdhealth/checker_test.go index 78fbc5c..2107a00 100644 --- a/pkg/appview/holdhealth/checker_test.go +++ b/pkg/appview/holdhealth/checker_test.go @@ -6,6 +6,8 @@ import ( "net/http/httptest" "testing" "time" + + "atcr.io/pkg/atproto" ) func TestNewChecker(t *testing.T) { @@ -317,7 +319,7 @@ func TestNormalizeHoldEndpoint(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := normalizeHoldEndpoint(tt.input) + result := atproto.ResolveHoldDIDFromURL(tt.input) if result != tt.expected { t.Errorf("normalizeHoldEndpoint(%q) = %q, want %q", tt.input, result, tt.expected) } diff --git a/pkg/appview/holdhealth/worker.go b/pkg/appview/holdhealth/worker.go index f65e694..9b2d24c 100644 --- a/pkg/appview/holdhealth/worker.go +++ b/pkg/appview/holdhealth/worker.go @@ -5,9 +5,10 @@ import ( "database/sql" "fmt" "log/slog" - "strings" "sync" "time" + + "atcr.io/pkg/atproto" ) // DBQuerier interface for database queries (allows mocking in tests) @@ -129,7 +130,7 @@ func (w *Worker) refreshAllHolds(ctx context.Context) { for _, endpoint := range endpoints { // Normalize to canonical DID format - normalizedDID := normalizeHoldEndpoint(endpoint) + normalizedDID := atproto.ResolveHoldDIDFromURL(endpoint) // Skip if we've already seen this normalized DID if seen[normalizedDID] { @@ -219,29 +220,3 @@ func (a *DBAdapter) GetUniqueHoldEndpoints() ([]string, error) { return endpoints, nil } - -// normalizeHoldEndpoint converts a hold endpoint (URL or DID) to canonical DID format -// This ensures that different representations of the same hold are deduplicated: -// - http://172.28.0.3:8080 → did:web:172.28.0.3:8080 -// - http://hold01.atcr.io → did:web:hold01.atcr.io -// - https://hold01.atcr.io → did:web:hold01.atcr.io -// - did:web:hold01.atcr.io → did:web:hold01.atcr.io (passthrough) -func normalizeHoldEndpoint(endpoint string) string { - // Strip protocol and trailing slashes - normalized := endpoint - normalized = strings.TrimPrefix(normalized, "http://") - normalized = strings.TrimPrefix(normalized, "https://") - normalized = strings.TrimSuffix(normalized, "/") - - // If already a DID, return as-is - if strings.HasPrefix(endpoint, "did:") { - return endpoint - } - - // Extract hostname (remove path if present) - parts := strings.Split(normalized, "/") - hostname := parts[0] - - // Convert to did:web - return "did:web:" + hostname -} diff --git a/pkg/appview/jetstream/processor.go b/pkg/appview/jetstream/processor.go index 602457d..ad30dc4 100644 --- a/pkg/appview/jetstream/processor.go +++ b/pkg/appview/jetstream/processor.go @@ -25,8 +25,8 @@ type Processor struct { // useCache: true for Worker (live streaming), false for Backfill (batch processing) func NewProcessor(database *sql.DB, useCache bool) *Processor { p := &Processor{ - db: database, - useCache: useCache, + db: database, + useCache: useCache, } if useCache { diff --git a/pkg/appview/ui_test.go b/pkg/appview/ui_test.go index 81a8d3a..aa5a7c8 100644 --- a/pkg/appview/ui_test.go +++ b/pkg/appview/ui_test.go @@ -525,7 +525,6 @@ func TestTemplates(t *testing.T) { // Test that all expected templates are loaded expectedTemplates := []string{ - "base.html", "nav", "repo-card", "repository", diff --git a/pkg/atproto/lexicon.go b/pkg/atproto/lexicon.go index 81a1e8e..af4d8a3 100644 --- a/pkg/atproto/lexicon.go +++ b/pkg/atproto/lexicon.go @@ -406,8 +406,11 @@ func ParseStarRecordKey(rkey string) (ownerDID, repository string, err error) { } // ResolveHoldDIDFromURL converts a hold endpoint URL to a did:web DID -// For did:web holds: https://hold01.atcr.io → did:web:hold01.atcr.io -// If input is already a DID, returns it as-is +// This ensures that different representations of the same hold are deduplicated: +// - http://172.28.0.3:8080 → did:web:172.28.0.3:8080 +// - http://hold01.atcr.io → did:web:hold01.atcr.io +// - https://hold01.atcr.io → did:web:hold01.atcr.io +// - did:web:hold01.atcr.io → did:web:hold01.atcr.io (passthrough) func ResolveHoldDIDFromURL(holdURL string) string { // Handle empty URLs if holdURL == "" { @@ -415,7 +418,7 @@ func ResolveHoldDIDFromURL(holdURL string) string { } // If already a DID, return as-is - if strings.HasPrefix(holdURL, "did:") { + if IsDID(holdURL) { return holdURL } diff --git a/pkg/auth/oauth/client.go b/pkg/auth/oauth/client.go index 4520e5c..caf6925 100644 --- a/pkg/auth/oauth/client.go +++ b/pkg/auth/oauth/client.go @@ -20,7 +20,6 @@ import ( type App struct { clientApp *oauth.ClientApp baseURL string - directory identity.Directory } // NewApp creates a new OAuth app for ATCR with default scopes @@ -32,11 +31,11 @@ func NewApp(baseURL string, store oauth.ClientAuthStore, holdDid string, testMod func NewAppWithScopes(baseURL string, store oauth.ClientAuthStore, scopes []string) (*App, error) { config := NewClientConfigWithScopes(baseURL, scopes) clientApp := oauth.NewClientApp(&config, store) + clientApp.Dir = atproto.GetDirectory() return &App{ clientApp: clientApp, baseURL: baseURL, - directory: atproto.GetDirectory(), }, nil } @@ -102,7 +101,7 @@ func (a *App) GetClientApp() *oauth.ClientApp { // Directory returns the identity directory used by the OAuth app func (a *App) Directory() identity.Directory { - return a.directory + return a.clientApp.Dir } // ClientIDWithScopes generates a client ID with custom scopes diff --git a/pkg/hold/pds/xrpc.go b/pkg/hold/pds/xrpc.go index bcb93f1..774709d 100644 --- a/pkg/hold/pds/xrpc.go +++ b/pkg/hold/pds/xrpc.go @@ -263,7 +263,7 @@ func (h *XRPCHandler) HandleGetProfile(w http.ResponseWriter, r *http.Request) { // Normalize actor to DID actorDID := actor - if !strings.HasPrefix(actor, "did:") { + if !atproto.IsDID(actor) { // It's a handle, resolve to DID expectedHandle := strings.TrimPrefix(h.pds.DID(), "did:web:") if actor == expectedHandle { @@ -306,7 +306,7 @@ func (h *XRPCHandler) HandleGetProfiles(w http.ResponseWriter, r *http.Request) for _, actor := range actors { // Normalize actor to DID actorDID := actor - if !strings.HasPrefix(actor, "did:") { + if !atproto.IsDID(actor) { // It's a handle, check if it matches if actor == expectedHandle { actorDID = h.pds.DID()