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 issue with mismatched scopes locally

+145 -21
+2 -1
Dockerfile.appview
··· 39 39 org.opencontainers.image.documentation="https://tangled.org/@evan.jarrett.net/at-container-registry" \ 40 40 org.opencontainers.image.licenses="MIT" \ 41 41 org.opencontainers.image.version="0.1.0" \ 42 - io.atcr.icon="https://imgs.blue/evan.jarrett.net/1TpTNrRelfloN2emuWZDrWmPT0o93bAjEnozjD6UPgoVV9m4" 42 + io.atcr.icon="https://imgs.blue/evan.jarrett.net/1TpTNrRelfloN2emuWZDrWmPT0o93bAjEnozjD6UPgoVV9m4" \ 43 + io.atcr.readme="https://tangled.org/@evan.jarrett.net/at-container-registry/raw/main/docs/appview.md" 43 44 44 45 ENTRYPOINT ["/atcr-appview"] 45 46 CMD ["serve"]
+13 -9
cmd/appview/serve.go
··· 154 154 // The extraction function normalizes URLs to DIDs for consistency 155 155 defaultHoldDID := appview.ExtractDefaultHoldDID(config) 156 156 157 + // Extract test mode from config (needed for OAuth scope configuration) 158 + testMode := appview.ExtractTestMode(config) 159 + if testMode { 160 + fmt.Println("TEST_MODE enabled - will use HTTP for local DID resolution and transition:generic scope") 161 + } 162 + 157 163 // Create OAuth app (indigo client) 158 - oauthApp, err := oauth.NewApp(baseURL, oauthStore, defaultHoldDID) 164 + oauthApp, err := oauth.NewApp(baseURL, oauthStore, defaultHoldDID, testMode) 159 165 if err != nil { 160 166 return fmt.Errorf("failed to create OAuth app: %w", err) 161 167 } 162 - fmt.Println("Using full OAuth scopes (including blob: scope)") 168 + if testMode { 169 + fmt.Println("Using OAuth scopes with transition:generic (test mode)") 170 + } else { 171 + fmt.Println("Using OAuth scopes with RPC scope (production mode)") 172 + } 163 173 164 174 // Invalidate sessions with mismatched scopes on startup 165 175 // This ensures all users have the latest required scopes after deployment 166 - desiredScopes := oauth.GetDefaultScopes(defaultHoldDID) 176 + desiredScopes := oauth.GetDefaultScopes(defaultHoldDID, testMode) 167 177 invalidatedCount, err := oauthStore.InvalidateSessionsWithMismatchedScopes(context.Background(), desiredScopes) 168 178 if err != nil { 169 179 fmt.Printf("Warning: Failed to invalidate sessions with mismatched scopes: %v\n", err) ··· 185 195 // Set global database for pull/push metrics tracking 186 196 metricsDB := db.NewMetricsDB(uiDatabase) 187 197 middleware.SetGlobalDatabase(metricsDB) 188 - 189 - // Extract test mode from config 190 - testMode := appview.ExtractTestMode(config) 191 - if testMode { 192 - fmt.Println("TEST_MODE enabled - will use HTTP for local DID resolution") 193 - } 194 198 195 199 // Create RemoteHoldAuthorizer for hold authorization with caching 196 200 holdAuthorizer := auth.NewRemoteHoldAuthorizer(uiDatabase, testMode)
+104
docs/appview.md
··· 1 + # ATCR AppView 2 + 3 + The **AppView** is the OCI-compliant registry server for ATCR (ATProto Container Registry). It provides the Docker Registry HTTP API V2 and a web interface for browsing container images. 4 + 5 + ## What is AppView? 6 + 7 + AppView serves as the central registry server that: 8 + 9 + - **Serves OCI Distribution API** - Compatible with Docker, containerd, podman, and other OCI clients 10 + - **Resolves ATProto identities** - Converts handles and DIDs to PDS endpoints 11 + - **Routes manifests** - Stores container manifests as ATProto records in users' Personal Data Servers 12 + - **Routes blobs** - Proxies blob operations to hold services (S3-compatible storage) 13 + - **Provides web UI** - Browse, search, and star repositories 14 + 15 + ## Image Format 16 + 17 + Container images use ATProto identities: 18 + 19 + ``` 20 + atcr.io/alice.bsky.social/myapp:latest 21 + atcr.io/did:plc:xyz123/myapp:latest 22 + ``` 23 + 24 + ## Using ATCR 25 + 26 + ### Push Images 27 + 28 + ```bash 29 + # Install credential helper 30 + curl -fsSL https://atcr.io/install.sh | bash 31 + 32 + # Configure Docker (add to ~/.docker/config.json) 33 + { 34 + "credHelpers": { 35 + "atcr.io": "atcr" 36 + } 37 + } 38 + 39 + # Push images (authenticates automatically) 40 + docker tag myapp:latest atcr.io/yourhandle/myapp:latest 41 + docker push atcr.io/yourhandle/myapp:latest 42 + ``` 43 + 44 + ### Pull Images 45 + 46 + ```bash 47 + # Public images (no auth required) 48 + docker pull atcr.io/alice.bsky.social/myapp:latest 49 + 50 + # Private images (automatic OAuth authentication) 51 + docker pull atcr.io/yourhandle/private-app:latest 52 + ``` 53 + 54 + ## Running Your Own AppView 55 + 56 + Deploy your own registry instance with Docker Compose: 57 + 58 + ```bash 59 + # Create configuration 60 + cp .env.appview.example .env.appview 61 + # Edit .env.appview with your settings 62 + 63 + # Start services 64 + docker-compose up -d 65 + ``` 66 + 67 + ### Configuration 68 + 69 + Key environment variables: 70 + 71 + - `ATCR_HTTP_ADDR` - HTTP listen address (default: `:5000`) 72 + - `ATCR_BASE_URL` - Public URL for OAuth/JWT realm 73 + - `ATCR_DEFAULT_HOLD_DID` - Default hold service DID for blob storage (required) 74 + - `ATCR_UI_ENABLED` - Enable web interface (default: `true`) 75 + - `JETSTREAM_URL` - ATProto event stream URL for real-time updates 76 + 77 + See [deployment documentation](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md) for production setup. 78 + 79 + ## Features 80 + 81 + - ✅ **OCI-compliant** - Full Docker Registry API V2 support 82 + - ✅ **ATProto OAuth** - Secure authentication with DPoP 83 + - ✅ **Decentralized storage** - Manifests stored in users' PDS 84 + - ✅ **Web UI** - Browse repositories, view tags, search images 85 + - ✅ **Real-time updates** - Jetstream integration for live indexing 86 + - ✅ **Multi-arch support** - ARM64, AMD64, and other platforms 87 + - ✅ **BYOS** - Bring Your Own Storage via hold services 88 + 89 + ## Storage Architecture 90 + 91 + **Hybrid model:** 92 + - **Manifests** → ATProto records in user's PDS (small JSON metadata) 93 + - **Blobs** → Hold services with S3-compatible backends (large binary layers) 94 + 95 + This design keeps metadata portable and federated while leveraging cheap blob storage for layers. 96 + 97 + ## License 98 + 99 + MIT 100 + 101 + --- 102 + 103 + **Documentation:** https://tangled.org/@evan.jarrett.net/at-container-registry 104 + **Source Code:** https://tangled.org/@evan.jarrett.net/at-container-registry
+2 -2
go.mod
··· 20 20 github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 21 21 github.com/klauspost/compress v1.18.0 22 22 github.com/mattn/go-sqlite3 v1.14.32 23 + github.com/microcosm-cc/bluemonday v1.0.27 23 24 github.com/multiformats/go-multihash v0.2.3 24 25 github.com/opencontainers/go-digest v1.0.0 25 26 github.com/spf13/cobra v1.8.0 26 27 github.com/whyrusleeping/cbor-gen v0.3.1 28 + github.com/yuin/goldmark v1.7.13 27 29 go.opentelemetry.io/otel v1.32.0 28 30 go.yaml.in/yaml/v4 v4.0.0-rc.2 29 31 golang.org/x/crypto v0.39.0 ··· 87 89 github.com/jmespath/go-jmespath v0.4.0 // indirect 88 90 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 89 91 github.com/mattn/go-isatty v0.0.20 // indirect 90 - github.com/microcosm-cc/bluemonday v1.0.27 // indirect 91 92 github.com/minio/sha256-simd v1.0.1 // indirect 92 93 github.com/mr-tron/base58 v1.2.0 // indirect 93 94 github.com/multiformats/go-base32 v0.1.0 // indirect ··· 108 109 github.com/sirupsen/logrus v1.9.3 // indirect 109 110 github.com/spaolacci/murmur3 v1.1.0 // indirect 110 111 github.com/spf13/pflag v1.0.5 // indirect 111 - github.com/yuin/goldmark v1.7.13 // indirect 112 112 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 113 113 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 114 114 go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect
+1 -1
pkg/appview/readme/fetcher.go
··· 29 29 // Configure markdown renderer with GitHub-flavored markdown 30 30 md := goldmark.New( 31 31 goldmark.WithExtensions( 32 - extension.GFM, // GitHub Flavored Markdown 32 + extension.GFM, // GitHub Flavored Markdown 33 33 extension.Typographer, // Smart quotes, dashes, etc. 34 34 ), 35 35 goldmark.WithParserOptions(
+20 -7
pkg/auth/oauth/client.go
··· 20 20 } 21 21 22 22 // NewApp creates a new OAuth app for ATCR with default scopes 23 - func NewApp(baseURL string, store oauth.ClientAuthStore, holdDid string) (*App, error) { 24 - return NewAppWithScopes(baseURL, store, GetDefaultScopes(holdDid)) 23 + func NewApp(baseURL string, store oauth.ClientAuthStore, holdDid string, testMode bool) (*App, error) { 24 + return NewAppWithScopes(baseURL, store, GetDefaultScopes(holdDid, testMode)) 25 25 } 26 26 27 27 // NewAppWithScopes creates a new OAuth app for ATCR with custom scopes ··· 120 120 } 121 121 122 122 // GetDefaultScopes returns the default OAuth scopes for ATCR registry operations 123 - func GetDefaultScopes(did string) []string { 124 - return []string{ 123 + // testMode determines whether to use transition:generic (test) or rpc scopes (production) 124 + func GetDefaultScopes(did string, testMode bool) []string { 125 + scopes := []string{ 125 126 "atproto", 126 - "transition:generic", 127 127 // Image manifest types (single-arch) 128 128 "blob:application/vnd.oci.image.manifest.v1+json", 129 129 "blob:application/vnd.docker.distribution.manifest.v2+json", ··· 132 132 "blob:application/vnd.docker.distribution.manifest.list.v2+json", 133 133 // OCI artifact manifests (for cosign signatures, SBOMs, attestations) 134 134 "blob:application/vnd.cncf.oras.artifact.manifest.v1+json", 135 - fmt.Sprintf("rpc:com.atproto.repo.getRecord?aud=%s#atcr_hold", did), 135 + } 136 + 137 + // In test mode: use transition:generic (local dev with test PDS) 138 + // In production: use rpc scope for service auth 139 + if testMode { 140 + scopes = append(scopes, "transition:generic") 141 + } else { 142 + scopes = append(scopes, fmt.Sprintf("rpc:com.atproto.repo.getRecord?aud=%s#atcr_hold", did)) 143 + } 144 + 145 + // Add repo scopes 146 + scopes = append(scopes, 136 147 fmt.Sprintf("repo:%s", atproto.ManifestCollection), 137 148 fmt.Sprintf("repo:%s", atproto.TagCollection), 138 149 fmt.Sprintf("repo:%s", atproto.StarCollection), 139 150 fmt.Sprintf("repo:%s", atproto.SailorProfileCollection), 140 - } 151 + ) 152 + 153 + return scopes 141 154 } 142 155 143 156 // ScopesMatch checks if two scope lists are equivalent (order-independent)
+3 -1
pkg/auth/oauth/interactive.go
··· 33 33 } 34 34 35 35 // Create OAuth app with custom scopes (or defaults if nil) 36 + // Interactive flows are typically for production use (credential helper, etc.) 37 + // so we default to testMode=false 36 38 var app *App 37 39 if scopes != nil { 38 40 app, err = NewAppWithScopes(baseURL, store, scopes) 39 41 } else { 40 - app, err = NewApp(baseURL, store, "*") 42 + app, err = NewApp(baseURL, store, "*", false) 41 43 } 42 44 if err != nil { 43 45 return nil, fmt.Errorf("failed to create OAuth app: %w", err)
test-e2e.sh scripts/test-e2e.sh