+26
.golangci.yml
+26
.golangci.yml
···
···
1
+
# golangci-lint configuration for ATCR
2
+
# See: https://golangci-lint.run/usage/configuration/
3
+
version: "2"
4
+
linters:
5
+
settings:
6
+
staticcheck:
7
+
checks:
8
+
- "all"
9
+
- "-SA1019" # Ignore deprecated package warnings for github.com/ipfs/go-ipfs-blockstore
10
+
# Cannot upgrade to github.com/ipfs/boxo/blockstore due to opentelemetry
11
+
# dependency conflicts with distribution/distribution
12
+
errcheck:
13
+
exclude-functions:
14
+
- (github.com/distribution/distribution/v3/registry/storage/driver.FileWriter).Cancel
15
+
- (github.com/distribution/distribution/v3.BlobWriter).Cancel
16
+
- (*database/sql.Tx).Rollback
17
+
- (*database/sql.Rows).Close
18
+
- (*net/http.Server).Shutdown
19
+
20
+
exclusions:
21
+
presets:
22
+
- std-error-handling
23
+
formatters:
24
+
enable:
25
+
- gofmt
26
+
- goimports
+5
pkg/appview/config.go
+5
pkg/appview/config.go
···
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.
6
package appview
7
8
import (
+4
pkg/appview/db/schema.go
+4
pkg/appview/db/schema.go
+3
pkg/appview/handlers/home.go
+3
pkg/appview/handlers/home.go
+3
pkg/appview/holdhealth/checker.go
+3
pkg/appview/holdhealth/checker.go
+3
pkg/appview/holdhealth/checker_test.go
+3
pkg/appview/holdhealth/checker_test.go
···
131
status := checker.GetStatus(context.Background(), endpoint)
132
if status == nil {
133
t.Fatal("GetStatus returned nil")
134
}
135
136
if !status.Reachable {
···
155
status := checker.GetStatus(context.Background(), server.URL)
156
if status == nil {
157
t.Fatal("GetStatus returned nil")
158
}
159
160
if !status.Reachable {
···
191
status := checker.GetCachedStatus(endpoint)
192
if status == nil {
193
t.Fatal("Status not found in cache")
194
}
195
196
if !status.Reachable {
···
131
status := checker.GetStatus(context.Background(), endpoint)
132
if status == nil {
133
t.Fatal("GetStatus returned nil")
134
+
return
135
}
136
137
if !status.Reachable {
···
156
status := checker.GetStatus(context.Background(), server.URL)
157
if status == nil {
158
t.Fatal("GetStatus returned nil")
159
+
return
160
}
161
162
if !status.Reachable {
···
193
status := checker.GetCachedStatus(endpoint)
194
if status == nil {
195
t.Fatal("Status not found in cache")
196
+
return
197
}
198
199
if !status.Reachable {
+3
pkg/appview/jetstream/worker.go
+3
pkg/appview/jetstream/worker.go
+4
pkg/appview/licenses/licenses.go
+4
pkg/appview/licenses/licenses.go
···
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.
5
package licenses
6
7
//go:generate curl -fsSL -o spdx-licenses.json https://spdx.org/licenses/licenses.json
+4
pkg/appview/middleware/auth.go
+4
pkg/appview/middleware/auth.go
···
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.
5
package middleware
6
7
import (
+4
pkg/appview/readme/cache.go
+4
pkg/appview/readme/cache.go
···
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.
5
package readme
6
7
import (
+1
-1
pkg/appview/storage/proxy_blob_store.go
+1
-1
pkg/appview/storage/proxy_blob_store.go
+1
-1
pkg/appview/storage/proxy_blob_store_test.go
+1
-1
pkg/appview/storage/proxy_blob_store_test.go
+4
pkg/appview/storage/routing_repository.go
+4
pkg/appview/storage/routing_repository.go
···
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.
5
package storage
6
7
import (
+3
-3
pkg/atproto/endpoints.go
+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"
17
18
-
// 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"
23
24
// 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"
17
18
+
// 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"
23
24
// HoldUploadPart handles direct buffered part uploads (alternative to presigned URLs).
25
// Method: PUT
+1
-1
pkg/atproto/profile.go
+1
-1
pkg/atproto/profile.go
+4
pkg/auth/oauth/client.go
+4
pkg/auth/oauth/client.go
···
1
+
// Package oauth provides OAuth client and flow implementation for ATCR.
2
+
// It wraps indigo's OAuth library with ATCR-specific configuration,
3
+
// including default scopes, client metadata, token refreshing, and
4
+
// interactive browser-based authentication flows.
5
package oauth
6
7
import (
+5
-1
pkg/auth/session.go
+5
-1
pkg/auth/session.go
···
1
+
// Package auth provides authentication and authorization for ATCR, including
2
+
// ATProto session validation, hold authorization (captain/crew membership),
3
+
// scope parsing, and token caching for OAuth and service tokens.
4
package auth
5
6
import (
7
"bytes"
8
"context"
9
"crypto/sha256"
···
14
"net/http"
15
"sync"
16
"time"
17
+
18
+
"atcr.io/pkg/atproto"
19
20
"github.com/bluesky-social/indigo/atproto/identity"
21
"github.com/bluesky-social/indigo/atproto/syntax"
+4
pkg/auth/token/cache.go
+4
pkg/auth/token/cache.go
···
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.
5
package token
6
7
import (
+5
pkg/hold/config.go
+5
pkg/hold/config.go
···
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.
6
package hold
7
8
import (
+3
pkg/hold/oci/http_helpers.go
+3
pkg/hold/oci/http_helpers.go
+1
-1
pkg/hold/oci/multipart.go
+1
-1
pkg/hold/oci/multipart.go
+3
-3
pkg/hold/oci/xrpc.go
+3
-3
pkg/hold/oci/xrpc.go
···
44
r.Use(h.requireBlobWriteAccess)
45
46
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
}
82
83
-
// 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)
45
46
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
}
82
83
+
// 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
+6
-6
pkg/hold/oci/xrpc_test.go
···
218
uploadID := initResp["uploadId"].(string)
219
220
// Now get part upload URL
221
-
req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, map[string]any{
222
"uploadId": uploadID,
223
"partNumber": 1,
224
})
225
addMockAuth(req)
226
227
w := httptest.NewRecorder()
228
-
handler.HandleGetPartUploadUrl(w, req)
229
230
if w.Code != http.StatusOK {
231
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
···
246
func TestHandleGetPartUploadUrl_InvalidSession(t *testing.T) {
247
handler, _ := setupTestOCIHandler(t)
248
249
-
req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, map[string]any{
250
"uploadId": "invalid-upload-id",
251
"partNumber": 1,
252
})
253
addMockAuth(req)
254
255
w := httptest.NewRecorder()
256
-
handler.HandleGetPartUploadUrl(w, req)
257
258
if w.Code != http.StatusInternalServerError {
259
t.Errorf("Expected status 500, got %d", w.Code)
···
274
275
for _, tt := range tests {
276
t.Run(tt.name, func(t *testing.T) {
277
-
req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, tt.body)
278
addMockAuth(req)
279
280
w := httptest.NewRecorder()
281
-
handler.HandleGetPartUploadUrl(w, req)
282
283
if w.Code != http.StatusBadRequest {
284
t.Errorf("Expected status 400, got %d", w.Code)
···
218
uploadID := initResp["uploadId"].(string)
219
220
// Now get part upload URL
221
+
req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, map[string]any{
222
"uploadId": uploadID,
223
"partNumber": 1,
224
})
225
addMockAuth(req)
226
227
w := httptest.NewRecorder()
228
+
handler.HandleGetPartUploadURL(w, req)
229
230
if w.Code != http.StatusOK {
231
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
···
246
func TestHandleGetPartUploadUrl_InvalidSession(t *testing.T) {
247
handler, _ := setupTestOCIHandler(t)
248
249
+
req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, map[string]any{
250
"uploadId": "invalid-upload-id",
251
"partNumber": 1,
252
})
253
addMockAuth(req)
254
255
w := httptest.NewRecorder()
256
+
handler.HandleGetPartUploadURL(w, req)
257
258
if w.Code != http.StatusInternalServerError {
259
t.Errorf("Expected status 500, got %d", w.Code)
···
274
275
for _, tt := range tests {
276
t.Run(tt.name, func(t *testing.T) {
277
+
req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, tt.body)
278
addMockAuth(req)
279
280
w := httptest.NewRecorder()
281
+
handler.HandleGetPartUploadURL(w, req)
282
283
if w.Code != http.StatusBadRequest {
284
t.Errorf("Expected status 400, got %d", w.Code)
+1
-1
pkg/hold/pds/repomgr.go
+1
-1
pkg/hold/pds/repomgr.go
+1
-1
pkg/hold/pds/xrpc.go
+1
-1
pkg/hold/pds/xrpc.go
···
1223
json.NewEncoder(w).Encode(response)
1224
}
1225
1226
-
// getPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
1227
// Distinguishes between ATProto blobs (per-DID) and OCI blobs (content-addressed)
1228
func (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
}
1225
1226
+
// GetPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
1227
// Distinguishes between ATProto blobs (per-DID) and OCI blobs (content-addressed)
1228
func (h *XRPCHandler) GetPresignedURL(ctx context.Context, operation string, digest string, did string) (string, error) {
1229
var path string
+8
-4
pkg/s3/types.go
+8
-4
pkg/s3/types.go
···
1
package s3
2
3
import (
4
"fmt"
5
"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
)
12
13
type S3Service struct {
···
16
PathPrefix string // S3 path prefix (if any)
17
}
18
19
-
// 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)
22
func NewS3Service(params map[string]any, disablePresigned bool, storageType string) (*S3Service, error) {
···
85
}, nil
86
}
87
88
-
// 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.
4
package s3
5
6
import (
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"
15
)
16
17
type S3Service struct {
···
20
PathPrefix string // S3 path prefix (if any)
21
}
22
23
+
// 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)
26
func NewS3Service(params map[string]any, disablePresigned bool, storageType string) (*S3Service, error) {
···
89
}, nil
90
}
91
92
+
// 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