A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

create identity resolver to reduce duplicate lookups

+104 -135
+2 -14
pkg/appview/handlers/api.go
··· 14 14 "atcr.io/pkg/atproto" 15 15 "atcr.io/pkg/auth/oauth" 16 16 "github.com/bluesky-social/indigo/atproto/identity" 17 - "github.com/bluesky-social/indigo/atproto/syntax" 18 17 "github.com/go-chi/chi/v5" 19 18 ) 20 19 ··· 257 256 258 257 // resolveIdentityToDID is a helper function that resolves a handle or DID to a DID 259 258 func resolveIdentityToDID(ctx context.Context, directory identity.Directory, identityStr string) (string, error) { 260 - // Parse as AT identifier (handle or DID) 261 - atID, err := syntax.ParseAtIdentifier(identityStr) 262 - if err != nil { 263 - return "", err 264 - } 265 - 266 - // Resolve to DID via directory 267 - ident, err := directory.Lookup(ctx, *atID) 268 - if err != nil { 269 - return "", err 270 - } 271 - 272 - return ident.DID.String(), nil 259 + // Resolve to DID via directory (handles both handles and DIDs) 260 + return atproto.ResolveHandleToDID(ctx, identityStr) 273 261 }
+1 -13
pkg/appview/jetstream/backfill.go
··· 9 9 "strings" 10 10 "time" 11 11 12 - "github.com/bluesky-social/indigo/atproto/syntax" 13 - 14 12 "atcr.io/pkg/appview/db" 15 13 "atcr.io/pkg/atproto" 16 14 ) ··· 137 135 } 138 136 139 137 // Resolve DID to get user's PDS endpoint 140 - didParsed, err := syntax.ParseDID(did) 141 - if err != nil { 142 - return 0, fmt.Errorf("invalid DID %s: %w", did, err) 143 - } 144 - 145 - ident, err := b.processor.directory.LookupDID(ctx, didParsed) 138 + pdsEndpoint, err := atproto.ResolveDIDToPDS(ctx, did) 146 139 if err != nil { 147 140 return 0, fmt.Errorf("failed to resolve DID to PDS: %w", err) 148 - } 149 - 150 - pdsEndpoint := ident.PDSEndpoint() 151 - if pdsEndpoint == "" { 152 - return 0, fmt.Errorf("no PDS endpoint found for DID %s", did) 153 141 } 154 142 155 143 // Create a client for this user's PDS with the user's DID
+2 -26
pkg/appview/jetstream/processor.go
··· 9 9 "strings" 10 10 "time" 11 11 12 - "github.com/bluesky-social/indigo/atproto/identity" 13 - "github.com/bluesky-social/indigo/atproto/syntax" 14 - 15 12 "atcr.io/pkg/appview/db" 16 13 "atcr.io/pkg/atproto" 17 14 ) ··· 20 17 // This eliminates code duplication between the two data ingestion paths 21 18 type Processor struct { 22 19 db *sql.DB 23 - directory identity.Directory 24 20 userCache *UserCache // Optional - enabled for Worker, disabled for Backfill 25 21 useCache bool 26 22 } ··· 30 26 func NewProcessor(database *sql.DB, useCache bool) *Processor { 31 27 p := &Processor{ 32 28 db: database, 33 - directory: atproto.GetDirectory(), 34 29 useCache: useCache, 35 30 } 36 31 ··· 62 57 } 63 58 64 59 // Resolve DID to get handle and PDS endpoint 65 - didParsed, err := syntax.ParseDID(did) 60 + resolvedDID, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, did) 66 61 if err != nil { 67 - return fmt.Errorf("failed to parse DID: %w", err) 68 - } 69 - 70 - ident, err := p.directory.LookupDID(ctx, didParsed) 71 - if err != nil { 72 - return fmt.Errorf("failed to lookup DID: %w", err) 73 - } 74 - 75 - resolvedDID := ident.DID.String() 76 - handle := ident.Handle.String() 77 - pdsEndpoint := ident.PDSEndpoint() 78 - 79 - // If handle is invalid, use DID as display name 80 - if handle == "handle.invalid" || handle == "" { 81 - handle = resolvedDID 82 - } 83 - 84 - // PDS endpoint is required - we can't make XRPC calls without it 85 - if pdsEndpoint == "" { 86 - return fmt.Errorf("no PDS endpoint found for DID: %s", resolvedDID) 62 + return err 87 63 } 88 64 89 65 // Fetch user's Bluesky profile record from their PDS (including avatar)
+3 -23
pkg/appview/middleware/registry.go
··· 8 8 "strings" 9 9 "sync" 10 10 11 - "github.com/bluesky-social/indigo/atproto/identity" 12 - "github.com/bluesky-social/indigo/atproto/syntax" 13 11 "github.com/distribution/distribution/v3" 14 12 "github.com/distribution/distribution/v3/registry/api/errcode" 15 13 registrymw "github.com/distribution/distribution/v3/registry/middleware/registry" ··· 68 66 // NamespaceResolver wraps a namespace and resolves names 69 67 type NamespaceResolver struct { 70 68 distribution.Namespace 71 - directory identity.Directory 72 69 defaultHoldDID string // Default hold DID (e.g., "did:web:hold01.atcr.io") 73 70 baseURL string // Base URL for error messages (e.g., "https://atcr.io") 74 71 testMode bool // If true, fallback to default hold when user's hold is unreachable ··· 81 78 82 79 // initATProtoResolver initializes the name resolution middleware 83 80 func initATProtoResolver(ctx context.Context, ns distribution.Namespace, _ driver.StorageDriver, options map[string]any) (distribution.Namespace, error) { 84 - // Use shared directory with 8h cache TTL 85 - directory := atproto.GetDirectory() 86 - 87 81 // Get default hold DID from config (required) 88 82 // Expected format: "did:web:hold01.atcr.io" 89 83 defaultHoldDID := "" ··· 107 101 // This avoids accessing globals during request handling 108 102 return &NamespaceResolver{ 109 103 Namespace: ns, 110 - directory: directory, 111 104 defaultHoldDID: defaultHoldDID, 112 105 baseURL: baseURL, 113 106 testMode: testMode, ··· 142 135 identityStr := parts[0] 143 136 imageName := parts[1] 144 137 145 - // Parse identity (handle or DID) 146 - atID, err := syntax.ParseAtIdentifier(identityStr) 147 - if err != nil { 148 - return nil, fmt.Errorf("invalid identity %s: %w", identityStr, err) 149 - } 150 - 151 - // Resolve identity to DID and PDS using indigo's directory 152 - ident, err := nr.directory.Lookup(ctx, *atID) 138 + // Resolve identity to DID, handle, and PDS endpoint 139 + did, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, identityStr) 153 140 if err != nil { 154 - return nil, fmt.Errorf("failed to resolve identity %s: %w", identityStr, err) 155 - } 156 - 157 - did := ident.DID.String() 158 - handle := ident.Handle.String() 159 - pdsEndpoint := ident.PDSEndpoint() 160 - if pdsEndpoint == "" { 161 - return nil, fmt.Errorf("no PDS endpoint found for %s", identityStr) 141 + return nil, err 162 142 } 163 143 164 144 slog.Debug("Resolved identity", "component", "registry/middleware", "did", did, "pds", pdsEndpoint, "handle", handle)
+82
pkg/atproto/resolver.go
··· 1 + package atproto 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 9 + 10 + // ResolveDIDToPDS resolves a DID to its PDS endpoint. 11 + // Uses the shared identity directory with 8h cache TTL. 12 + func ResolveDIDToPDS(ctx context.Context, did string) (string, error) { 13 + directory := GetDirectory() 14 + didParsed, err := syntax.ParseDID(did) 15 + if err != nil { 16 + return "", fmt.Errorf("invalid DID: %w", err) 17 + } 18 + 19 + ident, err := directory.LookupDID(ctx, didParsed) 20 + if err != nil { 21 + return "", fmt.Errorf("failed to resolve DID: %w", err) 22 + } 23 + 24 + pdsEndpoint := ident.PDSEndpoint() 25 + if pdsEndpoint == "" { 26 + return "", fmt.Errorf("no PDS endpoint found for DID") 27 + } 28 + 29 + return pdsEndpoint, nil 30 + } 31 + 32 + // ResolveIdentity resolves an ATProto identifier (handle or DID) to DID, handle, and PDS endpoint. 33 + // Uses the shared identity directory with 8h cache TTL. 34 + // 35 + // If the handle is invalid (handle.invalid), it returns the DID as the handle for display purposes. 36 + // Returns: did, handle, pdsEndpoint, error 37 + func ResolveIdentity(ctx context.Context, identifier string) (string, string, string, error) { 38 + directory := GetDirectory() 39 + atID, err := syntax.ParseAtIdentifier(identifier) 40 + if err != nil { 41 + return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err) 42 + } 43 + 44 + ident, err := directory.Lookup(ctx, *atID) 45 + if err != nil { 46 + return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err) 47 + } 48 + 49 + did := ident.DID.String() 50 + handle := ident.Handle.String() 51 + pdsEndpoint := ident.PDSEndpoint() 52 + 53 + // If handle is invalid, use DID as display name 54 + if handle == "handle.invalid" || handle == "" { 55 + handle = did 56 + } 57 + 58 + // PDS endpoint is required for XRPC calls 59 + if pdsEndpoint == "" { 60 + return "", "", "", fmt.Errorf("no PDS endpoint found for identifier %q", identifier) 61 + } 62 + 63 + return did, handle, pdsEndpoint, nil 64 + } 65 + 66 + // ResolveHandleToDID resolves a handle or DID to just the DID. 67 + // Uses the shared identity directory with 8h cache TTL. 68 + // This is useful when you only need the DID and don't care about handle/PDS. 69 + func ResolveHandleToDID(ctx context.Context, identifier string) (string, error) { 70 + directory := GetDirectory() 71 + atID, err := syntax.ParseAtIdentifier(identifier) 72 + if err != nil { 73 + return "", fmt.Errorf("invalid identifier: %w", err) 74 + } 75 + 76 + ident, err := directory.Lookup(ctx, *atID) 77 + if err != nil { 78 + return "", err 79 + } 80 + 81 + return ident.DID.String(), nil 82 + }
-22
pkg/auth/oauth/client.go
··· 177 177 178 178 return true 179 179 } 180 - 181 - // ResolveDIDToPDS resolves a DID to its PDS endpoint (for reference) 182 - // This is an alternative approach if we don't trust the token's issuer claim 183 - func ResolveDIDToPDS(ctx context.Context, did string) (string, error) { 184 - directory := atproto.GetDirectory() 185 - didParsed, err := syntax.ParseDID(did) 186 - if err != nil { 187 - return "", fmt.Errorf("invalid DID: %w", err) 188 - } 189 - 190 - ident, err := directory.LookupDID(ctx, didParsed) 191 - if err != nil { 192 - return "", fmt.Errorf("failed to resolve DID: %w", err) 193 - } 194 - 195 - pdsEndpoint := ident.PDSEndpoint() 196 - if pdsEndpoint == "" { 197 - return "", fmt.Errorf("no PDS endpoint found for DID") 198 - } 199 - 200 - return pdsEndpoint, nil 201 - }
+4 -3
pkg/auth/oauth/server.go
··· 8 8 "net/http" 9 9 "strings" 10 10 "time" 11 + 12 + "atcr.io/pkg/atproto" 11 13 ) 12 14 13 15 // UISessionStore is the interface for UI session management ··· 126 128 slog.Debug("Invalidated cached session after creating new session", "did", did) 127 129 } 128 130 129 - // Look up identity 130 - ident, err := s.app.directory.LookupDID(r.Context(), sessionData.AccountDID) 131 - handle := ident.Handle.String() 131 + // Look up identity (resolve DID to handle) 132 + _, handle, _, err := atproto.ResolveIdentity(r.Context(), did) 132 133 if err != nil { 133 134 slog.Warn("Failed to resolve DID to handle, using DID as fallback", "error", err, "did", did) 134 135 handle = did // Fallback to DID if resolution fails
+2 -17
pkg/auth/session.go
··· 17 17 "time" 18 18 19 19 "atcr.io/pkg/atproto" 20 - 21 - "github.com/bluesky-social/indigo/atproto/identity" 22 - "github.com/bluesky-social/indigo/atproto/syntax" 23 20 ) 24 21 25 22 // CachedSession represents a cached session ··· 33 30 34 31 // SessionValidator validates ATProto credentials 35 32 type SessionValidator struct { 36 - directory identity.Directory 37 33 httpClient *http.Client 38 34 cache map[string]*CachedSession 39 35 cacheMu sync.RWMutex ··· 42 38 // NewSessionValidator creates a new ATProto session validator 43 39 func NewSessionValidator() *SessionValidator { 44 40 return &SessionValidator{ 45 - directory: atproto.GetDirectory(), 46 41 httpClient: &http.Client{}, 47 42 cache: make(map[string]*CachedSession), 48 43 } ··· 102 97 slog.Debug("No cached session, creating new session", "identifier", identifier) 103 98 104 99 // Resolve identifier to PDS endpoint 105 - atID, err := syntax.ParseAtIdentifier(identifier) 106 - if err != nil { 107 - return "", "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err) 108 - } 109 - 110 - ident, err := v.directory.Lookup(ctx, *atID) 100 + _, _, pds, err := atproto.ResolveIdentity(ctx, identifier) 111 101 if err != nil { 112 - return "", "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err) 113 - } 114 - 115 - pds := ident.PDSEndpoint() 116 - if pds == "" { 117 - return "", "", "", fmt.Errorf("no PDS endpoint found for %q", identifier) 102 + return "", "", "", err 118 103 } 119 104 120 105 // Create session
+8 -17
pkg/auth/token/handler.go
··· 9 9 "strings" 10 10 "time" 11 11 12 - "github.com/bluesky-social/indigo/atproto/syntax" 13 - 14 12 "atcr.io/pkg/appview/db" 15 13 "atcr.io/pkg/atproto" 16 14 "atcr.io/pkg/auth" ··· 158 156 // Call post-auth callback for AppView business logic (profile management, etc.) 159 157 if h.postAuthCallback != nil { 160 158 // Resolve PDS endpoint for callback 161 - directory := atproto.GetDirectory() 162 - atID, err := syntax.ParseAtIdentifier(username) 163 - if err == nil { 164 - ident, err := directory.Lookup(r.Context(), *atID) 165 - if err != nil { 166 - // Log error but don't fail auth - profile management is not critical 167 - slog.Warn("Failed to resolve PDS for callback", "error", err, "username", username) 168 - } else { 169 - pdsEndpoint := ident.PDSEndpoint() 170 - if pdsEndpoint != "" { 171 - if err := h.postAuthCallback(r.Context(), did, handle, pdsEndpoint, accessToken); err != nil { 172 - // Log error but don't fail auth - business logic is non-critical 173 - slog.Warn("Post-auth callback failed", "error", err, "did", did) 174 - } 175 - } 159 + _, _, pdsEndpoint, err := atproto.ResolveIdentity(r.Context(), username) 160 + if err != nil { 161 + // Log error but don't fail auth - profile management is not critical 162 + slog.Warn("Failed to resolve PDS for callback", "error", err, "username", username) 163 + } else { 164 + if err := h.postAuthCallback(r.Context(), did, handle, pdsEndpoint, accessToken); err != nil { 165 + // Log error but don't fail auth - business logic is non-critical 166 + slog.Warn("Post-auth callback failed", "error", err, "did", did) 176 167 } 177 168 } 178 169 }