diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 84a1eb7..0000000 --- a/.claude/settings.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "permissions": { - "allow": [ - "WebSearch", - "WebFetch(domain:github.com)", - "WebFetch(domain:pkg.go.dev)", - "WebFetch(domain:distribution.github.io)", - "Write(*)", - "Edit(*)", - "Bash(find:*)", - "Bash(curl:*)", - "Bash(sed:*)", - "Bash(grep:*)", - "Bash(gofmt:*)", - "Bash(mkdir:*)", - "Bash(golangci-lint run:*)", - "Bash(go run:*)", - "Bash(go install:*)", - "Bash(go test:*)", - "Bash(go build:*)", - "Bash(go tool:*)", - "Bash(go vet:*)", - "Bash(go get:*)", - "Bash(go mod:*)", - "Bash(go get:*)" - ], - "deny": [], - "ask": [] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7f8beb7..fac4cce 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ bin/ .env # IDE +.claude/ .vscode/ .idea/ *.swp diff --git a/CLAUDE.md b/CLAUDE.md index 016e36a..1b3a9af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -285,6 +285,42 @@ Later (docker push): - Implements full `distribution.BlobStore` interface - Used when user has `io.atcr.hold` record +#### AppView Web UI (`pkg/appview/`) + +The AppView includes a web interface for browsing the registry: + +**Features:** +- Repository browsing and search +- Star/favorite repositories +- Pull count tracking +- User profiles and settings +- OAuth-based authentication for web users + +**Database Layer** (`pkg/appview/db/`): +- SQLite database for metadata (stars, pulls, repository info) +- Schema migrations via SQL files in `pkg/appview/db/schema.go` +- Stores: OAuth sessions, device flows, repository metadata +- **NOTE:** Simple SQLite for MVP. For production multi-instance: use PostgreSQL + +**Jetstream Integration** (`pkg/appview/jetstream/`): +- Consumes ATProto Jetstream for real-time updates +- Backfills repository records from PDS +- Indexes manifests, tags, and repository metadata +- Worker processes incoming events + +**Web Handlers** (`pkg/appview/handlers/`): +- `home.go` - Landing page +- `repository.go` - Repository detail pages +- `search.go` - Search functionality +- `auth.go` - OAuth login/logout for web +- `settings.go` - User settings management +- `api.go` - JSON API endpoints + +**Static Assets** (`pkg/appview/static/`, `pkg/appview/templates/`): +- Templates use Go html/template +- JavaScript in `static/js/app.js` +- Minimal CSS for clean UI + #### Hold Service (`cmd/hold/`) Lightweight standalone service for BYOS (Bring Your Own Storage): @@ -403,6 +439,8 @@ This ensures: - Name resolver under `middleware.registry` - Default storage endpoint: `middleware.registry.options.default_storage_endpoint` - Auth token signing keys and expiration +- Database path: `db.path` (SQLite database location) +- Jetstream endpoint: `jetstream.endpoint` (for ATProto event streaming) **Hold Service configuration** (environment variables): - Storage driver config via env vars: `STORAGE_DRIVER`, `AWS_*`, `S3_*` @@ -481,6 +519,20 @@ When writing tests: 4. Add case to `buildStorageConfig()` in `cmd/hold/main.go` 5. Update `.env.example` with new driver's env vars +**Working with the database**: +- Schema defined in `pkg/appview/db/schema.go` +- Queries in `pkg/appview/db/queries.go` +- Stores for OAuth, devices, sessions in separate files +- Run migrations automatically on startup +- Database path configurable via config.yml + +**Adding web UI features**: +- Add handler in `pkg/appview/handlers/` +- Register route in `cmd/appview/serve.go` +- Create template in `pkg/appview/templates/pages/` +- Use existing auth middleware for protected routes +- API endpoints return JSON, pages return HTML + ## Important Context Values When working with the codebase, these context values are used for routing: diff --git a/README.md b/README.md index e9b57a6..d095006 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,59 @@ ATCR is an OCI-compliant container registry that integrates with the AT Protocol ### Architecture -- **Manifests**: Stored as ATProto records in user PDSs (small JSON metadata) -- **Blobs/Layers**: Stored in S3 (large binary data) -- **Name Resolution**: Supports both ATProto handles and DIDs +ATCR consists of three main components: + +1. **AppView** - OCI registry server + web UI + - Serves OCI Distribution API (Docker push/pull) + - Resolves identities (handle/DID → PDS endpoint) + - Routes manifests to user's PDS, blobs to storage + - Web interface for browsing and search + - SQLite database for stars, pulls, metadata + +2. **Hold Service** - Optional storage service (BYOS) + - Lightweight HTTP server for presigned URLs + - Supports S3, Storj, Minio, filesystem, etc. + - Authorization via ATProto records + - Users can deploy their own hold + +3. **Credential Helper** - Client-side OAuth + - ATProto OAuth with DPoP + - Exchanges OAuth token for registry JWT + - Seamless Docker integration + +**Storage Model:** +- **Manifests** → ATProto records in user PDSs (small JSON metadata) +- **Blobs/Layers** → S3 or user's hold service (large binary data) +- **Name Resolution** → Supports both handles and DIDs - `atcr.io/alice.bsky.social/myimage:latest` - `atcr.io/did:plc:xyz123/myimage:latest` ## Features -- OCI Distribution Spec compliant -- ATProto-native manifest storage -- S3 blob storage for container layers -- DID/handle resolution -- Decentralized manifest ownership +### Core Registry +- **OCI Distribution Spec compliant** - Works with Docker, containerd, podman +- **ATProto-native manifest storage** - Manifests stored as records in user PDSs +- **Hybrid storage** - Small manifests in ATProto, large blobs in S3/BYOS +- **DID/handle resolution** - Supports both handles and DIDs for image names +- **Decentralized ownership** - Users own their manifest data via their PDS + +### Web Interface +- **Repository browser** - Browse and search container images +- **Star repositories** - Favorite images for quick access +- **Pull tracking** - View popularity and usage metrics +- **OAuth authentication** - Sign in with your ATProto identity +- **User profiles** - Manage your default storage hold + +### Authentication +- **ATProto OAuth with DPoP** - Cryptographic proof-of-possession tokens +- **Docker credential helper** - Seamless `docker push/pull` workflow +- **Token exchange** - OAuth tokens converted to registry JWTs + +### Storage +- **BYOS (Bring Your Own Storage)** - Deploy your own hold service +- **Multi-backend support** - S3, Storj, Minio, Azure, GCS, filesystem +- **Presigned URLs** - Direct client-to-storage uploads/downloads +- **Hold discovery** - Automatic routing based on user preferences ## Building @@ -35,20 +75,6 @@ docker build -t atcr.io/appview:latest . docker build -f Dockerfile.hold -t atcr.io/hold:latest . ``` -## Quick Start (Local Testing) - -**Automated setup:** -```bash -# Run the test script (handles everything) -./test-local.sh -``` - -The script will: -1. Create necessary directories (`/var/lib/atcr/*`) -2. Build all binaries -3. Start registry and hold service -4. Show you how to test - **Manual setup:** ```bash # 1. Create directories @@ -192,13 +218,29 @@ Key settings: ## Usage +### Configure Credential Helper (Recommended) + +```bash +# Build and configure the credential helper +go build -o docker-credential-atcr ./cmd/credential-helper +./docker-credential-atcr configure +# Follow the OAuth flow in your browser + +# Add to Docker config (~/.docker/config.json) +{ + "credHelpers": { + "atcr.io": "atcr" + } +} +``` + ### Pushing an Image ```bash # Tag your image docker tag myapp:latest atcr.io/alice/myapp:latest -# Push to ATCR +# Push to ATCR (credential helper handles auth) docker push atcr.io/alice/myapp:latest ``` @@ -209,19 +251,52 @@ docker push atcr.io/alice/myapp:latest docker pull atcr.io/alice/myapp:latest ``` +### Web Interface + +Visit the AppView URL (default: http://localhost:5000) to: +- Browse repositories +- Search for images +- Star your favorites +- View pull statistics +- Manage your storage settings + ## Development ### Project Structure ``` atcr.io/ -├── cmd/appview/ # AppView entrypoint +├── cmd/ +│ ├── appview/ # AppView entrypoint (registry + web UI) +│ ├── hold/ # Hold service entrypoint (BYOS) +│ └── credential-helper/ # Docker credential helper ├── pkg/ -│ ├── atproto/ # ATProto client and manifest store -│ ├── storage/ # S3 blob store and routing -│ ├── middleware/ # Registry and repository middleware -│ └── server/ # HTTP handlers -├── config/ # Configuration files +│ ├── appview/ # Web UI components +│ │ ├── handlers/ # HTTP handlers (home, repo, search, auth) +│ │ ├── db/ # SQLite database layer +│ │ ├── jetstream/ # ATProto Jetstream consumer +│ │ ├── static/ # JS, CSS assets +│ │ └── templates/ # HTML templates +│ ├── atproto/ # ATProto integration +│ │ ├── client.go # PDS client +│ │ ├── resolver.go # DID/handle resolution +│ │ ├── manifest_store.go # OCI manifest store +│ │ ├── lexicon.go # ATProto record schemas +│ │ └── profile.go # Sailor profile management +│ ├── storage/ # Storage layer +│ │ ├── routing_repository.go # Routes manifests/blobs +│ │ ├── proxy_blob_store.go # BYOS proxy +│ │ ├── s3_blob_store.go # S3 wrapper +│ │ └── hold_cache.go # Hold endpoint cache +│ ├── middleware/ # Registry middleware +│ │ ├── registry.go # Name resolution +│ │ └── repository.go # Storage routing +│ └── auth/ # Authentication +│ ├── oauth/ # ATProto OAuth with DPoP +│ ├── token/ # JWT issuer/validator +│ └── atproto/ # Session validation +├── config/ # Configuration files +├── docs/ # Documentation └── Dockerfile ``` diff --git a/cmd/appview/serve.go b/cmd/appview/serve.go index f3ce948..8c5a7df 100644 --- a/cmd/appview/serve.go +++ b/cmd/appview/serve.go @@ -541,6 +541,8 @@ func initializeUIRoutes(database *sql.DB, readOnlyDB *sql.DB, sessionStore *db.S DB: readOnlyDB, Templates: templates, RegistryURL: uihandlers.TrimRegistryURL(baseURL), + Directory: oauthApp.Directory(), + Refresher: refresher, }, )).Methods("GET") diff --git a/SPEC.md b/docs/SPEC.md similarity index 100% rename from SPEC.md rename to docs/SPEC.md diff --git a/pkg/appview/handlers/repository.go b/pkg/appview/handlers/repository.go index b59cabe..b06b51a 100644 --- a/pkg/appview/handlers/repository.go +++ b/pkg/appview/handlers/repository.go @@ -3,9 +3,14 @@ package handlers import ( "database/sql" "html/template" + "log" "net/http" "atcr.io/pkg/appview/db" + "atcr.io/pkg/appview/middleware" + "atcr.io/pkg/atproto" + "atcr.io/pkg/auth/oauth" + "github.com/bluesky-social/indigo/atproto/identity" "github.com/gorilla/mux" ) @@ -14,6 +19,8 @@ type RepositoryPageHandler struct { DB *sql.DB Templates *template.Template RegistryURL string + Directory identity.Directory + Refresher *oauth.Refresher } func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -45,14 +52,44 @@ func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } + // Fetch star count + stats, err := db.GetRepositoryStats(h.DB, owner.DID, repository) + if err != nil { + log.Printf("Failed to fetch repository stats: %v", err) + // Continue with zero stats on error + stats = &db.RepositoryStats{StarCount: 0} + } + + // Check if current user has starred this repo + isStarred := false + user := middleware.GetUser(r) + if user != nil && h.Refresher != nil && h.Directory != nil { + // Get OAuth session for the authenticated user + session, err := h.Refresher.GetSession(r.Context(), user.DID) + if err == nil { + // Get user's PDS client + apiClient := session.APIClient() + pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient) + + // Check if star record exists + rkey := atproto.StarRecordKey(owner.DID, repository) + _, err = pdsClient.GetRecord(r.Context(), atproto.StarCollection, rkey) + isStarred = (err == nil) + } + } + data := struct { PageData Owner *db.User // Repository owner Repository *db.Repository + StarCount int + IsStarred bool }{ PageData: NewPageData(r, h.RegistryURL), Owner: owner, Repository: repo, + StarCount: stats.StarCount, + IsStarred: isStarred, } if err := h.Templates.ExecuteTemplate(w, "repository", data); err != nil { diff --git a/pkg/appview/static/js/app.js b/pkg/appview/static/js/app.js index 50eb0c2..ddb6ca1 100644 --- a/pkg/appview/static/js/app.js +++ b/pkg/appview/static/js/app.js @@ -115,9 +115,6 @@ document.addEventListener('DOMContentLoaded', () => { dropdownMenu.setAttribute('hidden', ''); } } - - // Load star status on repository page - loadStarStatus(); }); // Toggle star on a repository diff --git a/pkg/appview/templates/pages/repository.html b/pkg/appview/templates/pages/repository.html index b5fec9e..263b2df 100644 --- a/pkg/appview/templates/pages/repository.html +++ b/pkg/appview/templates/pages/repository.html @@ -36,9 +36,9 @@