begin embedded pds with xrpc endpoints and well-known

This commit is contained in:
Evan Jarrett
2025-10-14 20:25:08 -05:00
parent 2ee8bd8786
commit 18fe0684d3
17 changed files with 1252 additions and 29 deletions

View File

@@ -3,6 +3,7 @@ package hold
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/distribution/distribution/v3/configuration"
@@ -14,6 +15,7 @@ type Config struct {
Storage StorageConfig `yaml:"storage"`
Server ServerConfig `yaml:"server"`
Registration RegistrationConfig `yaml:"registration"`
Database DatabaseConfig `yaml:"database"`
}
// RegistrationConfig defines auto-registration settings
@@ -57,6 +59,17 @@ type ServerConfig struct {
WriteTimeout time.Duration `yaml:"write_timeout"`
}
// DatabaseConfig defines embedded PDS database settings
type DatabaseConfig struct {
// Path is the directory path for carstore (from env: HOLD_DATABASE_DIR)
// If empty, embedded PDS is disabled
Path string `yaml:"path"`
// KeyPath is the path to the signing key (from env: HOLD_KEY_PATH)
// Defaults to {Path}/signing.key
KeyPath string `yaml:"key_path"`
}
// LoadConfigFromEnv loads all configuration from environment variables
func LoadConfigFromEnv() (*Config, error) {
cfg := &Config{
@@ -79,6 +92,15 @@ func LoadConfigFromEnv() (*Config, error) {
cfg.Registration.OwnerDID = os.Getenv("HOLD_OWNER")
cfg.Registration.AllowAllCrew = os.Getenv("HOLD_ALLOW_ALL_CREW") == "true"
// Database configuration (optional - enables embedded PDS)
// Note: HOLD_DATABASE_DIR is a directory path, carstore creates db.sqlite3 inside it
cfg.Database.Path = getEnvOrDefault("HOLD_DATABASE_DIR", "/var/lib/atcr-hold")
cfg.Database.KeyPath = os.Getenv("HOLD_KEY_PATH")
if cfg.Database.KeyPath == "" && cfg.Database.Path != "" {
// Default: signing key in same directory as carstore
cfg.Database.KeyPath = filepath.Join(cfg.Database.Path, "signing.key")
}
// Storage configuration - build from env vars based on storage type
storageType := getEnvOrDefault("STORAGE_DRIVER", "s3")
var err error

View File

@@ -0,0 +1,44 @@
package pds
import (
"context"
"atcr.io/pkg/hold"
)
// HoldServiceBlobStore adapts the hold service to implement the BlobStore interface
type HoldServiceBlobStore struct {
service *hold.HoldService
holdDID string
}
// NewHoldServiceBlobStore creates a blob store adapter for the hold service
func NewHoldServiceBlobStore(service *hold.HoldService, holdDID string) *HoldServiceBlobStore {
return &HoldServiceBlobStore{
service: service,
holdDID: holdDID,
}
}
// GetPresignedDownloadURL returns a presigned URL for downloading a blob
func (b *HoldServiceBlobStore) GetPresignedDownloadURL(digest string) (string, error) {
// Use the hold service's existing presigned URL logic
// We need to expose a wrapper method on HoldService
ctx := context.Background()
url, err := b.service.GetPresignedURL(ctx, hold.OperationGet, digest, b.holdDID)
if err != nil {
return "", err
}
return url, nil
}
// GetPresignedUploadURL returns a presigned URL for uploading a blob
func (b *HoldServiceBlobStore) GetPresignedUploadURL(digest string) (string, error) {
// Use the hold service's existing presigned URL logic
ctx := context.Background()
url, err := b.service.GetPresignedURL(ctx, hold.OperationPut, digest, b.holdDID)
if err != nil {
return "", err
}
return url, nil
}

113
pkg/hold/pds/crew.go Normal file
View File

@@ -0,0 +1,113 @@
package pds
import (
"context"
"fmt"
"io"
"time"
"github.com/ipfs/go-cid"
)
// CrewRecord represents a crew member in the hold
type CrewRecord struct {
Member string `json:"member" cborgen:"member"` // DID of the crew member
Role string `json:"role" cborgen:"role"` // "admin" or "member"
Permissions []string `json:"permissions" cborgen:"permissions"` // e.g., ["blob:read", "blob:write"]
AddedAt time.Time `json:"addedAt" cborgen:"addedAt"`
}
// MarshalCBOR implements cbg.CBORMarshaler
func (c *CrewRecord) MarshalCBOR(w io.Writer) error {
// TODO: Implement proper CBOR marshaling
return fmt.Errorf("CBOR marshaling not yet implemented")
}
// UnmarshalCBOR implements cbg.CBORUnmarshaler
func (c *CrewRecord) UnmarshalCBOR(r io.Reader) error {
// TODO: Implement proper CBOR unmarshaling
return fmt.Errorf("CBOR unmarshaling not yet implemented")
}
const (
CrewCollection = "io.atcr.hold.crew"
)
// AddCrewMember adds a new crew member to the hold
func (p *HoldPDS) AddCrewMember(ctx context.Context, memberDID, role string, permissions []string) (cid.Cid, error) {
crewRecord := &CrewRecord{
Member: memberDID,
Role: role,
Permissions: permissions,
AddedAt: time.Now(),
}
// Create record in repo
recordCID, rkey, err := p.repo.CreateRecord(ctx, CrewCollection, crewRecord)
if err != nil {
return cid.Undef, fmt.Errorf("failed to create crew record: %w", err)
}
// TODO: Commit the changes
// For now, just return the CID
_ = rkey // We'll use rkey for GetCrewMember later
return recordCID, nil
}
// GetCrewMember retrieves a crew member by their record key
func (p *HoldPDS) GetCrewMember(ctx context.Context, rkey string) (*CrewRecord, error) {
path := fmt.Sprintf("%s/%s", CrewCollection, rkey)
_, rec, err := p.repo.GetRecord(ctx, path)
if err != nil {
return nil, fmt.Errorf("failed to get crew record: %w", err)
}
crewRecord, ok := rec.(*CrewRecord)
if !ok {
return nil, fmt.Errorf("record is not a CrewRecord")
}
return crewRecord, nil
}
// ListCrewMembers returns all crew members
func (p *HoldPDS) ListCrewMembers(ctx context.Context) ([]*CrewRecord, error) {
var crew []*CrewRecord
err := p.repo.ForEach(ctx, CrewCollection, func(k string, v cid.Cid) error {
// Get the full record
path := fmt.Sprintf("%s/%s", CrewCollection, k)
_, rec, err := p.repo.GetRecord(ctx, path)
if err != nil {
return err
}
if crewRecord, ok := rec.(*CrewRecord); ok {
crew = append(crew, crewRecord)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to list crew members: %w", err)
}
return crew, nil
}
// RemoveCrewMember removes a crew member
func (p *HoldPDS) RemoveCrewMember(ctx context.Context, rkey string) error {
path := fmt.Sprintf("%s/%s", CrewCollection, rkey)
err := p.repo.DeleteRecord(ctx, path)
if err != nil {
return fmt.Errorf("failed to delete crew record: %w", err)
}
// TODO: Commit the changes
return nil
}

150
pkg/hold/pds/did.go Normal file
View File

@@ -0,0 +1,150 @@
package pds
import (
"crypto/ecdsa"
"crypto/elliptic"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"strings"
)
// DIDDocument represents a did:web document
type DIDDocument struct {
Context []string `json:"@context"`
ID string `json:"id"`
AlsoKnownAs []string `json:"alsoKnownAs,omitempty"`
VerificationMethod []VerificationMethod `json:"verificationMethod"`
Authentication []string `json:"authentication,omitempty"`
AssertionMethod []string `json:"assertionMethod,omitempty"`
Service []Service `json:"service,omitempty"`
}
// VerificationMethod represents a public key in a DID document
type VerificationMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Controller string `json:"controller"`
PublicKeyMultibase string `json:"publicKeyMultibase"`
}
// Service represents a service endpoint in a DID document
type Service struct {
ID string `json:"id"`
Type string `json:"type"`
ServiceEndpoint string `json:"serviceEndpoint"`
}
// GenerateDIDDocument creates a DID document for a did:web identity
func (p *HoldPDS) GenerateDIDDocument(publicURL string) (*DIDDocument, error) {
// Extract hostname from public URL
hostname := strings.TrimPrefix(publicURL, "http://")
hostname = strings.TrimPrefix(hostname, "https://")
hostname = strings.Split(hostname, "/")[0] // Remove any path
hostname = strings.Split(hostname, ":")[0] // Remove port for DID
did := fmt.Sprintf("did:web:%s", hostname)
// Convert public key to multibase format
publicKeyMultibase, err := encodePublicKeyMultibase(p.signingKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to encode public key: %w", err)
}
doc := &DIDDocument{
Context: []string{
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
},
ID: did,
VerificationMethod: []VerificationMethod{
{
ID: fmt.Sprintf("%s#atproto", did),
Type: "Multikey",
Controller: did,
PublicKeyMultibase: publicKeyMultibase,
},
},
Authentication: []string{
fmt.Sprintf("%s#atproto", did),
},
Service: []Service{
{
ID: "#atproto_pds",
Type: "AtprotoPersonalDataServer",
ServiceEndpoint: publicURL,
},
},
}
return doc, nil
}
// encodePublicKeyMultibase encodes an ECDSA public key in multibase format
// For P-256 keys, we use the compressed format with multicodec prefix
func encodePublicKeyMultibase(pubKey ecdsa.PublicKey) (string, error) {
// Check if this is a P-256 key
if pubKey.Curve != elliptic.P256() {
return "", fmt.Errorf("unsupported curve: only P-256 is supported")
}
// Use compressed point format
// 0x02 if Y is even, 0x03 if Y is odd
var prefix byte
if pubKey.Y.Bit(0) == 0 {
prefix = 0x02
} else {
prefix = 0x03
}
// Compressed format: prefix (1 byte) + X coordinate (32 bytes for P-256)
xBytes := pubKey.X.Bytes()
// Pad X to 32 bytes if needed
paddedX := make([]byte, 32)
copy(paddedX[32-len(xBytes):], xBytes)
compressed := append([]byte{prefix}, paddedX...)
// Multicodec prefix for P-256 public key: 0x1200
// See https://github.com/multiformats/multicodec/blob/master/table.csv
multicodec := []byte{0x80, 0x24} // P-256 public key multicodec
// Combine multicodec + compressed key
multicodecKey := append(multicodec, compressed...)
// Encode in multibase (base58btc, prefix 'z')
encoded := base64.RawURLEncoding.EncodeToString(multicodecKey)
return "z" + encoded, nil
}
// MarshalDIDDocument converts a DID document to JSON using the stored public URL
func (p *HoldPDS) MarshalDIDDocument() ([]byte, error) {
doc, err := p.GenerateDIDDocument(p.publicURL)
if err != nil {
return nil, err
}
return json.MarshalIndent(doc, "", " ")
}
// GenerateDIDFromURL creates a did:web identifier from a public URL
// Example: "http://hold1.example.com:8080" -> "did:web:hold1.example.com"
func GenerateDIDFromURL(publicURL string) string {
// Parse URL
u, err := url.Parse(publicURL)
if err != nil {
// Fallback: assume it's just a hostname
return fmt.Sprintf("did:web:%s", strings.Split(publicURL, ":")[0])
}
// Use hostname without port for DID
hostname := u.Hostname()
if hostname == "" {
hostname = "localhost"
}
return fmt.Sprintf("did:web:%s", hostname)
}

102
pkg/hold/pds/keys.go Normal file
View File

@@ -0,0 +1,102 @@
package pds
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"os"
"path/filepath"
)
// GenerateOrLoadKey generates a new K256 key pair or loads an existing one
func GenerateOrLoadKey(keyPath string) (*ecdsa.PrivateKey, error) {
// Ensure directory exists
dir := filepath.Dir(keyPath)
if err := os.MkdirAll(dir, 0700); err != nil {
return nil, fmt.Errorf("failed to create key directory: %w", err)
}
// Check if key already exists
if _, err := os.Stat(keyPath); err == nil {
// Key exists, load it
return loadKey(keyPath)
}
// Key doesn't exist, generate new one
return generateKey(keyPath)
}
// generateKey creates a new K256 (secp256k1) key pair
func generateKey(keyPath string) (*ecdsa.PrivateKey, error) {
// Generate K256 key (secp256k1)
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate key: %w", err)
}
// Marshal private key to DER format
derBytes, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal private key: %w", err)
}
// Create PEM block
pemBlock := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: derBytes,
}
// Write to file with restrictive permissions
file, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return nil, fmt.Errorf("failed to create key file: %w", err)
}
defer file.Close()
if err := pem.Encode(file, pemBlock); err != nil {
return nil, fmt.Errorf("failed to write PEM data: %w", err)
}
fmt.Printf("Generated new signing key at %s\n", keyPath)
return privateKey, nil
}
// loadKey loads an existing private key from disk
func loadKey(keyPath string) (*ecdsa.PrivateKey, error) {
// Read PEM file
pemData, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read key file: %w", err)
}
// Decode PEM block
block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
// Parse EC private key
privateKey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
fmt.Printf("Loaded existing signing key from %s\n", keyPath)
return privateKey, nil
}
// PublicKeyToBase58 converts an ECDSA public key to base58 format for DID documents
func PublicKeyToBase58(pubKey *ecdsa.PublicKey) (string, error) {
// Marshal public key to X.509 SPKI format
derBytes, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return "", fmt.Errorf("failed to marshal public key: %w", err)
}
// TODO: Convert to base58 (need to import base58 library)
// For now, just return hex encoding as placeholder
return fmt.Sprintf("%x", derBytes), nil
}

