A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go

fix error code checking to not just check the raw string response in the case that '401' shows up in the sha256

evan.jarrett.net 7c064ba8 136c0a0e

verified
+52 -11
+2 -1
deploy/upcloud/configs/cloudinit.sh.tmpl
··· 17 17 # System packages 18 18 export DEBIAN_FRONTEND=noninteractive 19 19 apt-get update && apt-get upgrade -y 20 - apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop 20 + apt-get install -y git gcc make curl libsqlite3-dev nodejs npm htop systemd-timesyncd 21 + timedatectl set-ntp true 21 22 22 23 # Swap (for small instances) 23 24 if [ ! -f /swapfile ]; then
+25 -6
pkg/appview/handlers/oauth_errors.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "log/slog" 6 7 "strings" 7 8 8 9 "atcr.io/pkg/auth/oauth" 10 + "github.com/bluesky-social/indigo/atproto/atclient" 11 + "github.com/bluesky-social/indigo/xrpc" 9 12 ) 10 13 11 14 // isOAuthError checks if an error indicates OAuth authentication failure 12 15 // These errors indicate the OAuth session is invalid and should be cleaned up 16 + // Uses structured error types to avoid false positives from substring matching 13 17 func isOAuthError(err error) bool { 14 18 if err == nil { 15 19 return false 16 20 } 21 + 22 + // Check structured error types first 23 + var xrpcErr *xrpc.Error 24 + if errors.As(err, &xrpcErr) && (xrpcErr.StatusCode == 401 || xrpcErr.StatusCode == 403) { 25 + return true 26 + } 27 + var apiErr *atclient.APIError 28 + if errors.As(err, &apiErr) { 29 + if apiErr.StatusCode == 401 || apiErr.StatusCode == 403 { 30 + return true 31 + } 32 + if apiErr.Name == "InvalidToken" || apiErr.Name == "InsufficientScope" || apiErr.Name == "InvalidGrant" { 33 + return true 34 + } 35 + } 36 + 37 + // Fallback: check for known auth-specific error strings that won't 38 + // appear in digests or URIs 17 39 errStr := strings.ToLower(err.Error()) 18 - return strings.Contains(errStr, "401") || 19 - strings.Contains(errStr, "403") || 20 - strings.Contains(errStr, "invalid_token") || 40 + return strings.Contains(errStr, "invalid_token") || 21 41 strings.Contains(errStr, "invalid_grant") || 22 42 strings.Contains(errStr, "use_dpop_nonce") || 23 - strings.Contains(errStr, "unauthorized") || 24 - strings.Contains(errStr, "token") && strings.Contains(errStr, "expired") || 25 - strings.Contains(errStr, "authentication failed") 43 + strings.Contains(errStr, "authentication failed") || 44 + strings.Contains(errStr, "token expired") 26 45 } 27 46 28 47 // handleOAuthError checks if an error is OAuth-related and invalidates UI sessions if so
+25 -4
pkg/auth/oauth/client.go
··· 6 6 7 7 import ( 8 8 "context" 9 + "errors" 9 10 "fmt" 10 11 "log/slog" 11 12 "strings" ··· 13 14 "time" 14 15 15 16 "atcr.io/pkg/atproto" 17 + "github.com/bluesky-social/indigo/atproto/atclient" 16 18 "github.com/bluesky-social/indigo/atproto/atcrypto" 17 19 "github.com/bluesky-social/indigo/atproto/auth/oauth" 18 20 "github.com/bluesky-social/indigo/atproto/syntax" 21 + "github.com/bluesky-social/indigo/xrpc" 19 22 ) 20 23 21 24 // permissionSetExpansions maps lexicon IDs to their expanded scope format. ··· 312 315 } 313 316 314 317 // isAuthError checks if an error looks like an OAuth/auth failure 318 + // Uses structured error types to avoid false positives from substring matching 319 + // (e.g., a digest hash containing "401" in a RecordNotFound error) 315 320 func isAuthError(err error) bool { 316 321 if err == nil { 317 322 return false 318 323 } 324 + 325 + // Check structured error types first 326 + var xrpcErr *xrpc.Error 327 + if errors.As(err, &xrpcErr) && xrpcErr.StatusCode == 401 { 328 + return true 329 + } 330 + var apiErr *atclient.APIError 331 + if errors.As(err, &apiErr) { 332 + if apiErr.StatusCode == 401 { 333 + return true 334 + } 335 + if apiErr.Name == "InvalidToken" || apiErr.Name == "InsufficientScope" { 336 + return true 337 + } 338 + } 339 + 340 + // Fallback: check for known auth-specific error strings that won't 341 + // appear in digests or URIs 319 342 errStr := strings.ToLower(err.Error()) 320 - return strings.Contains(errStr, "unauthorized") || 321 - strings.Contains(errStr, "invalid_token") || 343 + return strings.Contains(errStr, "invalid_token") || 322 344 strings.Contains(errStr, "insufficient_scope") || 323 - strings.Contains(errStr, "token expired") || 324 - strings.Contains(errStr, "401") 345 + strings.Contains(errStr, "token expired") 325 346 } 326 347 327 348 // resumeSession loads a session from storage