Files
at-container-registry/pkg/hold/pds/server.go
T
2025-10-14 22:11:00 -05:00

136 lines
3.6 KiB
Go

package pds
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/bluesky-social/indigo/atproto/crypto"
"github.com/bluesky-social/indigo/carstore"
"github.com/bluesky-social/indigo/models"
"github.com/bluesky-social/indigo/repo"
)
// HoldPDS is a minimal ATProto PDS implementation for a hold service
type HoldPDS struct {
did string
publicURL string
carstore carstore.CarStore
session *carstore.DeltaSession
repo *repo.Repo
dbPath string
uid models.Uid
signingKey *crypto.PrivateKeyK256
}
// NewHoldPDS creates or opens a hold PDS with SQLite carstore
func NewHoldPDS(ctx context.Context, did, publicURL, dbPath, keyPath string) (*HoldPDS, error) {
// Ensure directory exists
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("failed to create database directory: %w", err)
}
// Generate or load signing key
signingKey, err := GenerateOrLoadKey(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to initialize signing key: %w", err)
}
// Create and open SQLite-backed carstore
// dbPath is the directory, carstore creates and opens db.sqlite3 inside it
sqlStore, err := carstore.NewSqliteStore(dbPath)
if err != nil {
return nil, fmt.Errorf("failed to create sqlite store: %w", err)
}
cs := sqlStore.CarStore()
// For a single-user hold, we use a fixed UID (1)
uid := models.Uid(1)
// Try to get existing repo head
_, err = cs.GetUserRepoHead(ctx, uid)
var session *carstore.DeltaSession
var r *repo.Repo
if err != nil {
// Repo doesn't exist yet, create new delta session
session, err = cs.NewDeltaSession(ctx, uid, nil)
if err != nil {
return nil, fmt.Errorf("failed to create delta session: %w", err)
}
// Create new repo with session as blockstore (needs pointer)
r = repo.NewRepo(ctx, did, session)
} else {
// TODO: Load existing repo
// For now, just create a new session
session, err = cs.NewDeltaSession(ctx, uid, nil)
if err != nil {
return nil, fmt.Errorf("failed to create delta session: %w", err)
}
r = repo.NewRepo(ctx, did, session)
}
return &HoldPDS{
did: did,
publicURL: publicURL,
carstore: cs,
session: session,
repo: r,
dbPath: dbPath,
uid: uid,
signingKey: signingKey,
}, nil
}
// DID returns the hold's DID
func (p *HoldPDS) DID() string {
return p.did
}
// SigningKey returns the hold's signing key
func (p *HoldPDS) SigningKey() *crypto.PrivateKeyK256 {
return p.signingKey
}
// Bootstrap initializes the hold with the owner as the first crew member
func (p *HoldPDS) Bootstrap(ctx context.Context, ownerDID string) error {
if ownerDID == "" {
return nil
}
// Check if repo already has commits
head, err := p.carstore.GetUserRepoHead(ctx, p.uid)
if err == nil {
// Repo exists - check if we need to re-bootstrap due to key change
// If the repo exists but is empty/invalid, we should re-bootstrap
if head.String() == "" || head.String() == "b" {
fmt.Printf("⚠️ Detected invalid repo state, re-bootstrapping...\n")
} else {
fmt.Printf("⏭️ Skipping PDS bootstrap: repo already initialized (head: %s)\n", head.String()[:16])
return nil
}
}
// Add hold owner as first crew member with admin role
fmt.Printf("🚀 Bootstrapping hold PDS with owner: %s\n", ownerDID)
_, err = p.AddCrewMember(ctx, ownerDID, "admin", []string{"blob:read", "blob:write", "crew:admin"})
if err != nil {
return fmt.Errorf("failed to add owner as crew member: %w", err)
}
fmt.Printf("✅ Added %s as hold admin\n", ownerDID)
return nil
}
// Close closes the session and carstore
func (p *HoldPDS) Close() error {
// TODO: Close session properly
return nil
}