add middleware logging to xprc requests in hold. add tangled profile creation

This commit is contained in:
Evan Jarrett
2025-10-22 17:06:10 -05:00
parent 26c1b4e28e
commit 0e4dd9af20
6 changed files with 516 additions and 7 deletions
+4
View File
@@ -17,6 +17,7 @@ import (
_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
@@ -92,6 +93,9 @@ func main() {
// Setup HTTP routes with chi router
r := chi.NewRouter()
// Add logging middleware to log all HTTP requests
r.Use(middleware.Logger)
// Root page
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
+421
View File
@@ -612,3 +612,424 @@ func (t *CaptainRecord) UnmarshalCBOR(r io.Reader) (err error) {
return nil
}
func (t *TangledProfileRecord) MarshalCBOR(w io.Writer) error {
if t == nil {
_, err := w.Write(cbg.CborNull)
return err
}
cw := cbg.NewCborWriter(w)
if _, err := cw.Write([]byte{167}); err != nil {
return err
}
// t.Type (string) (string)
if len("$type") > 8192 {
return xerrors.Errorf("Value in field \"$type\" was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
return err
}
if _, err := cw.WriteString(string("$type")); err != nil {
return err
}
if len(t.Type) > 8192 {
return xerrors.Errorf("Value in field t.Type was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Type))); err != nil {
return err
}
if _, err := cw.WriteString(string(t.Type)); err != nil {
return err
}
// t.Links ([]string) (slice)
if len("links") > 8192 {
return xerrors.Errorf("Value in field \"links\" was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("links"))); err != nil {
return err
}
if _, err := cw.WriteString(string("links")); err != nil {
return err
}
if len(t.Links) > 8192 {
return xerrors.Errorf("Slice value in field t.Links was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Links))); err != nil {
return err
}
for _, v := range t.Links {
if len(v) > 8192 {
return xerrors.Errorf("Value in field v was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
return err
}
if _, err := cw.WriteString(string(v)); err != nil {
return err
}
}
// t.Stats ([]string) (slice)
if len("stats") > 8192 {
return xerrors.Errorf("Value in field \"stats\" was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("stats"))); err != nil {
return err
}
if _, err := cw.WriteString(string("stats")); err != nil {
return err
}
if len(t.Stats) > 8192 {
return xerrors.Errorf("Slice value in field t.Stats was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Stats))); err != nil {
return err
}
for _, v := range t.Stats {
if len(v) > 8192 {
return xerrors.Errorf("Value in field v was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
return err
}
if _, err := cw.WriteString(string(v)); err != nil {
return err
}
}
// t.Bluesky (bool) (bool)
if len("bluesky") > 8192 {
return xerrors.Errorf("Value in field \"bluesky\" was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("bluesky"))); err != nil {
return err
}
if _, err := cw.WriteString(string("bluesky")); err != nil {
return err
}
if err := cbg.WriteBool(w, t.Bluesky); err != nil {
return err
}
// t.Location (string) (string)
if len("location") > 8192 {
return xerrors.Errorf("Value in field \"location\" was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("location"))); err != nil {
return err
}
if _, err := cw.WriteString(string("location")); err != nil {
return err
}
if len(t.Location) > 8192 {
return xerrors.Errorf("Value in field t.Location was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Location))); err != nil {
return err
}
if _, err := cw.WriteString(string(t.Location)); err != nil {
return err
}
// t.Description (string) (string)
if len("description") > 8192 {
return xerrors.Errorf("Value in field \"description\" was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil {
return err
}
if _, err := cw.WriteString(string("description")); err != nil {
return err
}
if len(t.Description) > 8192 {
return xerrors.Errorf("Value in field t.Description was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Description))); err != nil {
return err
}
if _, err := cw.WriteString(string(t.Description)); err != nil {
return err
}
// t.PinnedRepositories ([]string) (slice)
if len("pinnedRepositories") > 8192 {
return xerrors.Errorf("Value in field \"pinnedRepositories\" was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedRepositories"))); err != nil {
return err
}
if _, err := cw.WriteString(string("pinnedRepositories")); err != nil {
return err
}
if len(t.PinnedRepositories) > 8192 {
return xerrors.Errorf("Slice value in field t.PinnedRepositories was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PinnedRepositories))); err != nil {
return err
}
for _, v := range t.PinnedRepositories {
if len(v) > 8192 {
return xerrors.Errorf("Value in field v was too long")
}
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
return err
}
if _, err := cw.WriteString(string(v)); err != nil {
return err
}
}
return nil
}
func (t *TangledProfileRecord) UnmarshalCBOR(r io.Reader) (err error) {
*t = TangledProfileRecord{}
cr := cbg.NewCborReader(r)
maj, extra, err := cr.ReadHeader()
if err != nil {
return err
}
defer func() {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
}()
if maj != cbg.MajMap {
return fmt.Errorf("cbor input should be of type map")
}
if extra > cbg.MaxLength {
return fmt.Errorf("TangledProfileRecord: map struct too large (%d)", extra)
}
n := extra
nameBuf := make([]byte, 18)
for i := uint64(0); i < n; i++ {
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192)
if err != nil {
return err
}
if !ok {
// Field doesn't exist on this type, so ignore it
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
return err
}
continue
}
switch string(nameBuf[:nameLen]) {
// t.Type (string) (string)
case "$type":
{
sval, err := cbg.ReadStringWithMax(cr, 8192)
if err != nil {
return err
}
t.Type = string(sval)
}
// t.Links ([]string) (slice)
case "links":
maj, extra, err = cr.ReadHeader()
if err != nil {
return err
}
if extra > 8192 {
return fmt.Errorf("t.Links: array too large (%d)", extra)
}
if maj != cbg.MajArray {
return fmt.Errorf("expected cbor array")
}
if extra > 0 {
t.Links = make([]string, extra)
}
for i := 0; i < int(extra); i++ {
{
var maj byte
var extra uint64
var err error
_ = maj
_ = extra
_ = err
{
sval, err := cbg.ReadStringWithMax(cr, 8192)
if err != nil {
return err
}
t.Links[i] = string(sval)
}
}
}
// t.Stats ([]string) (slice)
case "stats":
maj, extra, err = cr.ReadHeader()
if err != nil {
return err
}
if extra > 8192 {
return fmt.Errorf("t.Stats: array too large (%d)", extra)
}
if maj != cbg.MajArray {
return fmt.Errorf("expected cbor array")
}
if extra > 0 {
t.Stats = make([]string, extra)
}
for i := 0; i < int(extra); i++ {
{
var maj byte
var extra uint64
var err error
_ = maj
_ = extra
_ = err
{
sval, err := cbg.ReadStringWithMax(cr, 8192)
if err != nil {
return err
}
t.Stats[i] = string(sval)
}
}
}
// t.Bluesky (bool) (bool)
case "bluesky":
maj, extra, err = cr.ReadHeader()
if err != nil {
return err
}
if maj != cbg.MajOther {
return fmt.Errorf("booleans must be major type 7")
}
switch extra {
case 20:
t.Bluesky = false
case 21:
t.Bluesky = true
default:
return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra)
}
// t.Location (string) (string)
case "location":
{
sval, err := cbg.ReadStringWithMax(cr, 8192)
if err != nil {
return err
}
t.Location = string(sval)
}
// t.Description (string) (string)
case "description":
{
sval, err := cbg.ReadStringWithMax(cr, 8192)
if err != nil {
return err
}
t.Description = string(sval)
}
// t.PinnedRepositories ([]string) (slice)
case "pinnedRepositories":
maj, extra, err = cr.ReadHeader()
if err != nil {
return err
}
if extra > 8192 {
return fmt.Errorf("t.PinnedRepositories: array too large (%d)", extra)
}
if maj != cbg.MajArray {
return fmt.Errorf("expected cbor array")
}
if extra > 0 {
t.PinnedRepositories = make([]string, extra)
}
for i := 0; i < int(extra); i++ {
{
var maj byte
var extra uint64
var err error
_ = maj
_ = extra
_ = err
{
sval, err := cbg.ReadStringWithMax(cr, 8192)
if err != nil {
return err
}
t.PinnedRepositories[i] = string(sval)
}
}
}
default:
// Field doesn't exist on this type, so ignore it
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
return err
}
}
}
return nil
}
+2 -1
View File
@@ -25,10 +25,11 @@ import (
)
func main() {
// Generate map-style encoders for CrewRecord and CaptainRecord
// Generate map-style encoders for CrewRecord, CaptainRecord, and TangledProfileRecord
if err := cbg.WriteMapEncodersToFile("cbor_gen.go", "atproto",
atproto.CrewRecord{},
atproto.CaptainRecord{},
atproto.TangledProfileRecord{},
); err != nil {
fmt.Printf("Failed to generate CBOR encoders: %v\n", err)
os.Exit(1)
+18
View File
@@ -34,6 +34,10 @@ const (
// Note: Uses same collection name as HoldCrewCollection but stored in different PDS (hold's PDS vs owner's PDS)
CrewCollection = "io.atcr.hold.crew"
// TangledProfileCollection is the collection name for tangled profiles
// Stored in hold's embedded PDS (singleton record at rkey "self")
TangledProfileCollection = "sh.tangled.actor.profile"
// SailorProfileCollection is the collection name for user profiles
SailorProfileCollection = "io.atcr.sailor.profile"
@@ -515,3 +519,17 @@ type CrewRecord struct {
Permissions []string `json:"permissions" cborgen:"permissions"`
AddedAt string `json:"addedAt" cborgen:"addedAt"` // RFC3339 timestamp
}
// TangledProfileRecord represents a Tangled profile for the hold
// Collection: sh.tangled.actor.profile (singleton record at rkey "self")
// Stored in the hold's embedded PDS
// Uses CBOR encoding for efficient storage in hold's carstore
type TangledProfileRecord struct {
Type string `json:"$type" cborgen:"$type"`
Links []string `json:"links" cborgen:"links"`
Stats []string `json:"stats" cborgen:"stats"`
Bluesky bool `json:"bluesky" cborgen:"bluesky"`
Location string `json:"location" cborgen:"location"`
Description string `json:"description" cborgen:"description"`
PinnedRepositories []string `json:"pinnedRepositories" cborgen:"pinnedRepositories"`
}
+48
View File
@@ -9,6 +9,7 @@ import (
"net/http"
"time"
"atcr.io/pkg/atproto"
bsky "github.com/bluesky-social/indigo/api/bsky"
lexutil "github.com/bluesky-social/indigo/lex/util"
"github.com/distribution/distribution/v3/registry/storage/driver"
@@ -22,6 +23,12 @@ const (
// ProfileCollection is the collection name for Bluesky actor profiles
ProfileCollection = "app.bsky.actor.profile"
// TangledProfileRkey is the fixed rkey for the tangled profile record (singleton)
TangledProfileRkey = "self"
// TangledProfileCollection is the collection name for Tangled actor profiles
TangledProfileCollection = "sh.tangled.actor.profile"
)
// downloadImage downloads an image from a URL and returns the data and content type
@@ -172,3 +179,44 @@ func (p *HoldPDS) GetProfileRecord(ctx context.Context) (cid.Cid, *bsky.ActorPro
return recordCID, profileRecord, nil
}
// CreateTangledProfileRecord creates the sh.tangled.actor.profile record for the hold
// This will FAIL if the tangled profile record already exists.
func (p *HoldPDS) CreateTangledProfileRecord(ctx context.Context, links []string, description string) (cid.Cid, error) {
// Create tangled profile struct
profile := &atproto.TangledProfileRecord{
Type: atproto.TangledProfileCollection,
Links: links,
Stats: []string{}, // Empty for now
Bluesky: true,
Location: "",
Description: description,
PinnedRepositories: []string{}, // Empty for now
}
// Use repomgr.PutRecord - creates with explicit rkey, fails if already exists
recordPath, recordCID, err := p.repomgr.PutRecord(ctx, p.uid, TangledProfileCollection, TangledProfileRkey, profile)
if err != nil {
return cid.Undef, fmt.Errorf("failed to create tangled profile record: %w", err)
}
fmt.Printf("Created tangled profile record at %s, cid: %s\n", recordPath, recordCID)
return recordCID, nil
}
// GetTangledProfileRecord retrieves the sh.tangled.actor.profile record
func (p *HoldPDS) GetTangledProfileRecord(ctx context.Context) (cid.Cid, *atproto.TangledProfileRecord, error) {
// Use repomgr.GetRecord
recordCID, val, err := p.repomgr.GetRecord(ctx, p.uid, TangledProfileCollection, TangledProfileRkey, cid.Undef)
if err != nil {
return cid.Undef, nil, fmt.Errorf("failed to get tangled profile record: %w", err)
}
// Type assert to TangledProfileRecord
profileRecord, ok := val.(*atproto.TangledProfileRecord)
if !ok {
return cid.Undef, nil, fmt.Errorf("unexpected type for tangled profile record: %T", val)
}
return recordCID, profileRecord, nil
}
+23 -6
View File
@@ -20,10 +20,11 @@ import (
// init registers our custom ATProto types with indigo's lexutil type registry
// This allows repomgr.GetRecord to automatically unmarshal our types
func init() {
// Register captain and crew record types
// Register captain, crew, and tangled profile record types
// These must match the $type field in the records
lexutil.RegisterType(atproto.CaptainCollection, &atproto.CaptainRecord{})
lexutil.RegisterType(atproto.CrewCollection, &atproto.CrewRecord{})
lexutil.RegisterType(atproto.TangledProfileCollection, &atproto.TangledProfileRecord{})
}
// HoldPDS is a minimal ATProto PDS implementation for a hold service
@@ -157,23 +158,39 @@ func (p *HoldPDS) Bootstrap(ctx context.Context, storageDriver driver.StorageDri
fmt.Printf("✅ Added %s as hold admin\n", ownerDID)
}
// Create profile record (idempotent - check if exists first)
// Create Bluesky profile record (idempotent - check if exists first)
// This runs even if captain exists (for existing holds being upgraded)
// Skip if no storage driver (e.g., in tests)
if storageDriver != nil {
_, _, err = p.GetProfileRecord(ctx)
if err != nil {
// Profile doesn't exist, create it
// Bluesky profile doesn't exist, create it
displayName := "Cargo Hold"
description := "ahoy from the cargo hold"
_, err = p.CreateProfileRecord(ctx, storageDriver, displayName, description, avatarURL)
if err != nil {
return fmt.Errorf("failed to create profile record: %w", err)
return fmt.Errorf("failed to create bluesky profile record: %w", err)
}
fmt.Printf("✅ Created profile record (displayName=%s)\n", displayName)
fmt.Printf("✅ Created Bluesky profile record (displayName=%s)\n", displayName)
} else {
fmt.Printf("✅ Profile record already exists, skipping\n")
fmt.Printf("✅ Bluesky profile record already exists, skipping\n")
}
// Create Tangled profile record (idempotent - check if exists first)
_, _, err = p.GetTangledProfileRecord(ctx)
if err != nil {
// Tangled profile doesn't exist, create it
description := "ahoy from the cargo hold"
links := []string{"https://atcr.io"}
_, err = p.CreateTangledProfileRecord(ctx, links, description)
if err != nil {
return fmt.Errorf("failed to create tangled profile record: %w", err)
}
fmt.Printf("✅ Created Tangled profile record\n")
} else {
fmt.Printf("✅ Tangled profile record already exists, skipping\n")
}
}