From 4cfe6f221df4b287b0f62893c5172ca97395e5dd Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Sun, 26 Oct 2025 23:08:03 -0500 Subject: [PATCH] create identity resolver to reduce duplicate lookups --- pkg/appview/handlers/api.go | 16 +----- pkg/appview/jetstream/backfill.go | 14 +---- pkg/appview/jetstream/processor.go | 28 +--------- pkg/appview/middleware/registry.go | 26 ++-------- pkg/atproto/resolver.go | 82 ++++++++++++++++++++++++++++++ pkg/auth/oauth/client.go | 22 -------- pkg/auth/oauth/server.go | 7 +-- pkg/auth/session.go | 19 +------ pkg/auth/token/handler.go | 25 +++------ 9 files changed, 104 insertions(+), 135 deletions(-) create mode 100644 pkg/atproto/resolver.go diff --git a/pkg/appview/handlers/api.go b/pkg/appview/handlers/api.go index 7d81f30..5ec0626 100644 --- a/pkg/appview/handlers/api.go +++ b/pkg/appview/handlers/api.go @@ -14,7 +14,6 @@ import ( "atcr.io/pkg/atproto" "atcr.io/pkg/auth/oauth" "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/atproto/syntax" "github.com/go-chi/chi/v5" ) @@ -257,17 +256,6 @@ func (h *ManifestDetailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request // 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) { - // Parse as AT identifier (handle or DID) - atID, err := syntax.ParseAtIdentifier(identityStr) - if err != nil { - return "", err - } - - // Resolve to DID via directory - ident, err := directory.Lookup(ctx, *atID) - if err != nil { - return "", err - } - - return ident.DID.String(), nil + // Resolve to DID via directory (handles both handles and DIDs) + return atproto.ResolveHandleToDID(ctx, identityStr) } diff --git a/pkg/appview/jetstream/backfill.go b/pkg/appview/jetstream/backfill.go index 7484a5a..1e9c570 100644 --- a/pkg/appview/jetstream/backfill.go +++ b/pkg/appview/jetstream/backfill.go @@ -9,8 +9,6 @@ import ( "strings" "time" - "github.com/bluesky-social/indigo/atproto/syntax" - "atcr.io/pkg/appview/db" "atcr.io/pkg/atproto" ) @@ -137,21 +135,11 @@ func (b *BackfillWorker) backfillRepo(ctx context.Context, did, collection strin } // Resolve DID to get user's PDS endpoint - didParsed, err := syntax.ParseDID(did) - if err != nil { - return 0, fmt.Errorf("invalid DID %s: %w", did, err) - } - - ident, err := b.processor.directory.LookupDID(ctx, didParsed) + pdsEndpoint, err := atproto.ResolveDIDToPDS(ctx, did) if err != nil { return 0, fmt.Errorf("failed to resolve DID to PDS: %w", err) } - pdsEndpoint := ident.PDSEndpoint() - if pdsEndpoint == "" { - return 0, fmt.Errorf("no PDS endpoint found for DID %s", did) - } - // Create a client for this user's PDS with the user's DID // This allows GetRecord to work properly with the repo parameter pdsClient := atproto.NewClient(pdsEndpoint, did, "") diff --git a/pkg/appview/jetstream/processor.go b/pkg/appview/jetstream/processor.go index 9903bd4..602457d 100644 --- a/pkg/appview/jetstream/processor.go +++ b/pkg/appview/jetstream/processor.go @@ -9,9 +9,6 @@ import ( "strings" "time" - "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/atproto/syntax" - "atcr.io/pkg/appview/db" "atcr.io/pkg/atproto" ) @@ -20,7 +17,6 @@ import ( // This eliminates code duplication between the two data ingestion paths type Processor struct { db *sql.DB - directory identity.Directory userCache *UserCache // Optional - enabled for Worker, disabled for Backfill useCache bool } @@ -30,7 +26,6 @@ type Processor struct { func NewProcessor(database *sql.DB, useCache bool) *Processor { p := &Processor{ db: database, - directory: atproto.GetDirectory(), useCache: useCache, } @@ -62,28 +57,9 @@ func (p *Processor) EnsureUser(ctx context.Context, did string) error { } // Resolve DID to get handle and PDS endpoint - didParsed, err := syntax.ParseDID(did) + resolvedDID, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, did) if err != nil { - return fmt.Errorf("failed to parse DID: %w", err) - } - - ident, err := p.directory.LookupDID(ctx, didParsed) - if err != nil { - return fmt.Errorf("failed to lookup DID: %w", err) - } - - resolvedDID := ident.DID.String() - handle := ident.Handle.String() - pdsEndpoint := ident.PDSEndpoint() - - // If handle is invalid, use DID as display name - if handle == "handle.invalid" || handle == "" { - handle = resolvedDID - } - - // PDS endpoint is required - we can't make XRPC calls without it - if pdsEndpoint == "" { - return fmt.Errorf("no PDS endpoint found for DID: %s", resolvedDID) + return err } // Fetch user's Bluesky profile record from their PDS (including avatar) diff --git a/pkg/appview/middleware/registry.go b/pkg/appview/middleware/registry.go index 3808446..c1ce94f 100644 --- a/pkg/appview/middleware/registry.go +++ b/pkg/appview/middleware/registry.go @@ -8,8 +8,6 @@ import ( "strings" "sync" - "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/atproto/syntax" "github.com/distribution/distribution/v3" "github.com/distribution/distribution/v3/registry/api/errcode" registrymw "github.com/distribution/distribution/v3/registry/middleware/registry" @@ -68,7 +66,6 @@ func init() { // NamespaceResolver wraps a namespace and resolves names type NamespaceResolver struct { distribution.Namespace - directory identity.Directory defaultHoldDID string // Default hold DID (e.g., "did:web:hold01.atcr.io") baseURL string // Base URL for error messages (e.g., "https://atcr.io") testMode bool // If true, fallback to default hold when user's hold is unreachable @@ -81,9 +78,6 @@ type NamespaceResolver struct { // initATProtoResolver initializes the name resolution middleware func initATProtoResolver(ctx context.Context, ns distribution.Namespace, _ driver.StorageDriver, options map[string]any) (distribution.Namespace, error) { - // Use shared directory with 8h cache TTL - directory := atproto.GetDirectory() - // Get default hold DID from config (required) // Expected format: "did:web:hold01.atcr.io" defaultHoldDID := "" @@ -107,7 +101,6 @@ func initATProtoResolver(ctx context.Context, ns distribution.Namespace, _ drive // This avoids accessing globals during request handling return &NamespaceResolver{ Namespace: ns, - directory: directory, defaultHoldDID: defaultHoldDID, baseURL: baseURL, testMode: testMode, @@ -142,23 +135,10 @@ func (nr *NamespaceResolver) Repository(ctx context.Context, name reference.Name identityStr := parts[0] imageName := parts[1] - // Parse identity (handle or DID) - atID, err := syntax.ParseAtIdentifier(identityStr) + // Resolve identity to DID, handle, and PDS endpoint + did, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, identityStr) if err != nil { - return nil, fmt.Errorf("invalid identity %s: %w", identityStr, err) - } - - // Resolve identity to DID and PDS using indigo's directory - ident, err := nr.directory.Lookup(ctx, *atID) - if err != nil { - return nil, fmt.Errorf("failed to resolve identity %s: %w", identityStr, err) - } - - did := ident.DID.String() - handle := ident.Handle.String() - pdsEndpoint := ident.PDSEndpoint() - if pdsEndpoint == "" { - return nil, fmt.Errorf("no PDS endpoint found for %s", identityStr) + return nil, err } slog.Debug("Resolved identity", "component", "registry/middleware", "did", did, "pds", pdsEndpoint, "handle", handle) diff --git a/pkg/atproto/resolver.go b/pkg/atproto/resolver.go new file mode 100644 index 0000000..a984037 --- /dev/null +++ b/pkg/atproto/resolver.go @@ -0,0 +1,82 @@ +package atproto + +import ( + "context" + "fmt" + + "github.com/bluesky-social/indigo/atproto/syntax" +) + +// ResolveDIDToPDS resolves a DID to its PDS endpoint. +// Uses the shared identity directory with 8h cache TTL. +func ResolveDIDToPDS(ctx context.Context, did string) (string, error) { + directory := GetDirectory() + didParsed, err := syntax.ParseDID(did) + if err != nil { + return "", fmt.Errorf("invalid DID: %w", err) + } + + ident, err := directory.LookupDID(ctx, didParsed) + if err != nil { + return "", fmt.Errorf("failed to resolve DID: %w", err) + } + + pdsEndpoint := ident.PDSEndpoint() + if pdsEndpoint == "" { + return "", fmt.Errorf("no PDS endpoint found for DID") + } + + return pdsEndpoint, nil +} + +// ResolveIdentity resolves an ATProto identifier (handle or DID) to DID, handle, and PDS endpoint. +// Uses the shared identity directory with 8h cache TTL. +// +// If the handle is invalid (handle.invalid), it returns the DID as the handle for display purposes. +// Returns: did, handle, pdsEndpoint, error +func ResolveIdentity(ctx context.Context, identifier string) (string, string, string, error) { + directory := GetDirectory() + atID, err := syntax.ParseAtIdentifier(identifier) + if err != nil { + return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err) + } + + ident, err := directory.Lookup(ctx, *atID) + if err != nil { + return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err) + } + + did := ident.DID.String() + handle := ident.Handle.String() + pdsEndpoint := ident.PDSEndpoint() + + // If handle is invalid, use DID as display name + if handle == "handle.invalid" || handle == "" { + handle = did + } + + // PDS endpoint is required for XRPC calls + if pdsEndpoint == "" { + return "", "", "", fmt.Errorf("no PDS endpoint found for identifier %q", identifier) + } + + return did, handle, pdsEndpoint, nil +} + +// ResolveHandleToDID resolves a handle or DID to just the DID. +// Uses the shared identity directory with 8h cache TTL. +// This is useful when you only need the DID and don't care about handle/PDS. +func ResolveHandleToDID(ctx context.Context, identifier string) (string, error) { + directory := GetDirectory() + atID, err := syntax.ParseAtIdentifier(identifier) + if err != nil { + return "", fmt.Errorf("invalid identifier: %w", err) + } + + ident, err := directory.Lookup(ctx, *atID) + if err != nil { + return "", err + } + + return ident.DID.String(), nil +} diff --git a/pkg/auth/oauth/client.go b/pkg/auth/oauth/client.go index a951770..4520e5c 100644 --- a/pkg/auth/oauth/client.go +++ b/pkg/auth/oauth/client.go @@ -177,25 +177,3 @@ func ScopesMatch(stored, desired []string) bool { return true } - -// ResolveDIDToPDS resolves a DID to its PDS endpoint (for reference) -// This is an alternative approach if we don't trust the token's issuer claim -func ResolveDIDToPDS(ctx context.Context, did string) (string, error) { - directory := atproto.GetDirectory() - didParsed, err := syntax.ParseDID(did) - if err != nil { - return "", fmt.Errorf("invalid DID: %w", err) - } - - ident, err := directory.LookupDID(ctx, didParsed) - if err != nil { - return "", fmt.Errorf("failed to resolve DID: %w", err) - } - - pdsEndpoint := ident.PDSEndpoint() - if pdsEndpoint == "" { - return "", fmt.Errorf("no PDS endpoint found for DID") - } - - return pdsEndpoint, nil -} diff --git a/pkg/auth/oauth/server.go b/pkg/auth/oauth/server.go index bd60101..5c4f8c1 100644 --- a/pkg/auth/oauth/server.go +++ b/pkg/auth/oauth/server.go @@ -8,6 +8,8 @@ import ( "net/http" "strings" "time" + + "atcr.io/pkg/atproto" ) // UISessionStore is the interface for UI session management @@ -126,9 +128,8 @@ func (s *Server) ServeCallback(w http.ResponseWriter, r *http.Request) { slog.Debug("Invalidated cached session after creating new session", "did", did) } - // Look up identity - ident, err := s.app.directory.LookupDID(r.Context(), sessionData.AccountDID) - handle := ident.Handle.String() + // Look up identity (resolve DID to handle) + _, handle, _, err := atproto.ResolveIdentity(r.Context(), did) if err != nil { slog.Warn("Failed to resolve DID to handle, using DID as fallback", "error", err, "did", did) handle = did // Fallback to DID if resolution fails diff --git a/pkg/auth/session.go b/pkg/auth/session.go index 6ca7595..70283ea 100644 --- a/pkg/auth/session.go +++ b/pkg/auth/session.go @@ -17,9 +17,6 @@ import ( "time" "atcr.io/pkg/atproto" - - "github.com/bluesky-social/indigo/atproto/identity" - "github.com/bluesky-social/indigo/atproto/syntax" ) // CachedSession represents a cached session @@ -33,7 +30,6 @@ type CachedSession struct { // SessionValidator validates ATProto credentials type SessionValidator struct { - directory identity.Directory httpClient *http.Client cache map[string]*CachedSession cacheMu sync.RWMutex @@ -42,7 +38,6 @@ type SessionValidator struct { // NewSessionValidator creates a new ATProto session validator func NewSessionValidator() *SessionValidator { return &SessionValidator{ - directory: atproto.GetDirectory(), httpClient: &http.Client{}, cache: make(map[string]*CachedSession), } @@ -102,19 +97,9 @@ func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identif slog.Debug("No cached session, creating new session", "identifier", identifier) // Resolve identifier to PDS endpoint - atID, err := syntax.ParseAtIdentifier(identifier) + _, _, pds, err := atproto.ResolveIdentity(ctx, identifier) if err != nil { - return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err) - } - - ident, err := v.directory.Lookup(ctx, *atID) - if err != nil { - return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err) - } - - pds := ident.PDSEndpoint() - if pds == "" { - return "", "", "", fmt.Errorf("no PDS endpoint found for %q", identifier) + return "", "", "", err } // Create session diff --git a/pkg/auth/token/handler.go b/pkg/auth/token/handler.go index b7d324e..cdee101 100644 --- a/pkg/auth/token/handler.go +++ b/pkg/auth/token/handler.go @@ -9,8 +9,6 @@ import ( "strings" "time" - "github.com/bluesky-social/indigo/atproto/syntax" - "atcr.io/pkg/appview/db" "atcr.io/pkg/atproto" "atcr.io/pkg/auth" @@ -158,21 +156,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Call post-auth callback for AppView business logic (profile management, etc.) if h.postAuthCallback != nil { // Resolve PDS endpoint for callback - directory := atproto.GetDirectory() - atID, err := syntax.ParseAtIdentifier(username) - if err == nil { - ident, err := directory.Lookup(r.Context(), *atID) - if err != nil { - // Log error but don't fail auth - profile management is not critical - slog.Warn("Failed to resolve PDS for callback", "error", err, "username", username) - } else { - pdsEndpoint := ident.PDSEndpoint() - if pdsEndpoint != "" { - if err := h.postAuthCallback(r.Context(), did, handle, pdsEndpoint, accessToken); err != nil { - // Log error but don't fail auth - business logic is non-critical - slog.Warn("Post-auth callback failed", "error", err, "did", did) - } - } + _, _, pdsEndpoint, err := atproto.ResolveIdentity(r.Context(), username) + if err != nil { + // Log error but don't fail auth - profile management is not critical + slog.Warn("Failed to resolve PDS for callback", "error", err, "username", username) + } else { + if err := h.postAuthCallback(r.Context(), did, handle, pdsEndpoint, accessToken); err != nil { + // Log error but don't fail auth - business logic is non-critical + slog.Warn("Post-auth callback failed", "error", err, "did", did) } } }