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

Configure Feed

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

update indigo repo, fix pds carstore

+58 -44
+2 -2
go.mod
··· 4 4 5 5 require ( 6 6 github.com/aws/aws-sdk-go v1.55.5 7 - github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e 7 + github.com/bluesky-social/indigo v0.0.0-20251014222321-1e8718ae9f33 8 8 github.com/distribution/distribution/v3 v3.0.0 9 9 github.com/distribution/reference v0.6.0 10 10 github.com/golang-jwt/jwt/v5 v5.2.2 ··· 25 25 require ( 26 26 github.com/beorn7/perks v1.0.1 // indirect 27 27 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect 28 - github.com/carlmjohnson/versioninfo v0.22.5 // indirect 29 28 github.com/cenkalti/backoff/v4 v4.3.0 // indirect 30 29 github.com/cespare/xxhash/v2 v2.3.0 // indirect 31 30 github.com/coreos/go-systemd/v22 v22.5.0 // indirect ··· 33 32 github.com/docker/docker-credential-helpers v0.8.2 // indirect 34 33 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect 35 34 github.com/docker/go-metrics v0.0.1 // indirect 35 + github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect 36 36 github.com/felixge/httpsnoop v1.0.4 // indirect 37 37 github.com/go-jose/go-jose/v4 v4.1.2 // indirect 38 38 github.com/go-logr/logr v1.4.2 // indirect
+4 -4
go.sum
··· 16 16 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 17 17 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= 18 18 github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 19 - github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e h1:IutKPwmbU0LrYqw03EuwJtMdAe67rDTrL1U8S8dicRU= 20 - github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8= 19 + github.com/bluesky-social/indigo v0.0.0-20251014222321-1e8718ae9f33 h1:x06Y6VyYUCvqWl2AS4/3NBBbRf8wWNMd3YrI44NTHS8= 20 + github.com/bluesky-social/indigo v0.0.0-20251014222321-1e8718ae9f33/go.mod h1:GuGAU33qKulpZCZNPcUeIQ4RW6KzNvOy7s8MSUXbAng= 21 21 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= 22 22 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 23 23 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= ··· 28 28 github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 29 29 github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 30 30 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 31 - github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 32 - github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 33 31 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 34 32 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 35 33 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= ··· 60 58 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= 61 59 github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 62 60 github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 61 + github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg= 62 + github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw= 63 63 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 64 64 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 65 65 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+4 -4
pkg/atproto/client.go
··· 11 11 "net/http" 12 12 "strings" 13 13 14 - "github.com/bluesky-social/indigo/atproto/client" 14 + "github.com/bluesky-social/indigo/atproto/atclient" 15 15 ) 16 16 17 17 // Sentinel errors ··· 26 26 accessToken string // For Basic Auth only 27 27 httpClient *http.Client 28 28 useIndigoClient bool // true if using indigo's OAuth client (handles auth automatically) 29 - indigoClient *client.APIClient // indigo's API client for OAuth requests 29 + indigoClient *atclient.APIClient // indigo's API client for OAuth requests 30 30 } 31 31 32 32 // NewClient creates a new ATProto client for Basic Auth tokens (app passwords) ··· 41 41 42 42 // NewClientWithIndigoClient creates an ATProto client using indigo's API client 43 43 // This uses indigo's native XRPC methods with automatic DPoP handling 44 - func NewClientWithIndigoClient(pdsEndpoint, did string, indigoClient *client.APIClient) *Client { 44 + func NewClientWithIndigoClient(pdsEndpoint, did string, indigoClient *atclient.APIClient) *Client { 45 45 return &Client{ 46 46 pdsEndpoint: pdsEndpoint, 47 47 did: did, ··· 125 125 err := c.indigoClient.Get(ctx, "com.atproto.repo.getRecord", params, &result) 126 126 if err != nil { 127 127 // Check for RecordNotFound error from indigo's APIError type 128 - var apiErr *client.APIError 128 + var apiErr *atclient.APIError 129 129 if errors.As(err, &apiErr) { 130 130 if apiErr.StatusCode == 404 || apiErr.Name == "RecordNotFound" { 131 131 return nil, ErrRecordNotFound
+17
pkg/hold/pds/crew.go
··· 5 5 "fmt" 6 6 "time" 7 7 8 + "github.com/bluesky-social/indigo/repo" 8 9 "github.com/ipfs/go-cid" 9 10 ) 10 11 ··· 40 41 if err != nil { 41 42 return cid.Undef, fmt.Errorf("failed to persist commit: %w", err) 42 43 } 44 + 45 + // Create a new session for the next operation (old session is now closed) 46 + newSession, err := p.carstore.NewDeltaSession(ctx, p.uid, nil) 47 + if err != nil { 48 + return cid.Undef, fmt.Errorf("failed to create new session: %w", err) 49 + } 50 + 51 + // Load repo from the newly committed head (not NewRepo which creates empty MST) 52 + newRepo, err := repo.OpenRepo(ctx, newSession, root) 53 + if err != nil { 54 + return cid.Undef, fmt.Errorf("failed to reload repo after commit: %w", err) 55 + } 56 + 57 + // Update the stored session and repo 58 + p.session = newSession 59 + p.repo = newRepo 43 60 44 61 return recordCID, nil 45 62 }
+6 -6
pkg/hold/pds/keys.go
··· 5 5 "os" 6 6 "path/filepath" 7 7 8 - "github.com/bluesky-social/indigo/atproto/crypto" 8 + "github.com/bluesky-social/indigo/atproto/atcrypto" 9 9 ) 10 10 11 11 // GenerateOrLoadKey generates a new K256 key pair or loads an existing one 12 - func GenerateOrLoadKey(keyPath string) (*crypto.PrivateKeyK256, error) { 12 + func GenerateOrLoadKey(keyPath string) (*atcrypto.PrivateKeyK256, error) { 13 13 // Ensure directory exists 14 14 dir := filepath.Dir(keyPath) 15 15 if err := os.MkdirAll(dir, 0700); err != nil { ··· 27 27 } 28 28 29 29 // generateKey creates a new K256 (secp256k1) key pair using indigo's atcrypto 30 - func generateKey(keyPath string) (*crypto.PrivateKeyK256, error) { 30 + func generateKey(keyPath string) (*atcrypto.PrivateKeyK256, error) { 31 31 // Generate K256 key (secp256k1) using indigo 32 - privateKey, err := crypto.GeneratePrivateKeyK256() 32 + privateKey, err := atcrypto.GeneratePrivateKeyK256() 33 33 if err != nil { 34 34 return nil, fmt.Errorf("failed to generate key: %w", err) 35 35 } ··· 47 47 } 48 48 49 49 // loadKey loads an existing private key from disk 50 - func loadKey(keyPath string) (*crypto.PrivateKeyK256, error) { 50 + func loadKey(keyPath string) (*atcrypto.PrivateKeyK256, error) { 51 51 // Read key bytes 52 52 keyBytes, err := os.ReadFile(keyPath) 53 53 if err != nil { ··· 55 55 } 56 56 57 57 // Try to parse as K256 private key 58 - privateKey, err := crypto.ParsePrivateBytesK256(keyBytes) 58 + privateKey, err := atcrypto.ParsePrivateBytesK256(keyBytes) 59 59 if err != nil { 60 60 // Check if this is an old P-256 PEM key (migration) 61 61 if isPEMFormat(keyBytes) {
+25 -28
pkg/hold/pds/server.go
··· 6 6 "os" 7 7 "path/filepath" 8 8 9 - "github.com/bluesky-social/indigo/atproto/crypto" 9 + "github.com/bluesky-social/indigo/atproto/atcrypto" 10 10 "github.com/bluesky-social/indigo/carstore" 11 11 "github.com/bluesky-social/indigo/models" 12 12 "github.com/bluesky-social/indigo/repo" ··· 21 21 repo *repo.Repo 22 22 dbPath string 23 23 uid models.Uid 24 - signingKey *crypto.PrivateKeyK256 24 + signingKey *atcrypto.PrivateKeyK256 25 25 } 26 26 27 27 // NewHoldPDS creates or opens a hold PDS with SQLite carstore ··· 45 45 return nil, fmt.Errorf("failed to create sqlite store: %w", err) 46 46 } 47 47 48 - cs := sqlStore.CarStore() 48 + // Use SQLiteStore directly, not the CarStore() wrapper 49 + // The wrapper has a bug where GetUserRepoHead checks CarShard.ID which SQLite doesn't populate 50 + cs := sqlStore 49 51 50 52 // For a single-user hold, we use a fixed UID (1) 51 53 uid := models.Uid(1) 52 54 53 - // Try to get existing repo head 54 - _, err = cs.GetUserRepoHead(ctx, uid) 55 + // Check if repo already exists with valid head 56 + head, err := cs.GetUserRepoHead(ctx, uid) 57 + hasValidRepo := (err == nil && head.Defined()) 55 58 56 59 var session *carstore.DeltaSession 57 60 var r *repo.Repo 58 61 62 + // Create a session connected to this user's data in carstore 63 + session, err = cs.NewDeltaSession(ctx, uid, nil) 59 64 if err != nil { 60 - // Repo doesn't exist yet, create new delta session 61 - session, err = cs.NewDeltaSession(ctx, uid, nil) 62 - if err != nil { 63 - return nil, fmt.Errorf("failed to create delta session: %w", err) 64 - } 65 + return nil, fmt.Errorf("failed to create delta session: %w", err) 66 + } 65 67 66 - // Create new repo with session as blockstore (needs pointer) 68 + if !hasValidRepo { 69 + // No valid repo - create new empty repo 67 70 r = repo.NewRepo(ctx, did, session) 68 71 } else { 69 - // TODO: Load existing repo 70 - // For now, just create a new session 71 - session, err = cs.NewDeltaSession(ctx, uid, nil) 72 + // Repo exists with valid head - load from existing head 73 + r, err = repo.OpenRepo(ctx, session, head) 72 74 if err != nil { 73 - return nil, fmt.Errorf("failed to create delta session: %w", err) 75 + return nil, fmt.Errorf("failed to open existing repo: %w", err) 74 76 } 75 - 76 - r = repo.NewRepo(ctx, did, session) 77 77 } 78 78 79 79 return &HoldPDS{ ··· 94 94 } 95 95 96 96 // SigningKey returns the hold's signing key 97 - func (p *HoldPDS) SigningKey() *crypto.PrivateKeyK256 { 97 + func (p *HoldPDS) SigningKey() *atcrypto.PrivateKeyK256 { 98 98 return p.signingKey 99 99 } 100 100 ··· 106 106 107 107 // Check if repo already has commits 108 108 head, err := p.carstore.GetUserRepoHead(ctx, p.uid) 109 - if err == nil { 110 - // Repo exists - check if we need to re-bootstrap due to key change 111 - // If the repo exists but is empty/invalid, we should re-bootstrap 112 - if head.String() == "" || head.String() == "b" { 113 - fmt.Printf("⚠️ Detected invalid repo state, re-bootstrapping...\n") 114 - } else { 115 - fmt.Printf("⏭️ Skipping PDS bootstrap: repo already initialized (head: %s)\n", head.String()[:16]) 116 - return nil 117 - } 109 + if err != nil || !head.Defined() { 110 + // No repo exists yet, bootstrap 111 + fmt.Printf("🚀 Bootstrapping hold PDS with owner: %s\n", ownerDID) 112 + } else { 113 + // Repo exists and is valid 114 + fmt.Printf("⏭️ Skipping PDS bootstrap: repo already initialized (head: %s)\n", head.String()[:16]) 115 + return nil 118 116 } 119 117 120 118 // Add hold owner as first crew member with admin role 121 - fmt.Printf("🚀 Bootstrapping hold PDS with owner: %s\n", ownerDID) 122 119 _, err = p.AddCrewMember(ctx, ownerDID, "admin", []string{"blob:read", "blob:write", "crew:admin"}) 123 120 if err != nil { 124 121 return fmt.Errorf("failed to add owner as crew member: %w", err)