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

Configure Feed

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

minor bug fixes around hold did:web instead of url endpoint

+65 -63
+20 -2
pkg/appview/templates/pages/settings.html
··· 35 35 <!-- Default Hold Section --> 36 36 <section class="settings-section"> 37 37 <h2>Default Hold</h2> 38 - <p>Current: <strong>{{ if .Profile.DefaultHold }}{{ .Profile.DefaultHold }}{{ else }}Not set{{ end }}</strong></p> 38 + <p>Current: <strong id="current-hold">{{ if .Profile.DefaultHold }}{{ .Profile.DefaultHold }}{{ else }}Not set{{ end }}</strong></p> 39 39 40 40 <form hx-post="/api/profile/default-hold" 41 41 hx-target="#hold-status" 42 - hx-swap="innerHTML"> 42 + hx-swap="innerHTML" 43 + id="hold-form"> 43 44 44 45 <div class="form-group"> 45 46 <label for="hold-endpoint">Hold Endpoint:</label> ··· 115 116 <script src="/static/js/app.js"></script> 116 117 117 118 <script> 119 + // Default Hold Update - Dynamic display update 120 + document.addEventListener('DOMContentLoaded', function() { 121 + const holdForm = document.getElementById('hold-form'); 122 + 123 + holdForm.addEventListener('htmx:afterSwap', function(event) { 124 + // Check if the response contains success indicator 125 + if (event.detail.xhr.status === 200) { 126 + const holdInput = document.getElementById('hold-endpoint'); 127 + const currentHoldDisplay = document.getElementById('current-hold'); 128 + const newValue = holdInput.value.trim(); 129 + 130 + // Update the current hold display 131 + currentHoldDisplay.textContent = newValue || 'Not set'; 132 + } 133 + }); 134 + }); 135 + 118 136 // Device Management JavaScript 119 137 (function() { 120 138 // Load devices
+6
pkg/atproto/lexicon.go
··· 364 364 365 365 // ResolveHoldDIDFromURL converts a hold endpoint URL to a did:web DID 366 366 // For did:web holds: https://hold01.atcr.io → did:web:hold01.atcr.io 367 + // If input is already a DID, returns it as-is 367 368 func ResolveHoldDIDFromURL(holdURL string) string { 368 369 // Handle empty URLs 369 370 if holdURL == "" { 370 371 return "" 372 + } 373 + 374 + // If already a DID, return as-is 375 + if strings.HasPrefix(holdURL, "did:") { 376 + return holdURL 371 377 } 372 378 373 379 // Parse URL to get hostname
+34 -2
pkg/atproto/profile.go
··· 5 5 "encoding/json" 6 6 "errors" 7 7 "fmt" 8 + "sync" 9 + "time" 8 10 ) 9 11 10 12 // Profile record key is always "self" per lexicon 11 13 const ProfileRKey = "self" 14 + 15 + // Global map to track in-flight profile migrations (DID -> true) 16 + // Used to prevent duplicate migration goroutines 17 + var migrationLocks sync.Map 12 18 13 19 // EnsureProfile checks if a user's profile exists and creates it if needed 14 20 // This should be called during authentication (OAuth exchange or token service) ··· 65 71 // This ensures backward compatibility with profiles created before DID migration 66 72 if profile.DefaultHold != "" && !isDID(profile.DefaultHold) { 67 73 // Convert URL to DID transparently 68 - profile.DefaultHold = ResolveHoldDIDFromURL(profile.DefaultHold) 69 - fmt.Printf("DEBUG [profile]: Migrated defaultHold URL to DID: %s\n", profile.DefaultHold) 74 + migratedDID := ResolveHoldDIDFromURL(profile.DefaultHold) 75 + profile.DefaultHold = migratedDID 76 + 77 + // Persist the migration to PDS in a background goroutine 78 + // Use a lock to ensure only one goroutine migrates this DID 79 + did := client.did 80 + if _, loaded := migrationLocks.LoadOrStore(did, true); !loaded { 81 + // We got the lock - launch goroutine to persist the migration 82 + go func() { 83 + // Clean up lock when done (after a short delay to batch requests) 84 + defer func() { 85 + time.Sleep(1 * time.Second) 86 + migrationLocks.Delete(did) 87 + }() 88 + 89 + // Create a new context with timeout for the background operation 90 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 91 + defer cancel() 92 + 93 + // Update the profile on the PDS 94 + profile.UpdatedAt = time.Now() 95 + if err := UpdateProfile(ctx, client, &profile); err != nil { 96 + fmt.Printf("WARNING [profile]: Failed to persist URL-to-DID migration for %s: %v\n", did, err) 97 + } else { 98 + fmt.Printf("DEBUG [profile]: Persisted defaultHold migration to DID: %s (for DID: %s)\n", migratedDID, did) 99 + } 100 + }() 101 + } 70 102 } 71 103 72 104 return &profile, nil
-44
pkg/hold/service.go
··· 4 4 "context" 5 5 "fmt" 6 6 "log" 7 - "net/url" 8 7 9 8 "atcr.io/pkg/auth" 10 9 "github.com/aws/aws-sdk-go/service/s3" ··· 74 73 75 74 return service, nil 76 75 } 77 - 78 - // GetPresignedURL is a public wrapper around getPresignedURL for use by PDS blob store 79 - func (s *HoldService) GetPresignedURL(ctx context.Context, operation PresignedURLOperation, digest string, did string) (string, error) { 80 - return s.getPresignedURL(ctx, operation, digest, did) 81 - } 82 - 83 - // isAuthorizedRead checks if the given DID has read access to this hold 84 - // This is a helper wrapper around the authorizer for internal use 85 - func (s *HoldService) isAuthorizedRead(did string) bool { 86 - ctx := context.Background() 87 - allowed, err := s.authorizer.CheckReadAccess(ctx, s.pds.DID(), did) 88 - if err != nil { 89 - log.Printf("Authorization check failed: %v", err) 90 - return false 91 - } 92 - return allowed 93 - } 94 - 95 - // isAuthorizedWrite checks if the given DID has write access to this hold 96 - // This is a helper wrapper around the authorizer for internal use 97 - func (s *HoldService) isAuthorizedWrite(did string) bool { 98 - ctx := context.Background() 99 - allowed, err := s.authorizer.CheckWriteAccess(ctx, s.pds.DID(), did) 100 - if err != nil { 101 - log.Printf("Authorization check failed: %v", err) 102 - return false 103 - } 104 - return allowed 105 - } 106 - 107 - // extractHostname extracts the hostname from a URL 108 - func extractHostname(urlStr string) (string, error) { 109 - u, err := url.Parse(urlStr) 110 - if err != nil { 111 - return "", err 112 - } 113 - // Remove port if present 114 - hostname := u.Hostname() 115 - if hostname == "" { 116 - return "", fmt.Errorf("no hostname in URL") 117 - } 118 - return hostname, nil 119 - }
+5 -15
pkg/hold/storage.go
··· 9 9 10 10 "github.com/aws/aws-sdk-go/aws" 11 11 "github.com/aws/aws-sdk-go/service/s3" 12 + 13 + "atcr.io/pkg/atproto" 12 14 ) 13 15 14 16 // atprotoBlobPath creates a per-DID storage path for ATProto blobs ··· 51 53 52 54 // getPresignedURL generates a presigned URL for GET, HEAD, or PUT operations 53 55 // Distinguishes between ATProto blobs (per-DID) and OCI blobs (content-addressed) 54 - func (s *HoldService) getPresignedURL(ctx context.Context, operation PresignedURLOperation, digest string, did string) (string, error) { 56 + func (s *HoldService) GetPresignedURL(ctx context.Context, operation PresignedURLOperation, digest string, did string) (string, error) { 55 57 var path string 56 58 57 59 // Determine blob type and construct appropriate path ··· 151 153 func (s *HoldService) getProxyURL(digest, did string, operation PresignedURLOperation) string { 152 154 // For read operations, use XRPC getBlob endpoint 153 155 if operation == OperationGet || operation == OperationHead { 154 - // Generate hold DID from public URL 155 - holdDID := s.getHoldDID() 156 + // Generate hold DID from public URL using shared function 157 + holdDID := atproto.ResolveHoldDIDFromURL(s.config.Server.PublicURL) 156 158 return fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", 157 159 s.config.Server.PublicURL, holdDID, digest) 158 160 } ··· 161 163 // Clients should use multipart upload flow via com.atproto.repo.uploadBlob 162 164 return "" 163 165 } 164 - 165 - // getHoldDID generates a did:web from the hold's public URL 166 - func (s *HoldService) getHoldDID() string { 167 - // Convert URL to did:web format 168 - // https://hold01.atcr.io → did:web:hold01.atcr.io 169 - url := s.config.Server.PublicURL 170 - url = strings.TrimPrefix(url, "https://") 171 - url = strings.TrimPrefix(url, "http://") 172 - url = strings.Split(url, "/")[0] // Remove path 173 - url = strings.Split(url, ":")[0] // Remove port 174 - return fmt.Sprintf("did:web:%s", url) 175 - }