From 21e6d08f7530a4955cefd3834940cc4e75e43477 Mon Sep 17 00:00:00 2001 From: Evan Jarrett Date: Tue, 14 Oct 2025 22:11:00 -0500 Subject: [PATCH] cbor_gen crew --- gen/main.go | 32 +++++ go.mod | 6 +- go.sum | 4 +- pkg/hold/authorization.go | 2 +- pkg/hold/pds/cbor_gen.go | 295 ++++++++++++++++++++++++++++++++++++++ pkg/hold/pds/crew.go | 28 +--- pkg/hold/pds/did.go | 15 +- pkg/hold/pds/server.go | 13 +- pkg/hold/pds/types.go | 16 +++ pkg/hold/pds/xrpc.go | 6 +- 10 files changed, 370 insertions(+), 47 deletions(-) create mode 100644 gen/main.go create mode 100644 pkg/hold/pds/cbor_gen.go create mode 100644 pkg/hold/pds/types.go diff --git a/gen/main.go b/gen/main.go new file mode 100644 index 0000000..c589778 --- /dev/null +++ b/gen/main.go @@ -0,0 +1,32 @@ +package main + +// CBOR Code Generator +// +// This generates optimized CBOR marshaling code for ATProto records. +// +// Usage: +// go run gen/main.go +// +// This creates pkg/hold/pds/cbor_gen.go which should be committed to git. +// Only re-run when you modify types in pkg/hold/pds/types.go + +import ( + "fmt" + "os" + + cbg "github.com/whyrusleeping/cbor-gen" + + "atcr.io/pkg/hold/pds" +) + +func main() { + // Generate map-style encoders for CrewRecord + if err := cbg.WriteMapEncodersToFile("pkg/hold/pds/cbor_gen.go", "pds", + pds.CrewRecord{}, + ); err != nil { + fmt.Printf("Failed to generate CBOR encoders: %v\n", err) + os.Exit(1) + } + + fmt.Println("Generated CBOR encoders in pkg/hold/pds/cbor_gen.go") +} diff --git a/go.mod b/go.mod index e776e47..249521f 100644 --- a/go.mod +++ b/go.mod @@ -11,13 +11,15 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 + github.com/ipfs/go-cid v0.4.1 github.com/klauspost/compress v1.18.0 github.com/mattn/go-sqlite3 v1.14.32 github.com/opencontainers/go-digest v1.0.0 github.com/spf13/cobra v1.8.0 + github.com/whyrusleeping/cbor-gen v0.3.1 go.yaml.in/yaml/v4 v4.0.0-rc.2 golang.org/x/crypto v0.39.0 - github.com/ipfs/go-cid v0.4.1 + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 ) require ( @@ -100,7 +102,6 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 // indirect @@ -133,7 +134,6 @@ require ( golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.6.0 // indirect - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241104194629-dd2ea8efbc28 // indirect google.golang.org/grpc v1.68.0 // indirect diff --git a/go.sum b/go.sum index c1adf6c..aa90135 100644 --- a/go.sum +++ b/go.sum @@ -370,8 +370,8 @@ github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSD github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= -github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= -github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= +github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0= +github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= diff --git a/pkg/hold/authorization.go b/pkg/hold/authorization.go index 31dec64..bd26029 100644 --- a/pkg/hold/authorization.go +++ b/pkg/hold/authorization.go @@ -163,7 +163,7 @@ func (s *HoldService) isCrewMember(did string) (bool, error) { log.Printf("Warning: failed to resolve handle for DID %s: %v", did, err) // Continue checking explicit DIDs even if handle resolution fails handleResolved = true // Mark as attempted (don't retry) - handle = "" // Empty handle won't match patterns + handle = "" // Empty handle won't match patterns } else { handleResolved = true } diff --git a/pkg/hold/pds/cbor_gen.go b/pkg/hold/pds/cbor_gen.go new file mode 100644 index 0000000..be3a671 --- /dev/null +++ b/pkg/hold/pds/cbor_gen.go @@ -0,0 +1,295 @@ +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package pds + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +func (t *CrewRecord) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{165}); err != nil { + return err + } + + // t.Role (string) (string) + if len("role") > 8192 { + return xerrors.Errorf("Value in field \"role\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("role"))); err != nil { + return err + } + if _, err := cw.WriteString(string("role")); err != nil { + return err + } + + if len(t.Role) > 8192 { + return xerrors.Errorf("Value in field t.Role was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Role))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Role)); 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.Member (string) (string) + if len("member") > 8192 { + return xerrors.Errorf("Value in field \"member\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("member"))); err != nil { + return err + } + if _, err := cw.WriteString(string("member")); err != nil { + return err + } + + if len(t.Member) > 8192 { + return xerrors.Errorf("Value in field t.Member was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Member))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Member)); err != nil { + return err + } + + // t.AddedAt (string) (string) + if len("addedAt") > 8192 { + return xerrors.Errorf("Value in field \"addedAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("addedAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("addedAt")); err != nil { + return err + } + + if len(t.AddedAt) > 8192 { + return xerrors.Errorf("Value in field t.AddedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.AddedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.AddedAt)); err != nil { + return err + } + + // t.Permissions ([]string) (slice) + if len("permissions") > 8192 { + return xerrors.Errorf("Value in field \"permissions\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("permissions"))); err != nil { + return err + } + if _, err := cw.WriteString(string("permissions")); err != nil { + return err + } + + if len(t.Permissions) > 8192 { + return xerrors.Errorf("Slice value in field t.Permissions was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Permissions))); err != nil { + return err + } + for _, v := range t.Permissions { + 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 *CrewRecord) UnmarshalCBOR(r io.Reader) (err error) { + *t = CrewRecord{} + + 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("CrewRecord: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 11) + 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.Role (string) (string) + case "role": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Role = string(sval) + } + // t.Type (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Type = string(sval) + } + // t.Member (string) (string) + case "member": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.Member = string(sval) + } + // t.AddedAt (string) (string) + case "addedAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 8192) + if err != nil { + return err + } + + t.AddedAt = string(sval) + } + // t.Permissions ([]string) (slice) + case "permissions": + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > 8192 { + return fmt.Errorf("t.Permissions: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Permissions = 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.Permissions[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 +} diff --git a/pkg/hold/pds/crew.go b/pkg/hold/pds/crew.go index 59e304f..8a86ca9 100644 --- a/pkg/hold/pds/crew.go +++ b/pkg/hold/pds/crew.go @@ -3,43 +3,19 @@ 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 and commits to carstore func (p *HoldPDS) AddCrewMember(ctx context.Context, memberDID, role string, permissions []string) (cid.Cid, error) { crewRecord := &CrewRecord{ + Type: CrewCollection, Member: memberDID, Role: role, Permissions: permissions, - AddedAt: time.Now(), + AddedAt: time.Now().Format(time.RFC3339), } // Create record in repo (using memberDID as rkey for easy lookup) diff --git a/pkg/hold/pds/did.go b/pkg/hold/pds/did.go index b49f61a..eac9d05 100644 --- a/pkg/hold/pds/did.go +++ b/pkg/hold/pds/did.go @@ -9,13 +9,13 @@ import ( // 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"` + 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 @@ -83,7 +83,6 @@ func (p *HoldPDS) GenerateDIDDocument(publicURL string) (*DIDDocument, error) { return doc, 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) diff --git a/pkg/hold/pds/server.go b/pkg/hold/pds/server.go index ba7510b..6b0c3fa 100644 --- a/pkg/hold/pds/server.go +++ b/pkg/hold/pds/server.go @@ -101,15 +101,20 @@ func (p *HoldPDS) SigningKey() *crypto.PrivateKeyK256 { // Bootstrap initializes the hold with the owner as the first crew member func (p *HoldPDS) Bootstrap(ctx context.Context, ownerDID string) error { if ownerDID == "" { - // No owner specified, skip bootstrap return nil } // Check if repo already has commits - _, err := p.carstore.GetUserRepoHead(ctx, p.uid) + head, err := p.carstore.GetUserRepoHead(ctx, p.uid) if err == nil { - // Repo already has commits, skip bootstrap - return 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 diff --git a/pkg/hold/pds/types.go b/pkg/hold/pds/types.go new file mode 100644 index 0000000..a95297c --- /dev/null +++ b/pkg/hold/pds/types.go @@ -0,0 +1,16 @@ +package pds + +// ATProto record types for the hold service + +// CrewRecord represents a crew member in the hold +type CrewRecord struct { + Type string `cborgen:"$type"` + Member string `cborgen:"member"` + Role string `cborgen:"role"` + Permissions []string `cborgen:"permissions"` + AddedAt string `cborgen:"addedAt"` // RFC3339 timestamp +} + +const ( + CrewCollection = "io.atcr.hold.crew" +) diff --git a/pkg/hold/pds/xrpc.go b/pkg/hold/pds/xrpc.go index f11c2f5..da75808 100644 --- a/pkg/hold/pds/xrpc.go +++ b/pkg/hold/pds/xrpc.go @@ -11,9 +11,9 @@ import ( // XRPCHandler handles XRPC requests for the embedded PDS type XRPCHandler struct { - pds *HoldPDS - publicURL string - blobStore BlobStore + pds *HoldPDS + publicURL string + blobStore BlobStore } // BlobStore interface wraps the existing hold service storage operations