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.

fix app-passwords remove dead code

+9 -540
-122
cmd/profile-update/main.go
··· 1 - package main 2 - 3 - import ( 4 - "context" 5 - "encoding/base64" 6 - "encoding/json" 7 - "flag" 8 - "fmt" 9 - "log" 10 - "os" 11 - "path/filepath" 12 - "strings" 13 - 14 - "atcr.io/pkg/atproto" 15 - atprotoAuth "atcr.io/pkg/auth/atproto" 16 - ) 17 - 18 - // DockerConfig represents ~/.docker/config.json 19 - type DockerConfig struct { 20 - Auths map[string]AuthEntry `json:"auths"` 21 - } 22 - 23 - type AuthEntry struct { 24 - Auth string `json:"auth"` // base64(username:password) 25 - } 26 - 27 - func main() { 28 - var defaultHold string 29 - var registryURL string 30 - 31 - flag.StringVar(&defaultHold, "default-hold", "", "Default hold endpoint URL (e.g., http://172.28.0.3:8080)") 32 - flag.StringVar(&registryURL, "registry", "127.0.0.1:5000", "Registry URL to read auth from Docker config") 33 - flag.Parse() 34 - 35 - // Read Docker config 36 - home, err := os.UserHomeDir() 37 - if err != nil { 38 - log.Fatalf("Failed to get home directory: %v", err) 39 - } 40 - dockerConfigPath := filepath.Join(home, ".docker", "config.json") 41 - 42 - configData, err := os.ReadFile(dockerConfigPath) 43 - if err != nil { 44 - log.Fatalf("Failed to read Docker config: %v\n\nMake sure you've logged in with: docker login %s", err, registryURL) 45 - } 46 - 47 - var dockerConfig DockerConfig 48 - if err := json.Unmarshal(configData, &dockerConfig); err != nil { 49 - log.Fatalf("Failed to parse Docker config: %v", err) 50 - } 51 - 52 - // Get auth for registry 53 - authEntry, ok := dockerConfig.Auths[registryURL] 54 - if !ok { 55 - log.Fatalf("No auth found for registry %s in Docker config", registryURL) 56 - } 57 - 58 - // Decode base64 auth (format: "username:password") 59 - authBytes, err := base64.StdEncoding.DecodeString(authEntry.Auth) 60 - if err != nil { 61 - log.Fatalf("Failed to decode auth: %v", err) 62 - } 63 - 64 - parts := strings.SplitN(string(authBytes), ":", 2) 65 - if len(parts) != 2 { 66 - log.Fatalf("Invalid auth format") 67 - } 68 - 69 - handle := parts[0] 70 - password := parts[1] // This should be an app password 71 - 72 - fmt.Printf("Handle: %s\n", handle) 73 - 74 - // Create session validator and get access token 75 - validator := atprotoAuth.NewSessionValidator() 76 - ctx := context.Background() 77 - 78 - did, pdsEndpoint, accessToken, err := validator.CreateSessionAndGetToken(ctx, handle, password) 79 - if err != nil { 80 - log.Fatalf("Failed to authenticate: %v", err) 81 - } 82 - 83 - fmt.Printf("DID: %s\n", did) 84 - fmt.Printf("PDS: %s\n\n", pdsEndpoint) 85 - 86 - // Create client with the access token from createSession 87 - client := atproto.NewClient(pdsEndpoint, did, accessToken) 88 - 89 - // Get current profile 90 - profile, err := atproto.GetProfile(ctx, client) 91 - if err != nil { 92 - log.Fatalf("Failed to get current profile: %v", err) 93 - } 94 - 95 - if profile == nil { 96 - if defaultHold == "" { 97 - fmt.Println("No existing profile found.") 98 - fmt.Println("\nTo create profile with default hold, use: -default-hold <url>") 99 - return 100 - } 101 - fmt.Println("No existing profile found. Creating new profile...") 102 - profile = atproto.NewSailorProfileRecord(defaultHold) 103 - } else { 104 - fmt.Printf("Current defaultHold: %s\n", profile.DefaultHold) 105 - if defaultHold == "" { 106 - // Just show current profile 107 - fmt.Println("\nTo update, use: -default-hold <url>") 108 - return 109 - } 110 - profile.DefaultHold = defaultHold 111 - } 112 - 113 - // Update profile 114 - if defaultHold != "" { 115 - err = atproto.UpdateProfile(ctx, client, profile) 116 - if err != nil { 117 - log.Fatalf("Failed to update profile: %v", err) 118 - } 119 - 120 - fmt.Printf("\n✓ Updated defaultHold to: %s\n", defaultHold) 121 - } 122 - }
+6 -40
pkg/auth/atproto/session.go
··· 19 19 // CachedSession represents a cached session 20 20 type CachedSession struct { 21 21 DID string 22 + Handle string 22 23 PDS string 23 24 AccessToken string 24 25 ExpiresAt time.Time ··· 83 84 AccessToken string `json:"access_token,omitempty"` // Alternative field name 84 85 } 85 86 86 - // ValidateCredentials validates username and password against ATProto 87 - // Returns the user's DID and PDS endpoint if valid 88 - func (v *SessionValidator) ValidateCredentials(ctx context.Context, identifier, password string) (did, pdsEndpoint string, err error) { 89 - // Resolve identifier (handle or DID) to PDS endpoint 90 - atID, err := syntax.ParseAtIdentifier(identifier) 91 - if err != nil { 92 - return "", "", fmt.Errorf("invalid identifier %q: %w", identifier, err) 93 - } 94 - 95 - ident, err := v.directory.Lookup(ctx, *atID) 96 - if err != nil { 97 - return "", "", fmt.Errorf("failed to resolve identity %q: %w", identifier, err) 98 - } 99 - 100 - resolvedDID := ident.DID.String() 101 - pds := ident.PDSEndpoint() 102 - if pds == "" { 103 - return "", "", fmt.Errorf("no PDS endpoint found for %q", identifier) 104 - } 105 - 106 - fmt.Printf("DEBUG: Resolved %s to DID=%s, PDS=%s\n", identifier, resolvedDID, pds) 107 - 108 - // Create session with the PDS 109 - fmt.Printf("DEBUG [atproto/session]: Creating session for %s at PDS %s\n", identifier, pds) 110 - sessionResp, err := v.createSession(ctx, pds, identifier, password) 111 - if err != nil { 112 - fmt.Printf("DEBUG [atproto/session]: Session creation failed: %v\n", err) 113 - return "", "", fmt.Errorf("authentication failed for %s at PDS %s: %w", identifier, pds, err) 114 - } 115 - 116 - fmt.Printf("DEBUG [atproto/session]: Session created successfully, DID=%s, Handle=%s, AccessJWT length=%d\n", 117 - sessionResp.DID, sessionResp.Handle, len(sessionResp.AccessJWT)) 118 - 119 - return sessionResp.DID, pds, nil 120 - } 121 - 122 - // CreateSessionAndGetToken creates a session and returns the DID, PDS endpoint, and access token 123 - func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identifier, password string) (did, pdsEndpoint, accessToken string, err error) { 87 + // CreateSessionAndGetToken creates a session and returns the DID, handle, and access token 88 + func (v *SessionValidator) CreateSessionAndGetToken(ctx context.Context, identifier, password string) (did, handle, accessToken string, err error) { 124 89 // Check cache first 125 90 cacheKey := getCacheKey(identifier, password) 126 91 if cached, ok := v.getCachedSession(cacheKey); ok { 127 92 fmt.Printf("DEBUG [atproto/session]: Using cached session for %s (DID=%s)\n", identifier, cached.DID) 128 - return cached.DID, cached.PDS, cached.AccessToken, nil 93 + return cached.DID, cached.Handle, cached.AccessToken, nil 129 94 } 130 95 131 96 fmt.Printf("DEBUG [atproto/session]: No cached session for %s, creating new session\n", identifier) ··· 156 121 // Cache the session (ATProto sessions typically last 2 hours) 157 122 v.setCachedSession(cacheKey, &CachedSession{ 158 123 DID: sessionResp.DID, 124 + Handle: sessionResp.Handle, 159 125 PDS: pds, 160 126 AccessToken: sessionResp.AccessJWT, 161 127 ExpiresAt: time.Now().Add(2 * time.Hour), 162 128 }) 163 129 fmt.Printf("DEBUG [atproto/session]: Cached session for %s (expires in 2 hours)\n", identifier) 164 130 165 - return sessionResp.DID, pds, sessionResp.AccessJWT, nil 131 + return sessionResp.DID, sessionResp.Handle, sessionResp.AccessJWT, nil 166 132 } 167 133 168 134 // createSession calls com.atproto.server.createSession
-112
pkg/auth/atproto/validator.go
··· 1 - package atproto 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "io" 8 - "net/http" 9 - 10 - "github.com/bluesky-social/indigo/atproto/identity" 11 - "github.com/bluesky-social/indigo/atproto/syntax" 12 - ) 13 - 14 - // TokenValidator validates ATProto OAuth access tokens 15 - type TokenValidator struct { 16 - httpClient *http.Client 17 - } 18 - 19 - // NewTokenValidator creates a new token validator 20 - func NewTokenValidator() *TokenValidator { 21 - return &TokenValidator{ 22 - httpClient: &http.Client{}, 23 - } 24 - } 25 - 26 - // SessionInfo represents the response from com.atproto.server.getSession 27 - type SessionInfo struct { 28 - DID string `json:"did"` 29 - Handle string `json:"handle"` 30 - Email string `json:"email,omitempty"` 31 - EmailConfirmed bool `json:"emailConfirmed,omitempty"` 32 - Active bool `json:"active,omitempty"` 33 - } 34 - 35 - // ValidateToken validates an ATProto OAuth access token by calling getSession 36 - // Returns the user's DID and handle if the token is valid 37 - // dpopProof is optional - if provided, uses DPoP auth; otherwise uses Bearer 38 - func (v *TokenValidator) ValidateToken(ctx context.Context, pdsEndpoint, accessToken, dpopProof string) (*SessionInfo, error) { 39 - // Call com.atproto.server.getSession with the access token 40 - url := fmt.Sprintf("%s/xrpc/com.atproto.server.getSession", pdsEndpoint) 41 - 42 - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 43 - if err != nil { 44 - return nil, fmt.Errorf("failed to create request: %w", err) 45 - } 46 - 47 - // Always use Bearer auth for getSession validation 48 - // The DPoP proof from the client is bound to their request to us (POST /auth/exchange), 49 - // not to our request to the PDS (GET /getSession) 50 - req.Header.Set("Authorization", "Bearer "+accessToken) 51 - 52 - fmt.Printf("DEBUG [validator]: calling %s with Bearer auth, token_prefix=%s...\n", 53 - url, accessToken[:min(20, len(accessToken))]) 54 - 55 - resp, err := v.httpClient.Do(req) 56 - if err != nil { 57 - return nil, fmt.Errorf("failed to get session: %w", err) 58 - } 59 - defer resp.Body.Close() 60 - 61 - // Read body once for both logging and error handling 62 - bodyBytes, _ := io.ReadAll(resp.Body) 63 - 64 - if resp.StatusCode == http.StatusUnauthorized { 65 - fmt.Printf("DEBUG [validator]: getSession returned 401: %s\n", string(bodyBytes)) 66 - return nil, fmt.Errorf("invalid or expired token") 67 - } 68 - 69 - if resp.StatusCode != http.StatusOK { 70 - fmt.Printf("DEBUG [validator]: getSession failed with status %d: %s\n", resp.StatusCode, string(bodyBytes)) 71 - return nil, fmt.Errorf("getSession failed with status %d: %s", resp.StatusCode, string(bodyBytes)) 72 - } 73 - 74 - var session SessionInfo 75 - if err := json.Unmarshal(bodyBytes, &session); err != nil { 76 - return nil, fmt.Errorf("failed to decode session: %w", err) 77 - } 78 - 79 - // Validate required fields 80 - if session.DID == "" { 81 - return nil, fmt.Errorf("session response missing DID") 82 - } 83 - if session.Handle == "" { 84 - return nil, fmt.Errorf("session response missing handle") 85 - } 86 - 87 - return &session, nil 88 - } 89 - 90 - // ValidateTokenWithResolver validates a token and automatically resolves the PDS endpoint 91 - // dpopProof is optional - if provided, uses DPoP auth; otherwise uses Bearer 92 - func (v *TokenValidator) ValidateTokenWithResolver(ctx context.Context, handle, accessToken, dpopProof string) (*SessionInfo, error) { 93 - // Resolve handle to PDS endpoint 94 - directory := identity.DefaultDirectory() 95 - atID, err := syntax.ParseAtIdentifier(handle) 96 - if err != nil { 97 - return nil, fmt.Errorf("invalid identifier %q: %w", handle, err) 98 - } 99 - 100 - ident, err := directory.Lookup(ctx, *atID) 101 - if err != nil { 102 - return nil, fmt.Errorf("failed to resolve PDS endpoint: %w", err) 103 - } 104 - 105 - pdsEndpoint := ident.PDSEndpoint() 106 - if pdsEndpoint == "" { 107 - return nil, fmt.Errorf("no PDS endpoint found for %q", handle) 108 - } 109 - 110 - // Validate token against the PDS 111 - return v.ValidateToken(ctx, pdsEndpoint, accessToken, dpopProof) 112 - }
-87
pkg/auth/oauth/interactive.go
··· 4 4 "context" 5 5 "fmt" 6 6 "net/http" 7 - "net/url" 8 - "sync" 9 7 "time" 10 8 11 9 "github.com/bluesky-social/indigo/atproto/auth/oauth" ··· 16 14 SessionData *oauth.ClientSessionData 17 15 Session *oauth.ClientSession 18 16 App *App 19 - } 20 - 21 - // RunInteractiveFlow runs an interactive OAuth flow for CLI tools 22 - // This is a simplified wrapper around indigo's OAuth flow 23 - func RunInteractiveFlow( 24 - ctx context.Context, 25 - baseURL string, 26 - handle string, 27 - scopes []string, 28 - onAuthURL func(string) error, 29 - ) (*InteractiveResult, error) { 30 - // Create temporary file store for this flow 31 - store, err := NewFileStore("/tmp/atcr-oauth-temp.json") 32 - if err != nil { 33 - return nil, fmt.Errorf("failed to create OAuth store: %w", err) 34 - } 35 - 36 - // Create OAuth app 37 - app, err := NewApp(baseURL, store) 38 - if err != nil { 39 - return nil, fmt.Errorf("failed to create OAuth app: %w", err) 40 - } 41 - 42 - // Set custom scopes if provided 43 - if len(scopes) > 0 { 44 - // Note: indigo's ClientApp doesn't expose SetScopes, so we need to use default scopes 45 - // This is a limitation of the current implementation 46 - // TODO: Enhance if custom scopes are needed 47 - } 48 - 49 - // Start auth flow 50 - authURL, err := app.StartAuthFlow(ctx, handle) 51 - if err != nil { 52 - return nil, fmt.Errorf("failed to start auth flow: %w", err) 53 - } 54 - 55 - // Call the callback to display the auth URL 56 - if err := onAuthURL(authURL); err != nil { 57 - return nil, fmt.Errorf("auth URL callback failed: %w", err) 58 - } 59 - 60 - // Wait for OAuth callback 61 - // The callback will be handled by the http.HandleFunc registered by the caller 62 - // We need to wait for ProcessCallback to be called 63 - // This is a bit awkward, but matches the old pattern 64 - 65 - // Setup a channel to receive callback params 66 - callbackChan := make(chan url.Values, 1) 67 - var setupOnce sync.Once 68 - 69 - // Return a function that the caller can use to process the callback 70 - // This is called from the HTTP handler 71 - processCallback := func(params url.Values) (*oauth.ClientSessionData, error) { 72 - setupOnce.Do(func() { 73 - callbackChan <- params 74 - }) 75 - sessionData, err := app.ProcessCallback(ctx, params) 76 - if err != nil { 77 - return nil, fmt.Errorf("failed to process callback: %w", err) 78 - } 79 - return sessionData, nil 80 - } 81 - 82 - // Wait for callback with timeout 83 - select { 84 - case params := <-callbackChan: 85 - sessionData, err := processCallback(params) 86 - if err != nil { 87 - return nil, err 88 - } 89 - 90 - // Resume session to get ClientSession 91 - session, err := app.ResumeSession(ctx, sessionData.AccountDID, sessionData.SessionID) 92 - if err != nil { 93 - return nil, fmt.Errorf("failed to resume session: %w", err) 94 - } 95 - 96 - return &InteractiveResult{ 97 - SessionData: sessionData, 98 - Session: session, 99 - App: app, 100 - }, nil 101 - case <-time.After(5 * time.Minute): 102 - return nil, fmt.Errorf("OAuth flow timed out after 5 minutes") 103 - } 104 17 } 105 18 106 19 // InteractiveFlowWithCallback runs an interactive OAuth flow with explicit callback handling
-97
pkg/auth/oauth/refresher.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "net/http" 7 6 "sync" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/auth/oauth" ··· 74 73 return r.resumeSession(ctx, did) 75 74 } 76 75 77 - // GetAccessToken gets a fresh access token for a DID 78 - // This is a convenience method that extracts the access token from the session 79 - func (r *Refresher) GetAccessToken(ctx context.Context, did string) (string, error) { 80 - session, err := r.GetSession(ctx, did) 81 - if err != nil { 82 - return "", err 83 - } 84 - 85 - // Get access token and DPoP nonce from session 86 - accessToken, _ := session.GetHostAccessData() 87 - return accessToken, nil 88 - } 89 - 90 - // GetHTTPClient returns an HTTP client with DPoP authentication for a DID 91 - // The client automatically adds DPoP headers and refreshes tokens as needed 92 - func (r *Refresher) GetHTTPClient(ctx context.Context, did string) (*http.Client, error) { 93 - session, err := r.GetSession(ctx, did) 94 - if err != nil { 95 - return nil, err 96 - } 97 - 98 - // Get API client from session 99 - // This client automatically handles DPoP and token refresh 100 - apiClient := session.APIClient() 101 - return apiClient.Client, nil 102 - } 103 - 104 76 // resumeSession loads a session from storage and caches it 105 77 func (r *Refresher) resumeSession(ctx context.Context, did string) (*oauth.ClientSession, error) { 106 78 // Parse DID ··· 153 125 delete(r.sessions, did) 154 126 r.mu.Unlock() 155 127 } 156 - 157 - // RevokeSession removes a session from both cache and storage 158 - func (r *Refresher) RevokeSession(ctx context.Context, did string) error { 159 - // Remove from cache 160 - r.mu.Lock() 161 - cached, ok := r.sessions[did] 162 - delete(r.sessions, did) 163 - r.mu.Unlock() 164 - 165 - if !ok { 166 - // Not cached, still try to delete from storage 167 - accountDID, err := syntax.ParseDID(did) 168 - if err != nil { 169 - return fmt.Errorf("failed to parse DID: %w", err) 170 - } 171 - 172 - // Find session ID from store 173 - fileStore, ok := r.app.clientApp.Store.(*FileStore) 174 - if !ok { 175 - return fmt.Errorf("store is not a FileStore") 176 - } 177 - 178 - sessions := fileStore.ListSessions() 179 - for _, sessionData := range sessions { 180 - if sessionData.AccountDID.String() == did { 181 - return r.app.clientApp.Store.DeleteSession(ctx, accountDID, sessionData.SessionID) 182 - } 183 - } 184 - 185 - return fmt.Errorf("no session found for DID: %s", did) 186 - } 187 - 188 - // Revoke the session via OAuth 189 - if err := cached.Session.RevokeSession(ctx); err != nil { 190 - fmt.Printf("WARNING: failed to revoke session for %s: %v\n", did, err) 191 - // Continue anyway to delete from storage 192 - } 193 - 194 - // Delete from storage 195 - accountDID, err := syntax.ParseDID(did) 196 - if err != nil { 197 - return fmt.Errorf("failed to parse DID: %w", err) 198 - } 199 - 200 - return r.app.clientApp.Store.DeleteSession(ctx, accountDID, cached.SessionID) 201 - } 202 - 203 - // CleanupExpiredSessions removes expired sessions from cache 204 - // Note: indigo handles token expiry automatically, but we clean up orphaned cache entries 205 - func (r *Refresher) CleanupExpiredSessions(ctx context.Context) { 206 - r.mu.Lock() 207 - defer r.mu.Unlock() 208 - 209 - // For each cached session, verify it still exists in storage 210 - for did, cached := range r.sessions { 211 - accountDID, err := syntax.ParseDID(did) 212 - if err != nil { 213 - delete(r.sessions, did) 214 - continue 215 - } 216 - 217 - // Try to get session from store 218 - _, err = r.app.clientApp.Store.GetSession(ctx, accountDID, cached.SessionID) 219 - if err != nil { 220 - // Session no longer exists, remove from cache 221 - delete(r.sessions, did) 222 - } 223 - } 224 - }
+3 -25
pkg/auth/oauth/server.go
··· 1 1 package oauth 2 2 3 3 import ( 4 - "context" 5 4 "fmt" 6 5 "html/template" 7 6 "net/http" 8 7 "time" 9 - 10 - "github.com/bluesky-social/indigo/atproto/syntax" 11 8 ) 12 9 13 10 // UISessionStore is the interface for UI session management ··· 102 99 fmt.Printf("DEBUG [oauth/server]: Invalidated cached session for DID=%s after creating new session\n", did) 103 100 } 104 101 105 - // We need to get the handle for UI sessions and settings redirect 106 - // Resolve DID to handle using our resolver 107 - handle, err := s.resolveHandle(r.Context(), did) 102 + // Look up identity 103 + ident, err := s.app.directory.LookupDID(r.Context(), sessionData.AccountDID) 104 + handle := ident.Handle.String() 108 105 if err != nil { 109 106 fmt.Printf("WARNING [oauth/server]: Failed to resolve DID to handle: %v, using DID as handle\n", err) 110 107 handle = did // Fallback to DID if resolution fails ··· 150 147 151 148 // Non-UI flow: redirect to settings to get API key 152 149 s.renderRedirectToSettings(w, handle) 153 - } 154 - 155 - // resolveHandle attempts to resolve a DID to a handle 156 - // This is a best-effort helper - we use the directory to look up the handle 157 - func (s *Server) resolveHandle(ctx context.Context, didStr string) (string, error) { 158 - // Parse DID 159 - did, err := syntax.ParseDID(didStr) 160 - if err != nil { 161 - return "", fmt.Errorf("invalid DID: %w", err) 162 - } 163 - 164 - // Look up identity 165 - ident, err := s.app.directory.LookupDID(ctx, did) 166 - if err != nil { 167 - return "", fmt.Errorf("failed to lookup DID: %w", err) 168 - } 169 - 170 - // Return handle (may be handle.invalid if verification failed) 171 - return ident.Handle.String(), nil 172 150 } 173 151 174 152 // renderRedirectToSettings redirects to the settings page to generate an API key
-57
pkg/server/handler.go
··· 1 - package server 2 - 3 - import ( 4 - "net/http" 5 - "strings" 6 - 7 - "github.com/bluesky-social/indigo/atproto/identity" 8 - ) 9 - 10 - // ATProtoHandler wraps an HTTP handler to provide name resolution 11 - // This is an optional layer if middleware doesn't provide enough control 12 - type ATProtoHandler struct { 13 - handler http.Handler 14 - directory identity.Directory 15 - } 16 - 17 - // NewATProtoHandler creates a new HTTP handler wrapper 18 - func NewATProtoHandler(handler http.Handler) *ATProtoHandler { 19 - return &ATProtoHandler{ 20 - handler: handler, 21 - directory: identity.DefaultDirectory(), 22 - } 23 - } 24 - 25 - // ServeHTTP handles HTTP requests with name resolution 26 - func (h *ATProtoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 27 - // Parse the request path to extract user/image 28 - // OCI Distribution API paths look like: 29 - // /v2/<name>/manifests/<reference> 30 - // /v2/<name>/blobs/<digest> 31 - 32 - path := r.URL.Path 33 - 34 - // Check if this is a v2 API request 35 - if strings.HasPrefix(path, "/v2/") { 36 - // Extract the repository name 37 - parts := strings.Split(strings.TrimPrefix(path, "/v2/"), "/") 38 - if len(parts) >= 2 { 39 - // parts[0] might be username/DID 40 - // We could do early resolution here if needed 41 - // For now, we'll let the middleware handle it 42 - } 43 - } 44 - 45 - // Delegate to the underlying handler 46 - // The registry middleware will handle the actual resolution 47 - h.handler.ServeHTTP(w, r) 48 - } 49 - 50 - // Note: In the current architecture, most of the name resolution 51 - // is handled by the registry middleware. This HTTP handler wrapper 52 - // is here for cases where you need to intercept requests before 53 - // they reach the distribution handlers, such as for: 54 - // - Custom authentication based on DIDs 55 - // - Request rewriting 56 - // - Early validation 57 - // - Custom API endpoints beyond OCI spec