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.

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 }