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.

fix post schema

+27 -18
+3 -3
pkg/appview/middleware/registry.go
··· 33 33 // These are set by main.go during startup and copied into NamespaceResolver instances. 34 34 // After initialization, request handling uses the NamespaceResolver's instance fields. 35 35 var ( 36 - globalRefresher *oauth.Refresher 37 - globalDatabase storage.DatabaseMetrics 38 - globalAuthorizer auth.HoldAuthorizer 36 + globalRefresher *oauth.Refresher 37 + globalDatabase storage.DatabaseMetrics 38 + globalAuthorizer auth.HoldAuthorizer 39 39 globalReadmeCache storage.ReadmeCache 40 40 ) 41 41
+1
pkg/hold/oci/xrpc.go
··· 291 291 req.Repository, 292 292 req.Tag, 293 293 req.UserHandle, 294 + req.UserDID, 294 295 manifestDigest, 295 296 totalSize, 296 297 )
+8 -8
pkg/hold/pds/manifest_post.go
··· 13 13 // Includes facets for clickable mentions and links 14 14 func (p *HoldPDS) CreateManifestPost( 15 15 ctx context.Context, 16 - repository, tag, userHandle, digest string, 16 + repository, tag, userHandle, userDID, digest string, 17 17 totalSize int64, 18 18 ) (string, error) { 19 19 now := time.Now() ··· 30 30 text := fmt.Sprintf("@%s just pushed %s\nDigest: %s Size: %s", userHandle, repoWithTag, digestShort, sizeStr) 31 31 32 32 // Create facets for mentions and links 33 - facets := buildFacets(text, userHandle, repoWithTag, appViewURL) 33 + facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 34 34 35 35 // Create post struct with facets 36 36 post := &bsky.FeedPost{ ··· 60 60 return postURI, nil 61 61 } 62 62 63 - // formatDigest truncates digest to first 7 and last 7 chars 64 - // Example: sha256:abc1234567890...fedcba9876543210 -> sha256:abc1234...9876543 63 + // formatDigest truncates digest to first 10 chars 64 + // Example: sha256:abc1234567890fedcba9876543210 -> sha256:abc1234567... 65 65 func formatDigest(digest string) string { 66 66 if !strings.HasPrefix(digest, "sha256:") { 67 67 return digest // Return as-is if not sha256 68 68 } 69 69 70 70 hash := strings.TrimPrefix(digest, "sha256:") 71 - if len(hash) <= 14 { 71 + if len(hash) <= 10 { 72 72 return digest // Too short to truncate 73 73 } 74 74 75 - return fmt.Sprintf("sha256:%s...%s", hash[:7], hash[len(hash)-7:]) 75 + return fmt.Sprintf("sha256:%s...", hash[:10]) 76 76 } 77 77 78 78 // formatSize converts bytes to human-readable format ··· 98 98 99 99 // buildFacets creates mention and link facets for rich text 100 100 // IMPORTANT: Byte offsets must be calculated for UTF-8 encoded text 101 - func buildFacets(text, userHandle, repoWithTag, appViewURL string) []*bsky.RichtextFacet { 101 + func buildFacets(text, userHandle, userDID, repoWithTag, appViewURL string) []*bsky.RichtextFacet { 102 102 facets := []*bsky.RichtextFacet{} 103 103 104 104 // Find mention: "@alice.bsky.social" ··· 117 117 Features: []*bsky.RichtextFacet_Features_Elem{ 118 118 { 119 119 RichtextFacet_Mention: &bsky.RichtextFacet_Mention{ 120 - Did: "", // Will be resolved by Bluesky from handle 120 + Did: userDID, 121 121 }, 122 122 }, 123 123 },
+15 -7
pkg/hold/pds/manifest_post_test.go
··· 16 16 { 17 17 name: "standard sha256 digest", 18 18 digest: "sha256:abc1234567890fedcba9876543210", 19 - expected: "sha256:abc1234...6543210", // Last 7 chars of hash 19 + expected: "sha256:abc1234567...", // First 10 chars 20 20 }, 21 21 { 22 22 name: "short digest (no truncation)", ··· 31 31 { 32 32 name: "real sha256 digest", 33 33 digest: "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", 34 - expected: "sha256:e692418...7fc331f", // Last 7 chars are "7fc331f" 34 + expected: "sha256:e692418e4c...", // First 10 chars 35 35 }, 36 36 } 37 37 ··· 108 108 name string 109 109 text string 110 110 userHandle string 111 + userDID string 111 112 repoWithTag string 112 113 appViewURL string 113 114 wantFacets int // number of facets expected ··· 116 117 name: "standard post with mention and link", 117 118 text: "@alice.bsky.social just pushed myapp:latest\nDigest: sha256:abc...def Size: 12.2 MB", 118 119 userHandle: "alice.bsky.social", 120 + userDID: "did:plc:alice123", 119 121 repoWithTag: "myapp:latest", 120 122 appViewURL: "https://atcr.io/r/alice.bsky.social/myapp", 121 123 wantFacets: 2, ··· 124 126 name: "no matches found", 125 127 text: "random text", 126 128 userHandle: "alice.bsky.social", 129 + userDID: "did:plc:alice123", 127 130 repoWithTag: "myapp:latest", 128 131 appViewURL: "https://atcr.io/r/alice.bsky.social/myapp", 129 132 wantFacets: 0, ··· 132 135 name: "only mention found", 133 136 text: "@alice.bsky.social did something", 134 137 userHandle: "alice.bsky.social", 138 + userDID: "did:plc:alice123", 135 139 repoWithTag: "myapp:latest", 136 140 appViewURL: "https://atcr.io/r/alice.bsky.social/myapp", 137 141 wantFacets: 1, ··· 140 144 141 145 for _, tt := range tests { 142 146 t.Run(tt.name, func(t *testing.T) { 143 - facets := buildFacets(tt.text, tt.userHandle, tt.repoWithTag, tt.appViewURL) 147 + facets := buildFacets(tt.text, tt.userHandle, tt.userDID, tt.repoWithTag, tt.appViewURL) 144 148 145 149 if len(facets) != tt.wantFacets { 146 150 t.Errorf("buildFacets() returned %d facets, want %d", len(facets), tt.wantFacets) ··· 183 187 // Test that byte offsets are correctly calculated 184 188 text := "@alice.bsky.social just pushed myapp:latest" 185 189 userHandle := "alice.bsky.social" 190 + userDID := "did:plc:alice123" 186 191 repoWithTag := "myapp:latest" 187 192 appViewURL := "https://atcr.io/r/alice.bsky.social/myapp" 188 193 189 - facets := buildFacets(text, userHandle, repoWithTag, appViewURL) 194 + facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 190 195 191 196 if len(facets) != 2 { 192 197 t.Fatalf("expected 2 facets, got %d", len(facets)) ··· 235 240 // Test with Unicode characters to ensure byte offsets work correctly 236 241 text := "@alice.bsky.social just pushed 🚀myapp:latest" 237 242 userHandle := "alice.bsky.social" 243 + userDID := "did:plc:alice123" 238 244 repoWithTag := "🚀myapp:latest" // Note: emoji is multi-byte 239 245 appViewURL := "https://atcr.io/r/alice.bsky.social/myapp" 240 246 241 - facets := buildFacets(text, userHandle, repoWithTag, appViewURL) 247 + facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 242 248 243 249 if len(facets) != 2 { 244 250 t.Fatalf("expected 2 facets, got %d", len(facets)) ··· 263 269 // Ensure facets don't overlap 264 270 text := "@alice.bsky.social just pushed myapp:latest" 265 271 userHandle := "alice.bsky.social" 272 + userDID := "did:plc:alice123" 266 273 repoWithTag := "myapp:latest" 267 274 appViewURL := "https://atcr.io/r/alice.bsky.social/myapp" 268 275 269 - facets := buildFacets(text, userHandle, repoWithTag, appViewURL) 276 + facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 270 277 271 278 if len(facets) != 2 { 272 279 t.Fatalf("expected 2 facets, got %d", len(facets)) ··· 287 294 repository := "hsm-secrets-operator" 288 295 tag := "latest" 289 296 userHandle := "evan.jarrett.net" 297 + userDID := "did:plc:pddp4xt5lgnv2qsegbzzs4xg" 290 298 digest := "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" 291 299 totalSize := int64(12800000) // ~12.2 MB 292 300 ··· 297 305 text := "@" + userHandle + " just pushed " + repoWithTag + "\nDigest: " + digestShort + " Size: " + sizeStr 298 306 appViewURL := "https://atcr.io/r/" + userHandle + "/" + repository 299 307 300 - facets := buildFacets(text, userHandle, repoWithTag, appViewURL) 308 + facets := buildFacets(text, userHandle, userDID, repoWithTag, appViewURL) 301 309 302 310 // Should have 2 facets: mention and link 303 311 if len(facets) != 2 {