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.

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 - }