105
pkg/hold/pds/server.go Normal file
View File

@@ -0,0 +1,105 @@
package pds
import (
"context"
"crypto/ecdsa"
"fmt"
"os"
"path/filepath"
"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 *ecdsa.PrivateKey
}
// 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() *ecdsa.PrivateKey {
return p.signingKey
}
// Close closes the session and carstore
func (p *HoldPDS) Close() error {
// TODO: Close session properly
return nil
}

273
pkg/hold/pds/xrpc.go Normal file
View File

@@ -0,0 +1,273 @@
package pds
import (
"encoding/json"
"fmt"
"net/http"
)
// XRPC handler for ATProto endpoints
// XRPCHandler handles XRPC requests for the embedded PDS
type XRPCHandler struct {
pds *HoldPDS
publicURL string
blobStore BlobStore
}
// BlobStore interface wraps the existing hold service storage operations
type BlobStore interface {
// GetPresignedDownloadURL returns a presigned URL for downloading a blob
GetPresignedDownloadURL(digest string) (string, error)
// GetPresignedUploadURL returns a presigned URL for uploading a blob
GetPresignedUploadURL(digest string) (string, error)
}
// NewXRPCHandler creates a new XRPC handler
func NewXRPCHandler(pds *HoldPDS, publicURL string, blobStore BlobStore) *XRPCHandler {
return &XRPCHandler{
pds: pds,
publicURL: publicURL,
blobStore: blobStore,
}
}
// RegisterHandlers registers all XRPC endpoints
func (h *XRPCHandler) RegisterHandlers(mux *http.ServeMux) {
// Standard PDS endpoints
mux.HandleFunc("/xrpc/com.atproto.server.describeServer", h.HandleDescribeServer)
mux.HandleFunc("/xrpc/com.atproto.repo.describeRepo", h.HandleDescribeRepo)
mux.HandleFunc("/xrpc/com.atproto.repo.getRecord", h.HandleGetRecord)
mux.HandleFunc("/xrpc/com.atproto.repo.listRecords", h.HandleListRecords)
// Blob endpoints (wrap existing presigned URL logic)
mux.HandleFunc("/xrpc/com.atproto.repo.uploadBlob", h.HandleUploadBlob)
mux.HandleFunc("/xrpc/com.atproto.sync.getBlob", h.HandleGetBlob)
// DID document
mux.HandleFunc("/.well-known/did.json", h.HandleDIDDocument)
}
// HandleDescribeServer returns server metadata
func (h *XRPCHandler) HandleDescribeServer(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
response := map[string]interface{}{
"did": h.pds.DID(),
"availableUserDomains": []string{},
"inviteCodeRequired": false,
"links": map[string]string{},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleDescribeRepo returns repository information
func (h *XRPCHandler) HandleDescribeRepo(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// Get repo parameter
repo := r.URL.Query().Get("repo")
if repo == "" || repo != h.pds.DID() {
http.Error(w, "invalid repo", http.StatusBadRequest)
return
}
// Generate DID document
didDoc, err := h.pds.GenerateDIDDocument(h.publicURL)
if err != nil {
http.Error(w, fmt.Sprintf("failed to generate DID document: %v", err), http.StatusInternalServerError)
return
}
// Extract handle from did:web (remove "did:web:" prefix)
handle := h.pds.DID()
if len(handle) > 8 && handle[:8] == "did:web:" {
handle = handle[8:] // "did:web:example.com" -> "example.com"
}
// TODO: Get actual repo head from carstore
response := map[string]interface{}{
"did": h.pds.DID(),
"handle": handle,
"didDoc": didDoc,
"collections": []string{CrewCollection},
"handleIsCorrect": true,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleGetRecord retrieves a record from the repository
func (h *XRPCHandler) HandleGetRecord(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
repo := r.URL.Query().Get("repo")
collection := r.URL.Query().Get("collection")
rkey := r.URL.Query().Get("rkey")
if repo == "" || collection == "" || rkey == "" {
http.Error(w, "missing required parameters", http.StatusBadRequest)
return
}
if repo != h.pds.DID() {
http.Error(w, "invalid repo", http.StatusBadRequest)
return
}
// Only support crew collection for now
if collection != CrewCollection {
http.Error(w, "collection not found", http.StatusNotFound)
return
}
crewRecord, err := h.pds.GetCrewMember(r.Context(), rkey)
if err != nil {
http.Error(w, fmt.Sprintf("failed to get record: %v", err), http.StatusNotFound)
return
}
response := map[string]interface{}{
"uri": fmt.Sprintf("at://%s/%s/%s", h.pds.DID(), collection, rkey),
"value": crewRecord,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleListRecords lists records in a collection
func (h *XRPCHandler) HandleListRecords(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
repo := r.URL.Query().Get("repo")
collection := r.URL.Query().Get("collection")
if repo == "" || collection == "" {
http.Error(w, "missing required parameters", http.StatusBadRequest)
return
}
if repo != h.pds.DID() {
http.Error(w, "invalid repo", http.StatusBadRequest)
return
}
// Only support crew collection for now
if collection != CrewCollection {
http.Error(w, "collection not found", http.StatusNotFound)
return
}
crew, err := h.pds.ListCrewMembers(r.Context())
if err != nil {
http.Error(w, fmt.Sprintf("failed to list records: %v", err), http.StatusInternalServerError)
return
}
records := make([]map[string]interface{}, len(crew))
for i, member := range crew {
// TODO: Get actual rkey from somewhere
records[i] = map[string]interface{}{
"uri": fmt.Sprintf("at://%s/%s/%s", h.pds.DID(), collection, member.Member),
"value": member,
}
}
response := map[string]interface{}{
"records": records,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// HandleUploadBlob wraps existing presigned upload URL logic
func (h *XRPCHandler) HandleUploadBlob(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// TODO: Authentication check
// Read digest from query or calculate from body
digest := r.URL.Query().Get("digest")
if digest == "" {
http.Error(w, "digest required", http.StatusBadRequest)
return
}
// Get presigned upload URL from existing blob store
uploadURL, err := h.blobStore.GetPresignedUploadURL(digest)
if err != nil {
http.Error(w, fmt.Sprintf("failed to get upload URL: %v", err), http.StatusInternalServerError)
return
}
// Return 302 redirect to presigned URL
http.Redirect(w, r, uploadURL, http.StatusFound)
}
// HandleGetBlob wraps existing presigned download URL logic
func (h *XRPCHandler) HandleGetBlob(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
did := r.URL.Query().Get("did")
digest := r.URL.Query().Get("cid")
if did == "" || digest == "" {
http.Error(w, "missing required parameters", http.StatusBadRequest)
return
}
if did != h.pds.DID() {
http.Error(w, "invalid did", http.StatusBadRequest)
return
}
// Get presigned download URL from existing blob store
downloadURL, err := h.blobStore.GetPresignedDownloadURL(digest)
if err != nil {
http.Error(w, fmt.Sprintf("failed to get download URL: %v", err), http.StatusInternalServerError)
return
}
// Return 302 redirect to presigned URL
http.Redirect(w, r, downloadURL, http.StatusFound)
}
// HandleDIDDocument returns the DID document
func (h *XRPCHandler) HandleDIDDocument(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
doc, err := h.pds.GenerateDIDDocument(h.publicURL)
if err != nil {
http.Error(w, fmt.Sprintf("failed to generate DID document: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/did+json")
json.NewEncoder(w).Encode(doc)
}

View File

@@ -42,3 +42,8 @@ func NewHoldService(cfg *Config) (*HoldService, error) {
return service, nil
}
// GetPresignedURL is a public wrapper around getPresignedURL for use by PDS blob store
func (s *HoldService) GetPresignedURL(ctx context.Context, operation PresignedURLOperation, digest string, did string) (string, error) {
return s.getPresignedURL(ctx, operation, digest, did)
}