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

Configure Feed

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

actually wrap them in a envvar check

+59 -43
+2 -2
cmd/hold/main.go
··· 43 43 44 44 // Initialize PDS with carstore and keys 45 45 ctx := context.Background() 46 - holdPDS, err = pds.NewHoldPDS(ctx, holdDID, cfg.Server.PublicURL, cfg.Database.Path, cfg.Database.KeyPath) 46 + holdPDS, err = pds.NewHoldPDS(ctx, holdDID, cfg.Server.PublicURL, cfg.Database.Path, cfg.Database.KeyPath, cfg.Registration.EnableBlueskyPosts) 47 47 if err != nil { 48 48 log.Fatalf("Failed to initialize embedded PDS: %v", err) 49 49 } ··· 103 103 xrpcHandler = pds.NewXRPCHandler(holdPDS, *s3Service, driver, broadcaster, nil) 104 104 105 105 // Create OCI XRPC handler (multipart upload endpoints) 106 - ociHandler = oci.NewXRPCHandler(holdPDS, *s3Service, driver, cfg.Server.DisablePresignedURLs, nil) 106 + ociHandler = oci.NewXRPCHandler(holdPDS, *s3Service, driver, cfg.Server.DisablePresignedURLs, cfg.Registration.EnableBlueskyPosts, nil) 107 107 } 108 108 109 109 // Setup HTTP routes with chi router
+6
pkg/hold/config.go
··· 32 32 // ProfileAvatarURL is the URL to download the avatar image from (from env: HOLD_PROFILE_AVATAR) 33 33 // If set, the avatar will be downloaded and uploaded as a blob during bootstrap 34 34 ProfileAvatarURL string `yaml:"profile_avatar_url"` 35 + 36 + // EnableBlueskyPosts controls whether to create Bluesky posts for manifest uploads (from env: HOLD_BLUESKY_POSTS_ENABLED) 37 + // If true, creates posts when users push images 38 + // Can be overridden per-hold via captain record's enableManifestPosts field 39 + EnableBlueskyPosts bool `yaml:"enable_bluesky_posts"` 35 40 } 36 41 37 42 // StorageConfig wraps distribution's storage configuration ··· 96 101 cfg.Registration.OwnerDID = os.Getenv("HOLD_OWNER") 97 102 cfg.Registration.AllowAllCrew = os.Getenv("HOLD_ALLOW_ALL_CREW") == "true" 98 103 cfg.Registration.ProfileAvatarURL = getEnvOrDefault("HOLD_PROFILE_AVATAR", "https://imgs.blue/evan.jarrett.net/1TpTOdtS60GdJWBYEqtK22y688jajbQ9a5kbYRFtwuqrkBAE") 104 + cfg.Registration.EnableBlueskyPosts = os.Getenv("HOLD_BLUESKY_POSTS_ENABLED") == "true" 99 105 100 106 // Database configuration (optional - enables embedded PDS) 101 107 // Note: HOLD_DATABASE_DIR is a directory path, carstore creates db.sqlite3 inside it
+6 -4
pkg/hold/oci/xrpc.go
··· 21 21 MultipartMgr *MultipartManager // Exported for access in route handlers 22 22 pds *pds.HoldPDS 23 23 httpClient pds.HTTPClient 24 + enableBlueskyPosts bool 24 25 } 25 26 26 27 // NewXRPCHandler creates a new OCI XRPC handler 27 - func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, driver storagedriver.StorageDriver, disablePresignedURLs bool, httpClient pds.HTTPClient) *XRPCHandler { 28 + func NewXRPCHandler(holdPDS *pds.HoldPDS, s3Service s3.S3Service, driver storagedriver.StorageDriver, disablePresignedURLs bool, enableBlueskyPosts bool, httpClient pds.HTTPClient) *XRPCHandler { 28 29 return &XRPCHandler{ 29 30 driver: driver, 30 31 disablePresignedURLs: disablePresignedURLs, ··· 32 33 s3Service: s3Service, 33 34 pds: holdPDS, 34 35 httpClient: httpClient, 36 + enableBlueskyPosts: enableBlueskyPosts, 35 37 } 36 38 } 37 39 ··· 242 244 } 243 245 244 246 // Check if manifest posts are enabled 245 - // TODO: Check captain record enableManifestPosts field 246 - // For now, posts are always created 247 - postsEnabled := true 247 + // Controlled by HOLD_BLUESKY_POSTS_ENABLED environment variable 248 + // TODO: Override with captain record enableManifestPosts field if set 249 + postsEnabled := h.enableBlueskyPosts 248 250 249 251 // Create layer records for each blob 250 252 layersCreated := 0
+2 -2
pkg/hold/oci/xrpc_test.go
··· 97 97 t.Fatalf("Failed to copy shared signing key: %v", err) 98 98 } 99 99 100 - holdPDS, err := pds.NewHoldPDS(ctx, holdDID, publicURL, dbPath, keyPath) 100 + holdPDS, err := pds.NewHoldPDS(ctx, holdDID, publicURL, dbPath, keyPath, false) 101 101 if err != nil { 102 102 t.Fatalf("Failed to create PDS: %v", err) 103 103 } ··· 126 126 127 127 // Create OCI handler with buffered mode (no S3) 128 128 mockS3 := s3.S3Service{} 129 - handler := NewXRPCHandler(holdPDS, mockS3, driver, true, mockClient) 129 + handler := NewXRPCHandler(holdPDS, mockS3, driver, true, false, mockClient) 130 130 131 131 return handler, ctx 132 132 }
+1 -1
pkg/hold/pds/captain_test.go
··· 28 28 t.Fatalf("Failed to copy shared signing key: %v", err) 29 29 } 30 30 31 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 31 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 32 32 if err != nil { 33 33 t.Fatalf("Failed to create test PDS: %v", err) 34 34 }
+4 -4
pkg/hold/pds/did_test.go
··· 84 84 keyPath := filepath.Join(tmpDir, "signing-key") 85 85 publicURL := "https://hold.example.com" 86 86 87 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", publicURL, dbPath, keyPath) 87 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", publicURL, dbPath, keyPath, false) 88 88 if err != nil { 89 89 t.Fatalf("Failed to create PDS: %v", err) 90 90 } ··· 183 183 keyPath := filepath.Join(tmpDir, "signing-key") 184 184 publicURL := "https://hold.example.com:8443" 185 185 186 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com:8443", publicURL, dbPath, keyPath) 186 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com:8443", publicURL, dbPath, keyPath, false) 187 187 if err != nil { 188 188 t.Fatalf("Failed to create PDS: %v", err) 189 189 } ··· 213 213 keyPath := filepath.Join(tmpDir, "signing-key") 214 214 publicURL := "https://hold.example.com" 215 215 216 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", publicURL, dbPath, keyPath) 216 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", publicURL, dbPath, keyPath, false) 217 217 if err != nil { 218 218 t.Fatalf("Failed to create PDS: %v", err) 219 219 } ··· 261 261 keyPath := filepath.Join(tmpDir, "signing-key") 262 262 publicURL := "https://hold.example.com" 263 263 264 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", publicURL, dbPath, keyPath) 264 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", publicURL, dbPath, keyPath, false) 265 265 if err != nil { 266 266 t.Fatalf("Failed to create PDS: %v", err) 267 267 }
+17 -15
pkg/hold/pds/server.go
··· 30 30 31 31 // HoldPDS is a minimal ATProto PDS implementation for a hold service 32 32 type HoldPDS struct { 33 - did string 34 - PublicURL string 35 - carstore carstore.CarStore 36 - repomgr *RepoManager 37 - dbPath string 38 - uid models.Uid 39 - signingKey *atcrypto.PrivateKeyK256 33 + did string 34 + PublicURL string 35 + carstore carstore.CarStore 36 + repomgr *RepoManager 37 + dbPath string 38 + uid models.Uid 39 + signingKey *atcrypto.PrivateKeyK256 40 + enableBlueskyPosts bool 40 41 } 41 42 42 43 // NewHoldPDS creates or opens a hold PDS with SQLite carstore 43 - func NewHoldPDS(ctx context.Context, did, publicURL, dbPath, keyPath string) (*HoldPDS, error) { 44 + func NewHoldPDS(ctx context.Context, did, publicURL, dbPath, keyPath string, enableBlueskyPosts bool) (*HoldPDS, error) { 44 45 // Generate or load signing key 45 46 signingKey, err := GenerateOrLoadKey(keyPath) 46 47 if err != nil { ··· 95 96 } 96 97 97 98 return &HoldPDS{ 98 - did: did, 99 - PublicURL: publicURL, 100 - carstore: cs, 101 - repomgr: rm, 102 - dbPath: dbPath, 103 - uid: uid, 104 - signingKey: signingKey, 99 + did: did, 100 + PublicURL: publicURL, 101 + carstore: cs, 102 + repomgr: rm, 103 + dbPath: dbPath, 104 + uid: uid, 105 + signingKey: signingKey, 106 + enableBlueskyPosts: enableBlueskyPosts, 105 107 }, nil 106 108 } 107 109
+11 -11
pkg/hold/pds/server_test.go
··· 23 23 did := "did:web:hold.example.com" 24 24 publicURL := "https://hold.example.com" 25 25 26 - pds, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath) 26 + pds, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, false) 27 27 if err != nil { 28 28 t.Fatalf("NewHoldPDS failed: %v", err) 29 29 } ··· 62 62 publicURL := "https://hold.example.com" 63 63 64 64 // Create first PDS instance and bootstrap it 65 - pds1, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath) 65 + pds1, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, false) 66 66 if err != nil { 67 67 t.Fatalf("First NewHoldPDS failed: %v", err) 68 68 } ··· 86 86 pds1.Close() 87 87 88 88 // Re-open the same database 89 - pds2, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath) 89 + pds2, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, false) 90 90 if err != nil { 91 91 t.Fatalf("Second NewHoldPDS failed: %v", err) 92 92 } ··· 118 118 dbPath := filepath.Join(tmpDir, "pds.db") 119 119 keyPath := filepath.Join(tmpDir, "signing-key") 120 120 121 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 121 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 122 122 if err != nil { 123 123 t.Fatalf("NewHoldPDS failed: %v", err) 124 124 } ··· 195 195 dbPath := filepath.Join(tmpDir, "pds.db") 196 196 keyPath := filepath.Join(tmpDir, "signing-key") 197 197 198 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 198 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 199 199 if err != nil { 200 200 t.Fatalf("NewHoldPDS failed: %v", err) 201 201 } ··· 261 261 dbPath := filepath.Join(tmpDir, "pds.db") 262 262 keyPath := filepath.Join(tmpDir, "signing-key") 263 263 264 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 264 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 265 265 if err != nil { 266 266 t.Fatalf("NewHoldPDS failed: %v", err) 267 267 } ··· 294 294 dbPath := filepath.Join(tmpDir, "pds.db") 295 295 keyPath := filepath.Join(tmpDir, "signing-key") 296 296 297 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 297 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 298 298 if err != nil { 299 299 t.Fatalf("NewHoldPDS failed: %v", err) 300 300 } ··· 344 344 dbPath := filepath.Join(tmpDir, "pds.db") 345 345 keyPath := filepath.Join(tmpDir, "signing-key") 346 346 347 - pds, err := NewHoldPDS(ctx, "did:web:hold01.atcr.io", "https://hold01.atcr.io", dbPath, keyPath) 347 + pds, err := NewHoldPDS(ctx, "did:web:hold01.atcr.io", "https://hold01.atcr.io", dbPath, keyPath, false) 348 348 if err != nil { 349 349 t.Fatalf("NewHoldPDS failed: %v", err) 350 350 } ··· 406 406 407 407 // Create hold with did:web 408 408 holdDID := "did:web:hold.example.com" 409 - pds, err := NewHoldPDS(ctx, holdDID, "https://hold.example.com", dbPath, keyPath) 409 + pds, err := NewHoldPDS(ctx, holdDID, "https://hold.example.com", dbPath, keyPath, false) 410 410 if err != nil { 411 411 t.Fatalf("NewHoldPDS failed: %v", err) 412 412 } ··· 474 474 dbPath := filepath.Join(tmpDir, "pds.db") 475 475 keyPath := filepath.Join(tmpDir, "signing-key") 476 476 477 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 477 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 478 478 if err != nil { 479 479 t.Fatalf("NewHoldPDS failed: %v", err) 480 480 } ··· 546 546 dbPath := filepath.Join(tmpDir, "pds.db") 547 547 keyPath := filepath.Join(tmpDir, "signing-key") 548 548 549 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 549 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 550 550 if err != nil { 551 551 t.Fatalf("NewHoldPDS failed: %v", err) 552 552 }
+6
pkg/hold/pds/status.go
··· 17 17 // status should be "online" or "offline" 18 18 // Each call creates a unique post with a TID-based rkey 19 19 func (p *HoldPDS) SetStatus(ctx context.Context, status string) error { 20 + // Check if Bluesky posts are enabled 21 + if !p.enableBlueskyPosts { 22 + fmt.Printf("Bluesky posts disabled, skipping status post: %s\n", status) 23 + return nil 24 + } 25 + 20 26 // Format the post text with emoji indicator 21 27 emoji := "🟢" 22 28 if status == "offline" {
+2 -2
pkg/hold/pds/status_test.go
··· 42 42 did := "did:web:test.example.com" 43 43 publicURL := "https://test.example.com" 44 44 45 - holdPDS, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath) 45 + holdPDS, err := NewHoldPDS(ctx, did, publicURL, dbPath, keyPath, true) 46 46 if err != nil { 47 47 t.Fatalf("Failed to create test PDS: %v", err) 48 48 } ··· 276 276 // Create one shared, bootstrapped PDS for read-only tests 277 277 // Use in-memory database for speed 278 278 sharedCtx = context.Background() 279 - sharedPDS, err = NewHoldPDS(sharedCtx, "did:web:hold.example.com", "https://hold.example.com", ":memory:", sharedTestKeyPath) 279 + sharedPDS, err = NewHoldPDS(sharedCtx, "did:web:hold.example.com", "https://hold.example.com", ":memory:", sharedTestKeyPath, true) 280 280 if err != nil { 281 281 panic(fmt.Sprintf("Failed to create shared PDS: %v", err)) 282 282 }
+2 -2
pkg/hold/pds/xrpc_test.go
··· 44 44 t.Fatalf("Failed to copy shared signing key: %v", err) 45 45 } 46 46 47 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 47 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 48 48 if err != nil { 49 49 t.Fatalf("Failed to create test PDS: %v", err) 50 50 } ··· 1377 1377 t.Fatalf("Failed to copy shared signing key: %v", err) 1378 1378 } 1379 1379 1380 - pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath) 1380 + pds, err := NewHoldPDS(ctx, "did:web:hold.example.com", "https://hold.example.com", dbPath, keyPath, false) 1381 1381 if err != nil { 1382 1382 t.Fatalf("Failed to create test PDS: %v", err) 1383 1383 }