+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.
1
6
package appview
2
7
3
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
131
status := checker.GetStatus(context.Background(), endpoint)
132
132
if status == nil {
133
133
t.Fatal("GetStatus returned nil")
134
+
return
134
135
}
135
136
136
137
if !status.Reachable {
···
155
156
status := checker.GetStatus(context.Background(), server.URL)
156
157
if status == nil {
157
158
t.Fatal("GetStatus returned nil")
159
+
return
158
160
}
159
161
160
162
if !status.Reachable {
···
191
193
status := checker.GetCachedStatus(endpoint)
192
194
if status == nil {
193
195
t.Fatal("Status not found in cache")
196
+
return
194
197
}
195
198
196
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.
1
5
package licenses
2
6
3
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.
1
5
package middleware
2
6
3
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.
1
5
package readme
2
6
3
7
import (
+1
-1
pkg/appview/storage/proxy_blob_store.go
+1
-1
pkg/appview/storage/proxy_blob_store.go
···
451
451
return nil, err
452
452
}
453
453
454
-
url := fmt.Sprintf("%s%s", p.holdURL, atproto.HoldGetPartUploadUrl)
454
+
url := fmt.Sprintf("%s%s", p.holdURL, atproto.HoldGetPartUploadURL)
455
455
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(body))
456
456
if err != nil {
457
457
return nil, err
+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.
1
5
package storage
2
6
3
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.
1
+
// Package atproto provides constants for XRPC endpoint paths used throughout ATCR.
2
2
//
3
3
// This package serves as a single source of truth for all XRPC endpoint URLs,
4
4
// preventing typos and making refactoring easier. All endpoint paths follow the
···
15
15
// Response: {"uploadId": "..."}
16
16
HoldInitiateUpload = "/xrpc/io.atcr.hold.initiateUpload"
17
17
18
-
// HoldGetPartUploadUrl gets a presigned URL or endpoint info for uploading a specific part.
18
+
// HoldGetPartUploadURL gets a presigned URL or endpoint info for uploading a specific part.
19
19
// Method: POST
20
20
// Request: {"uploadId": "...", "partNumber": 1}
21
21
// Response: {"url": "...", "method": "PUT", "headers": {...}}
22
-
HoldGetPartUploadUrl = "/xrpc/io.atcr.hold.getPartUploadUrl"
22
+
HoldGetPartUploadURL = "/xrpc/io.atcr.hold.getPartUploadUrl"
23
23
24
24
// HoldUploadPart handles direct buffered part uploads (alternative to presigned URLs).
25
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.
1
5
package oauth
2
6
3
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.
1
4
package auth
2
5
3
6
import (
4
-
"atcr.io/pkg/atproto"
5
7
"bytes"
6
8
"context"
7
9
"crypto/sha256"
···
12
14
"net/http"
13
15
"sync"
14
16
"time"
17
+
18
+
"atcr.io/pkg/atproto"
15
19
16
20
"github.com/bluesky-social/indigo/atproto/identity"
17
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.
1
5
package token
2
6
3
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.
1
6
package hold
2
7
3
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
44
r.Use(h.requireBlobWriteAccess)
45
45
46
46
r.Post(atproto.HoldInitiateUpload, h.HandleInitiateUpload)
47
-
r.Post(atproto.HoldGetPartUploadUrl, h.HandleGetPartUploadUrl)
47
+
r.Post(atproto.HoldGetPartUploadURL, h.HandleGetPartUploadURL)
48
48
r.Put(atproto.HoldUploadPart, h.HandleUploadPart)
49
49
r.Post(atproto.HoldCompleteUpload, h.HandleCompleteUpload)
50
50
r.Post(atproto.HoldAbortUpload, h.HandleAbortUpload)
···
80
80
})
81
81
}
82
82
83
-
// HandleGetPartUploadUrl returns a presigned URL or endpoint info for uploading a part
83
+
// HandleGetPartUploadURL returns a presigned URL or endpoint info for uploading a part
84
84
// Replaces the old "action: part" pattern
85
-
func (h *XRPCHandler) HandleGetPartUploadUrl(w http.ResponseWriter, r *http.Request) {
85
+
func (h *XRPCHandler) HandleGetPartUploadURL(w http.ResponseWriter, r *http.Request) {
86
86
var req struct {
87
87
UploadID string `json:"uploadId"`
88
88
PartNumber int `json:"partNumber"`
+6
-6
pkg/hold/oci/xrpc_test.go
+6
-6
pkg/hold/oci/xrpc_test.go
···
218
218
uploadID := initResp["uploadId"].(string)
219
219
220
220
// Now get part upload URL
221
-
req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, map[string]any{
221
+
req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, map[string]any{
222
222
"uploadId": uploadID,
223
223
"partNumber": 1,
224
224
})
225
225
addMockAuth(req)
226
226
227
227
w := httptest.NewRecorder()
228
-
handler.HandleGetPartUploadUrl(w, req)
228
+
handler.HandleGetPartUploadURL(w, req)
229
229
230
230
if w.Code != http.StatusOK {
231
231
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
···
246
246
func TestHandleGetPartUploadUrl_InvalidSession(t *testing.T) {
247
247
handler, _ := setupTestOCIHandler(t)
248
248
249
-
req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, map[string]any{
249
+
req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, map[string]any{
250
250
"uploadId": "invalid-upload-id",
251
251
"partNumber": 1,
252
252
})
253
253
addMockAuth(req)
254
254
255
255
w := httptest.NewRecorder()
256
-
handler.HandleGetPartUploadUrl(w, req)
256
+
handler.HandleGetPartUploadURL(w, req)
257
257
258
258
if w.Code != http.StatusInternalServerError {
259
259
t.Errorf("Expected status 500, got %d", w.Code)
···
274
274
275
275
for _, tt := range tests {
276
276
t.Run(tt.name, func(t *testing.T) {
277
-
req := makeJSONRequest("POST", atproto.HoldGetPartUploadUrl, tt.body)
277
+
req := makeJSONRequest("POST", atproto.HoldGetPartUploadURL, tt.body)
278
278
addMockAuth(req)
279
279
280
280
w := httptest.NewRecorder()
281
-
handler.HandleGetPartUploadUrl(w, req)
281
+
handler.HandleGetPartUploadURL(w, req)
282
282
283
283
if w.Code != http.StatusBadRequest {
284
284
t.Errorf("Expected status 400, got %d", w.Code)
+1
-1
pkg/hold/pds/repomgr.go
+1
-1
pkg/hold/pds/repomgr.go
···
1250
1250
return rm.cs.WipeUserData(ctx, uid)
1251
1251
}
1252
1252
1253
-
// technically identical to TakeDownRepo, for now
1253
+
// ResetRepo is technically identical to TakeDownRepo, for now
1254
1254
func (rm *RepoManager) ResetRepo(ctx context.Context, uid models.Uid) error {
1255
1255
unlock := rm.lockUser(ctx, uid)
1256
1256
defer unlock()
+1
-1
pkg/hold/pds/xrpc.go
+1
-1
pkg/hold/pds/xrpc.go
···
1223
1223
json.NewEncoder(w).Encode(response)
1224
1224
}
1225
1225
1226
-
// getPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
1226
+
// GetPresignedURL generates a presigned URL for GET, HEAD, or PUT operations
1227
1227
// Distinguishes between ATProto blobs (per-DID) and OCI blobs (content-addressed)
1228
1228
func (h *XRPCHandler) GetPresignedURL(ctx context.Context, operation string, digest string, did string) (string, error) {
1229
1229
var path string
+8
-4
pkg/s3/types.go
+8
-4
pkg/s3/types.go
···
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.
1
4
package s3
2
5
3
6
import (
4
7
"fmt"
8
+
"log"
9
+
"strings"
10
+
5
11
"github.com/aws/aws-sdk-go/aws"
6
12
"github.com/aws/aws-sdk-go/aws/credentials"
7
13
"github.com/aws/aws-sdk-go/aws/session"
8
14
"github.com/aws/aws-sdk-go/service/s3"
9
-
"log"
10
-
"strings"
11
15
)
12
16
13
17
type S3Service struct {
···
16
20
PathPrefix string // S3 path prefix (if any)
17
21
}
18
22
19
-
// initializes the S3 client for presigned URL generation
23
+
// NewS3Service initializes the S3 client for presigned URL generation
20
24
// Returns nil error if S3 client is successfully initialized
21
25
// Returns error if storage is not S3 or if initialization fails (service will fall back to proxy mode)
22
26
func NewS3Service(params map[string]any, disablePresigned bool, storageType string) (*S3Service, error) {
···
85
89
}, nil
86
90
}
87
91
88
-
// blobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path
92
+
// BlobPath converts a digest (e.g., "sha256:abc123...") or temp path to a storage path
89
93
// Distribution stores blobs as: /docker/registry/v2/blobs/{algorithm}/{xx}/{hash}/data
90
94
// where xx is the first 2 characters of the hash for directory sharding
91
95
// NOTE: Path must start with / for filesystem driver