A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
73
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 }