···1+// Package appview implements the ATCR AppView component, which serves as the main
2+// OCI Distribution API server. It resolves identities (handle/DID to PDS endpoint),
3+// routes manifests to user's PDS, routes blobs to hold services, validates OAuth tokens,
4+// and issues registry JWTs. This package provides environment-based configuration,
5+// middleware registration, and HTTP server setup for the AppView service.
6package appview
78import (
···1+// Package db provides the database layer for the AppView web UI, including
2+// SQLite schema initialization, migrations, and query functions for OAuth
3+// sessions, device flows, repository metadata, stars, pull counts, and
4+// user profiles.
5package db
67import (
···1+// Package handlers provides HTTP handlers for the AppView web UI, including
2+// home page, repository browsing, search, user authentication, settings,
3+// device management, and API endpoints for the web interface.
4package handlers
56import (
···1+// Package holdhealth provides health checking for hold service endpoints.
2+// It periodically checks hold availability and caches health status with
3+// configurable TTL to avoid excessive health check requests.
4package holdhealth
56import (
+3
pkg/appview/holdhealth/checker_test.go
···131 status := checker.GetStatus(context.Background(), endpoint)
132 if status == nil {
133 t.Fatal("GetStatus returned nil")
0134 }
135136 if !status.Reachable {
···155 status := checker.GetStatus(context.Background(), server.URL)
156 if status == nil {
157 t.Fatal("GetStatus returned nil")
0158 }
159160 if !status.Reachable {
···191 status := checker.GetCachedStatus(endpoint)
192 if status == nil {
193 t.Fatal("Status not found in cache")
0194 }
195196 if !status.Reachable {
···131 status := checker.GetStatus(context.Background(), endpoint)
132 if status == nil {
133 t.Fatal("GetStatus returned nil")
134+ return
135 }
136137 if !status.Reachable {
···156 status := checker.GetStatus(context.Background(), server.URL)
157 if status == nil {
158 t.Fatal("GetStatus returned nil")
159+ return
160 }
161162 if !status.Reachable {
···193 status := checker.GetCachedStatus(endpoint)
194 if status == nil {
195 t.Fatal("Status not found in cache")
196+ return
197 }
198199 if !status.Reachable {
···1+// Package jetstream provides an ATProto Jetstream consumer for real-time updates.
2+// It connects to the Bluesky Jetstream WebSocket, processes repository events,
3+// indexes manifests and tags, and populates the AppView database for the web UI.
4package jetstream
56import (
···1+// Package licenses provides SPDX license validation and parsing for container
2+// image annotations. It embeds the official SPDX license list and provides
3+// functions to look up license identifiers, validate them, and parse
4+// multi-license strings with fuzzy matching support.
5package licenses
67//go:generate curl -fsSL -o spdx-licenses.json https://spdx.org/licenses/licenses.json
···1+// Package middleware provides HTTP middleware for AppView, including
2+// authentication (session-based for web UI, token-based for registry),
3+// identity resolution (handle/DID to PDS endpoint), and hold discovery
4+// for routing blobs to storage endpoints.
5package middleware
67import (
···1+// Package readme provides README fetching, rendering, and caching functionality
2+// for container repositories. It fetches markdown content from URLs, renders it
3+// to sanitized HTML using GitHub-flavored markdown, and caches the results in
4+// a database with configurable TTL.
5package readme
67import (
···1+// Package storage implements the storage routing layer for AppView.
2+// It routes manifests to ATProto PDS (as io.atcr.manifest records) and
3+// blobs to hold services via XRPC, with hold DID caching for efficient pulls.
4+// All storage operations are proxied - AppView stores nothing locally.
5package storage
67import (
+3-3
pkg/atproto/endpoints.go
···1-// Package xrpc provides constants for XRPC endpoint paths used throughout ATCR.
2//
3// This package serves as a single source of truth for all XRPC endpoint URLs,
4// preventing typos and making refactoring easier. All endpoint paths follow the
···15 // Response: {"uploadId": "..."}
16 HoldInitiateUpload = "/xrpc/io.atcr.hold.initiateUpload"
1718- // HoldGetPartUploadUrl gets a presigned URL or endpoint info for uploading a specific part.
19 // Method: POST
20 // Request: {"uploadId": "...", "partNumber": 1}
21 // Response: {"url": "...", "method": "PUT", "headers": {...}}
22- HoldGetPartUploadUrl = "/xrpc/io.atcr.hold.getPartUploadUrl"
2324 // HoldUploadPart handles direct buffered part uploads (alternative to presigned URLs).
25 // Method: PUT
···1+// Package atproto provides constants for XRPC endpoint paths used throughout ATCR.
2//
3// This package serves as a single source of truth for all XRPC endpoint URLs,
4// preventing typos and making refactoring easier. All endpoint paths follow the
···15 // Response: {"uploadId": "..."}
16 HoldInitiateUpload = "/xrpc/io.atcr.hold.initiateUpload"
1718+ // HoldGetPartUploadURL gets a presigned URL or endpoint info for uploading a specific part.
19 // Method: POST
20 // Request: {"uploadId": "...", "partNumber": 1}
21 // Response: {"url": "...", "method": "PUT", "headers": {...}}
22+ HoldGetPartUploadURL = "/xrpc/io.atcr.hold.getPartUploadUrl"
2324 // HoldUploadPart handles direct buffered part uploads (alternative to presigned URLs).
25 // Method: PUT
+1-1
pkg/atproto/profile.go
···9 "time"
10)
1112-// Profile record key is always "self" per lexicon
13const ProfileRKey = "self"
1415// Global map to track in-flight profile migrations (DID -> true)
···9 "time"
10)
1112+// ProfileRKey is always "self" per lexicon
13const ProfileRKey = "self"
1415// Global map to track in-flight profile migrations (DID -> true)
···1+// Package token provides service token caching and management for AppView.
2+// Service tokens are JWTs issued by a user's PDS to authorize AppView to
3+// act on their behalf when communicating with hold services. Tokens are
4+// cached with automatic expiry parsing and 10-second safety margins.
5package token
67import (
···1+// Package hold implements the ATCR hold service, which provides BYOS
2+// (Bring Your Own Storage) functionality. It includes an embedded PDS for
3+// storing captain and crew records, generates presigned URLs for blob storage,
4+// and handles authorization based on crew membership. Configuration is loaded
5+// entirely from environment variables.
6package hold
78import (
···1+// Package oci provides HTTP helpers for OCI registry endpoints in the hold service.
2+// It includes utilities for JSON encoding/decoding of request/response bodies
3+// and standardized error responses for XRPC endpoints.
4package oci
56import (
+1-1
pkg/hold/oci/multipart.go
···26 Buffered
27)
2829-// CompletedPart represents an uploaded part with its ETag
30type PartInfo struct {
31 PartNumber int `json:"part_number"`
32 ETag string `json:"etag"`
···26 Buffered
27)
2829+// PartInfo represents an uploaded part with its ETag
30type PartInfo struct {
31 PartNumber int `json:"part_number"`
32 ETag string `json:"etag"`
+3-3
pkg/hold/oci/xrpc.go
···44 r.Use(h.requireBlobWriteAccess)
4546 r.Post(atproto.HoldInitiateUpload, h.HandleInitiateUpload)
47- r.Post(atproto.HoldGetPartUploadUrl, h.HandleGetPartUploadUrl)
48 r.Put(atproto.HoldUploadPart, h.HandleUploadPart)
49 r.Post(atproto.HoldCompleteUpload, h.HandleCompleteUpload)
50 r.Post(atproto.HoldAbortUpload, h.HandleAbortUpload)
···80 })
81}
8283-// HandleGetPartUploadUrl returns a presigned URL or endpoint info for uploading a part
84// Replaces the old "action: part" pattern
85-func (h *XRPCHandler) HandleGetPartUploadUrl(w http.ResponseWriter, r *http.Request) {
86 var req struct {
87 UploadID string `json:"uploadId"`
88 PartNumber int `json:"partNumber"`
···44 r.Use(h.requireBlobWriteAccess)
4546 r.Post(atproto.HoldInitiateUpload, h.HandleInitiateUpload)
47+ r.Post(atproto.HoldGetPartUploadURL, h.HandleGetPartUploadURL)
48 r.Put(atproto.HoldUploadPart, h.HandleUploadPart)
49 r.Post(atproto.HoldCompleteUpload, h.HandleCompleteUpload)
50 r.Post(atproto.HoldAbortUpload, h.HandleAbortUpload)
···80 })
81}
8283+// HandleGetPartUploadURL returns a presigned URL or endpoint info for uploading a part
84// Replaces the old "action: part" pattern
85+func (h *XRPCHandler) HandleGetPartUploadURL(w http.ResponseWriter, r *http.Request) {
86 var req struct {
87 UploadID string `json:"uploadId"`
88 PartNumber int `json:"partNumber"`
+6-6
pkg/hold/oci/xrpc_test.go
···218 uploadID := initResp["uploadId"].(string)
219220 // Now get part upload URL
221- req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, map[string]any{
222 "uploadId": uploadID,
223 "partNumber": 1,
224 })
225 addMockAuth(req)
226227 w := httptest.NewRecorder()
228- handler.HandleGetPartUploadUrl(w, req)
229230 if w.Code != http.StatusOK {
231 t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
···246func TestHandleGetPartUploadUrl_InvalidSession(t *testing.T) {
247 handler, _ := setupTestOCIHandler(t)
248249- req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, map[string]any{
250 "uploadId": "invalid-upload-id",
251 "partNumber": 1,
252 })
253 addMockAuth(req)
254255 w := httptest.NewRecorder()
256- handler.HandleGetPartUploadUrl(w, req)
257258 if w.Code != http.StatusInternalServerError {
259 t.Errorf("Expected status 500, got %d", w.Code)
···274275 for _, tt := range tests {
276 t.Run(tt.name, func(t *testing.T) {
277- req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, tt.body)
278 addMockAuth(req)
279280 w := httptest.NewRecorder()
281- handler.HandleGetPartUploadUrl(w, req)
282283 if w.Code != http.StatusBadRequest {
284 t.Errorf("Expected status 400, got %d", w.Code)
···218 uploadID := initResp["uploadId"].(string)
219220 // Now get part upload URL
221+ req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, map[string]any{
222 "uploadId": uploadID,
223 "partNumber": 1,
224 })
225 addMockAuth(req)
226227 w := httptest.NewRecorder()
228+ handler.HandleGetPartUploadURL(w, req)
229230 if w.Code != http.StatusOK {
231 t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
···246func TestHandleGetPartUploadUrl_InvalidSession(t *testing.T) {
247 handler, _ := setupTestOCIHandler(t)
248249+ req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, map[string]any{
250 "uploadId": "invalid-upload-id",
251 "partNumber": 1,
252 })
253 addMockAuth(req)
254255 w := httptest.NewRecorder()
256+ handler.HandleGetPartUploadURL(w, req)
257258 if w.Code != http.StatusInternalServerError {
259 t.Errorf("Expected status 500, got %d", w.Code)
···274275 for _, tt := range tests {
276 t.Run(tt.name, func(t *testing.T) {
277+ req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, tt.body)
278 addMockAuth(req)
279280 w := httptest.NewRecorder()
281+ handler.HandleGetPartUploadURL(w, req)
282283 if w.Code != http.StatusBadRequest {
284 t.Errorf("Expected status 400, got %d", w.Code)
+1-1
pkg/hold/pds/repomgr.go
···1250 return rm.cs.WipeUserData(ctx, uid)
1251}
12521253-// technically identical to TakeDownRepo, for now
1254func (rm *RepoManager) ResetRepo(ctx context.Context, uid models.Uid) error {
1255 unlock := rm.lockUser(ctx, uid)
1256 defer unlock()
···1250 return rm.cs.WipeUserData(ctx, uid)
1251}
12521253+// ResetRepo is technically identical to TakeDownRepo, for now
1254func (rm *RepoManager) ResetRepo(ctx context.Context, uid models.Uid) error {
1255 unlock := rm.lockUser(ctx, uid)
1256 defer unlock()
+1-1
pkg/hold/pds/xrpc.go
···1223 json.NewEncoder(w).Encode(response)
1224}
12251226-// getPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
1227// Distinguishes between ATProto blobs (per-DID) and OCI blobs (content-addressed)
1228func (h *XRPCHandler) GetPresignedURL(ctx context.Context, operation string, digest string, did string) (string, error) {
1229 var path string
···1223 json.NewEncoder(w).Encode(response)
1224}
12251226+// GetPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
1227// Distinguishes between ATProto blobs (per-DID) and OCI blobs (content-addressed)
1228func (h *XRPCHandler) GetPresignedURL(ctx context.Context, operation string, digest string, did string) (string, error) {
1229 var path string
+8-4
pkg/s3/types.go
···0001package s3
23import (
4 "fmt"
0005 "github.com/aws/aws-sdk-go/aws"
6 "github.com/aws/aws-sdk-go/aws/credentials"
7 "github.com/aws/aws-sdk-go/aws/session"
8 "github.com/aws/aws-sdk-go/service/s3"
9- "log"
10- "strings"
11)
1213type S3Service struct {
···16 PathPrefix string // S3 path prefix (if any)
17}
1819-// initializes the S3 client for presigned URL generation
20// Returns nil error if S3 client is successfully initialized
21// Returns error if storage is not S3 or if initialization fails (service will fall back to proxy mode)
22func NewS3Service(params map[string]any, disablePresigned bool, storageType string) (*S3Service, error) {
···85 }, nil
86}
8788-// blobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path
89// Distribution stores blobs as: /docker/registry/v2/blobs/{algorithm}/{xx}/{hash}/data
90// where xx is the first 2 characters of the hash for directory sharding
91// NOTE: Path must start with / for filesystem driver
···1+// Package s3 provides S3 client initialization and presigned URL generation
2+// for hold services. It supports S3, Storj, and Minio storage backends,
3+// with fallback to buffered proxy mode when presigned URLs are unavailable.
4package s3
56import (
7 "fmt"
8+ "log"
9+ "strings"
10+11 "github.com/aws/aws-sdk-go/aws"
12 "github.com/aws/aws-sdk-go/aws/credentials"
13 "github.com/aws/aws-sdk-go/aws/session"
14 "github.com/aws/aws-sdk-go/service/s3"
0015)
1617type S3Service struct {
···20 PathPrefix string // S3 path prefix (if any)
21}
2223+// NewS3Service initializes the S3 client for presigned URL generation
24// Returns nil error if S3 client is successfully initialized
25// Returns error if storage is not S3 or if initialization fails (service will fall back to proxy mode)
26func NewS3Service(params map[string]any, disablePresigned bool, storageType string) (*S3Service, error) {
···89 }, nil
90}
9192+// BlobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path
93// Distribution stores blobs as: /docker/registry/v2/blobs/{algorithm}/{xx}/{hash}/data
94// where xx is the first 2 characters of the hash for directory sharding
95// NOTE: Path must start with / for filesystem driver