create identity resolver to reduce duplicate lookups

This commit is contained in:
Evan Jarrett
2025-10-26 23:08:03 -05:00
parent 0cf03109be
commit 4cfe6f221d
9 changed files with 104 additions and 135 deletions
+2 -14
View File
@@ -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)
}
+1 -13
View File
@@ -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, "")
+2 -26
View File
@@ -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)
+3 -23
View File
@@ -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)
+82
View File
@@ -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
}
-22
View File
@@ -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
}
+4 -3
View File
@@ -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
+2 -17
View File
@@ -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
+8 -17
View File
@@ -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)
}
}
}