A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+516 -7
+4
cmd/hold/main.go
··· 17 17 _ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws" 18 18 19 19 "github.com/go-chi/chi/v5" 20 + "github.com/go-chi/chi/v5/middleware" 20 21 ) 21 22 22 23 func main() { ··· 91 92 92 93 // Setup HTTP routes with chi router 93 94 r := chi.NewRouter() 95 + 96 + // Add logging middleware to log all HTTP requests 97 + r.Use(middleware.Logger) 94 98 95 99 // Root page 96 100 r.Get("/", func(w http.ResponseWriter, r *http.Request) {
+421
pkg/atproto/cbor_gen.go
··· 612 612 613 613 return nil 614 614 } 615 + func (t *TangledProfileRecord) MarshalCBOR(w io.Writer) error { 616 + if t == nil { 617 + _, err := w.Write(cbg.CborNull) 618 + return err 619 + } 620 + 621 + cw := cbg.NewCborWriter(w) 622 + 623 + if _, err := cw.Write([]byte{167}); err != nil { 624 + return err 625 + } 626 + 627 + // t.Type (string) (string) 628 + if len("$type") > 8192 { 629 + return xerrors.Errorf("Value in field \"$type\" was too long") 630 + } 631 + 632 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 633 + return err 634 + } 635 + if _, err := cw.WriteString(string("$type")); err != nil { 636 + return err 637 + } 638 + 639 + if len(t.Type) > 8192 { 640 + return xerrors.Errorf("Value in field t.Type was too long") 641 + } 642 + 643 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Type))); err != nil { 644 + return err 645 + } 646 + if _, err := cw.WriteString(string(t.Type)); err != nil { 647 + return err 648 + } 649 + 650 + // t.Links ([]string) (slice) 651 + if len("links") > 8192 { 652 + return xerrors.Errorf("Value in field \"links\" was too long") 653 + } 654 + 655 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("links"))); err != nil { 656 + return err 657 + } 658 + if _, err := cw.WriteString(string("links")); err != nil { 659 + return err 660 + } 661 + 662 + if len(t.Links) > 8192 { 663 + return xerrors.Errorf("Slice value in field t.Links was too long") 664 + } 665 + 666 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Links))); err != nil { 667 + return err 668 + } 669 + for _, v := range t.Links { 670 + if len(v) > 8192 { 671 + return xerrors.Errorf("Value in field v was too long") 672 + } 673 + 674 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 675 + return err 676 + } 677 + if _, err := cw.WriteString(string(v)); err != nil { 678 + return err 679 + } 680 + 681 + } 682 + 683 + // t.Stats ([]string) (slice) 684 + if len("stats") > 8192 { 685 + return xerrors.Errorf("Value in field \"stats\" was too long") 686 + } 687 + 688 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("stats"))); err != nil { 689 + return err 690 + } 691 + if _, err := cw.WriteString(string("stats")); err != nil { 692 + return err 693 + } 694 + 695 + if len(t.Stats) > 8192 { 696 + return xerrors.Errorf("Slice value in field t.Stats was too long") 697 + } 698 + 699 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Stats))); err != nil { 700 + return err 701 + } 702 + for _, v := range t.Stats { 703 + if len(v) > 8192 { 704 + return xerrors.Errorf("Value in field v was too long") 705 + } 706 + 707 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 708 + return err 709 + } 710 + if _, err := cw.WriteString(string(v)); err != nil { 711 + return err 712 + } 713 + 714 + } 715 + 716 + // t.Bluesky (bool) (bool) 717 + if len("bluesky") > 8192 { 718 + return xerrors.Errorf("Value in field \"bluesky\" was too long") 719 + } 720 + 721 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("bluesky"))); err != nil { 722 + return err 723 + } 724 + if _, err := cw.WriteString(string("bluesky")); err != nil { 725 + return err 726 + } 727 + 728 + if err := cbg.WriteBool(w, t.Bluesky); err != nil { 729 + return err 730 + } 731 + 732 + // t.Location (string) (string) 733 + if len("location") > 8192 { 734 + return xerrors.Errorf("Value in field \"location\" was too long") 735 + } 736 + 737 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("location"))); err != nil { 738 + return err 739 + } 740 + if _, err := cw.WriteString(string("location")); err != nil { 741 + return err 742 + } 743 + 744 + if len(t.Location) > 8192 { 745 + return xerrors.Errorf("Value in field t.Location was too long") 746 + } 747 + 748 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Location))); err != nil { 749 + return err 750 + } 751 + if _, err := cw.WriteString(string(t.Location)); err != nil { 752 + return err 753 + } 754 + 755 + // t.Description (string) (string) 756 + if len("description") > 8192 { 757 + return xerrors.Errorf("Value in field \"description\" was too long") 758 + } 759 + 760 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { 761 + return err 762 + } 763 + if _, err := cw.WriteString(string("description")); err != nil { 764 + return err 765 + } 766 + 767 + if len(t.Description) > 8192 { 768 + return xerrors.Errorf("Value in field t.Description was too long") 769 + } 770 + 771 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Description))); err != nil { 772 + return err 773 + } 774 + if _, err := cw.WriteString(string(t.Description)); err != nil { 775 + return err 776 + } 777 + 778 + // t.PinnedRepositories ([]string) (slice) 779 + if len("pinnedRepositories") > 8192 { 780 + return xerrors.Errorf("Value in field \"pinnedRepositories\" was too long") 781 + } 782 + 783 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedRepositories"))); err != nil { 784 + return err 785 + } 786 + if _, err := cw.WriteString(string("pinnedRepositories")); err != nil { 787 + return err 788 + } 789 + 790 + if len(t.PinnedRepositories) > 8192 { 791 + return xerrors.Errorf("Slice value in field t.PinnedRepositories was too long") 792 + } 793 + 794 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PinnedRepositories))); err != nil { 795 + return err 796 + } 797 + for _, v := range t.PinnedRepositories { 798 + if len(v) > 8192 { 799 + return xerrors.Errorf("Value in field v was too long") 800 + } 801 + 802 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 803 + return err 804 + } 805 + if _, err := cw.WriteString(string(v)); err != nil { 806 + return err 807 + } 808 + 809 + } 810 + return nil 811 + } 812 + 813 + func (t *TangledProfileRecord) UnmarshalCBOR(r io.Reader) (err error) { 814 + *t = TangledProfileRecord{} 815 + 816 + cr := cbg.NewCborReader(r) 817 + 818 + maj, extra, err := cr.ReadHeader() 819 + if err != nil { 820 + return err 821 + } 822 + defer func() { 823 + if err == io.EOF { 824 + err = io.ErrUnexpectedEOF 825 + } 826 + }() 827 + 828 + if maj != cbg.MajMap { 829 + return fmt.Errorf("cbor input should be of type map") 830 + } 831 + 832 + if extra > cbg.MaxLength { 833 + return fmt.Errorf("TangledProfileRecord: map struct too large (%d)", extra) 834 + } 835 + 836 + n := extra 837 + 838 + nameBuf := make([]byte, 18) 839 + for i := uint64(0); i < n; i++ { 840 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 841 + if err != nil { 842 + return err 843 + } 844 + 845 + if !ok { 846 + // Field doesn't exist on this type, so ignore it 847 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 848 + return err 849 + } 850 + continue 851 + } 852 + 853 + switch string(nameBuf[:nameLen]) { 854 + // t.Type (string) (string) 855 + case "$type": 856 + 857 + { 858 + sval, err := cbg.ReadStringWithMax(cr, 8192) 859 + if err != nil { 860 + return err 861 + } 862 + 863 + t.Type = string(sval) 864 + } 865 + // t.Links ([]string) (slice) 866 + case "links": 867 + 868 + maj, extra, err = cr.ReadHeader() 869 + if err != nil { 870 + return err 871 + } 872 + 873 + if extra > 8192 { 874 + return fmt.Errorf("t.Links: array too large (%d)", extra) 875 + } 876 + 877 + if maj != cbg.MajArray { 878 + return fmt.Errorf("expected cbor array") 879 + } 880 + 881 + if extra > 0 { 882 + t.Links = make([]string, extra) 883 + } 884 + 885 + for i := 0; i < int(extra); i++ { 886 + { 887 + var maj byte 888 + var extra uint64 889 + var err error 890 + _ = maj 891 + _ = extra 892 + _ = err 893 + 894 + { 895 + sval, err := cbg.ReadStringWithMax(cr, 8192) 896 + if err != nil { 897 + return err 898 + } 899 + 900 + t.Links[i] = string(sval) 901 + } 902 + 903 + } 904 + } 905 + // t.Stats ([]string) (slice) 906 + case "stats": 907 + 908 + maj, extra, err = cr.ReadHeader() 909 + if err != nil { 910 + return err 911 + } 912 + 913 + if extra > 8192 { 914 + return fmt.Errorf("t.Stats: array too large (%d)", extra) 915 + } 916 + 917 + if maj != cbg.MajArray { 918 + return fmt.Errorf("expected cbor array") 919 + } 920 + 921 + if extra > 0 { 922 + t.Stats = make([]string, extra) 923 + } 924 + 925 + for i := 0; i < int(extra); i++ { 926 + { 927 + var maj byte 928 + var extra uint64 929 + var err error 930 + _ = maj 931 + _ = extra 932 + _ = err 933 + 934 + { 935 + sval, err := cbg.ReadStringWithMax(cr, 8192) 936 + if err != nil { 937 + return err 938 + } 939 + 940 + t.Stats[i] = string(sval) 941 + } 942 + 943 + } 944 + } 945 + // t.Bluesky (bool) (bool) 946 + case "bluesky": 947 + 948 + maj, extra, err = cr.ReadHeader() 949 + if err != nil { 950 + return err 951 + } 952 + if maj != cbg.MajOther { 953 + return fmt.Errorf("booleans must be major type 7") 954 + } 955 + switch extra { 956 + case 20: 957 + t.Bluesky = false 958 + case 21: 959 + t.Bluesky = true 960 + default: 961 + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) 962 + } 963 + // t.Location (string) (string) 964 + case "location": 965 + 966 + { 967 + sval, err := cbg.ReadStringWithMax(cr, 8192) 968 + if err != nil { 969 + return err 970 + } 971 + 972 + t.Location = string(sval) 973 + } 974 + // t.Description (string) (string) 975 + case "description": 976 + 977 + { 978 + sval, err := cbg.ReadStringWithMax(cr, 8192) 979 + if err != nil { 980 + return err 981 + } 982 + 983 + t.Description = string(sval) 984 + } 985 + // t.PinnedRepositories ([]string) (slice) 986 + case "pinnedRepositories": 987 + 988 + maj, extra, err = cr.ReadHeader() 989 + if err != nil { 990 + return err 991 + } 992 + 993 + if extra > 8192 { 994 + return fmt.Errorf("t.PinnedRepositories: array too large (%d)", extra) 995 + } 996 + 997 + if maj != cbg.MajArray { 998 + return fmt.Errorf("expected cbor array") 999 + } 1000 + 1001 + if extra > 0 { 1002 + t.PinnedRepositories = make([]string, extra) 1003 + } 1004 + 1005 + for i := 0; i < int(extra); i++ { 1006 + { 1007 + var maj byte 1008 + var extra uint64 1009 + var err error 1010 + _ = maj 1011 + _ = extra 1012 + _ = err 1013 + 1014 + { 1015 + sval, err := cbg.ReadStringWithMax(cr, 8192) 1016 + if err != nil { 1017 + return err 1018 + } 1019 + 1020 + t.PinnedRepositories[i] = string(sval) 1021 + } 1022 + 1023 + } 1024 + } 1025 + 1026 + default: 1027 + // Field doesn't exist on this type, so ignore it 1028 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 1029 + return err 1030 + } 1031 + } 1032 + } 1033 + 1034 + return nil 1035 + }
+2 -1
pkg/atproto/generate.go
··· 25 25 ) 26 26 27 27 func main() { 28 - // Generate map-style encoders for CrewRecord and CaptainRecord 28 + // Generate map-style encoders for CrewRecord, CaptainRecord, and TangledProfileRecord 29 29 if err := cbg.WriteMapEncodersToFile("cbor_gen.go", "atproto", 30 30 atproto.CrewRecord{}, 31 31 atproto.CaptainRecord{}, 32 + atproto.TangledProfileRecord{}, 32 33 ); err != nil { 33 34 fmt.Printf("Failed to generate CBOR encoders: %v\n", err) 34 35 os.Exit(1)
+18
pkg/atproto/lexicon.go
··· 34 34 // Note: Uses same collection name as HoldCrewCollection but stored in different PDS (hold's PDS vs owner's PDS) 35 35 CrewCollection = "io.atcr.hold.crew" 36 36 37 + // TangledProfileCollection is the collection name for tangled profiles 38 + // Stored in hold's embedded PDS (singleton record at rkey "self") 39 + TangledProfileCollection = "sh.tangled.actor.profile" 40 + 37 41 // SailorProfileCollection is the collection name for user profiles 38 42 SailorProfileCollection = "io.atcr.sailor.profile" 39 43 ··· 515 519 Permissions []string `json:"permissions" cborgen:"permissions"` 516 520 AddedAt string `json:"addedAt" cborgen:"addedAt"` // RFC3339 timestamp 517 521 } 522 + 523 + // TangledProfileRecord represents a Tangled profile for the hold 524 + // Collection: sh.tangled.actor.profile (singleton record at rkey "self") 525 + // Stored in the hold's embedded PDS 526 + // Uses CBOR encoding for efficient storage in hold's carstore 527 + type TangledProfileRecord struct { 528 + Type string `json:"$type" cborgen:"$type"` 529 + Links []string `json:"links" cborgen:"links"` 530 + Stats []string `json:"stats" cborgen:"stats"` 531 + Bluesky bool `json:"bluesky" cborgen:"bluesky"` 532 + Location string `json:"location" cborgen:"location"` 533 + Description string `json:"description" cborgen:"description"` 534 + PinnedRepositories []string `json:"pinnedRepositories" cborgen:"pinnedRepositories"` 535 + }
+48
pkg/hold/pds/profile.go
··· 9 9 "net/http" 10 10 "time" 11 11 12 + "atcr.io/pkg/atproto" 12 13 bsky "github.com/bluesky-social/indigo/api/bsky" 13 14 lexutil "github.com/bluesky-social/indigo/lex/util" 14 15 "github.com/distribution/distribution/v3/registry/storage/driver" ··· 22 23 23 24 // ProfileCollection is the collection name for Bluesky actor profiles 24 25 ProfileCollection = "app.bsky.actor.profile" 26 + 27 + // TangledProfileRkey is the fixed rkey for the tangled profile record (singleton) 28 + TangledProfileRkey = "self" 29 + 30 + // TangledProfileCollection is the collection name for Tangled actor profiles 31 + TangledProfileCollection = "sh.tangled.actor.profile" 25 32 ) 26 33 27 34 // downloadImage downloads an image from a URL and returns the data and content type ··· 172 179 173 180 return recordCID, profileRecord, nil 174 181 } 182 + 183 + // CreateTangledProfileRecord creates the sh.tangled.actor.profile record for the hold 184 + // This will FAIL if the tangled profile record already exists. 185 + func (p *HoldPDS) CreateTangledProfileRecord(ctx context.Context, links []string, description string) (cid.Cid, error) { 186 + // Create tangled profile struct 187 + profile := &atproto.TangledProfileRecord{ 188 + Type: atproto.TangledProfileCollection, 189 + Links: links, 190 + Stats: []string{}, // Empty for now 191 + Bluesky: true, 192 + Location: "", 193 + Description: description, 194 + PinnedRepositories: []string{}, // Empty for now 195 + } 196 + 197 + // Use repomgr.PutRecord - creates with explicit rkey, fails if already exists 198 + recordPath, recordCID, err := p.repomgr.PutRecord(ctx, p.uid, TangledProfileCollection, TangledProfileRkey, profile) 199 + if err != nil { 200 + return cid.Undef, fmt.Errorf("failed to create tangled profile record: %w", err) 201 + } 202 + 203 + fmt.Printf("Created tangled profile record at %s, cid: %s\n", recordPath, recordCID) 204 + return recordCID, nil 205 + } 206 + 207 + // GetTangledProfileRecord retrieves the sh.tangled.actor.profile record 208 + func (p *HoldPDS) GetTangledProfileRecord(ctx context.Context) (cid.Cid, *atproto.TangledProfileRecord, error) { 209 + // Use repomgr.GetRecord 210 + recordCID, val, err := p.repomgr.GetRecord(ctx, p.uid, TangledProfileCollection, TangledProfileRkey, cid.Undef) 211 + if err != nil { 212 + return cid.Undef, nil, fmt.Errorf("failed to get tangled profile record: %w", err) 213 + } 214 + 215 + // Type assert to TangledProfileRecord 216 + profileRecord, ok := val.(*atproto.TangledProfileRecord) 217 + if !ok { 218 + return cid.Undef, nil, fmt.Errorf("unexpected type for tangled profile record: %T", val) 219 + } 220 + 221 + return recordCID, profileRecord, nil 222 + }
+23 -6
pkg/hold/pds/server.go
··· 20 20 // init registers our custom ATProto types with indigo's lexutil type registry 21 21 // This allows repomgr.GetRecord to automatically unmarshal our types 22 22 func init() { 23 - // Register captain and crew record types 23 + // Register captain, crew, and tangled profile record types 24 24 // These must match the $type field in the records 25 25 lexutil.RegisterType(atproto.CaptainCollection, &atproto.CaptainRecord{}) 26 26 lexutil.RegisterType(atproto.CrewCollection, &atproto.CrewRecord{}) 27 + lexutil.RegisterType(atproto.TangledProfileCollection, &atproto.TangledProfileRecord{}) 27 28 } 28 29 29 30 // HoldPDS is a minimal ATProto PDS implementation for a hold service ··· 157 158 fmt.Printf("✅ Added %s as hold admin\n", ownerDID) 158 159 } 159 160 160 - // Create profile record (idempotent - check if exists first) 161 + // Create Bluesky profile record (idempotent - check if exists first) 161 162 // This runs even if captain exists (for existing holds being upgraded) 162 163 // Skip if no storage driver (e.g., in tests) 163 164 if storageDriver != nil { 164 165 _, _, err = p.GetProfileRecord(ctx) 165 166 if err != nil { 166 - // Profile doesn't exist, create it 167 + // Bluesky profile doesn't exist, create it 167 168 displayName := "Cargo Hold" 168 169 description := "ahoy from the cargo hold" 169 170 170 171 _, err = p.CreateProfileRecord(ctx, storageDriver, displayName, description, avatarURL) 171 172 if err != nil { 172 - return fmt.Errorf("failed to create profile record: %w", err) 173 + return fmt.Errorf("failed to create bluesky profile record: %w", err) 173 174 } 174 - fmt.Printf("✅ Created profile record (displayName=%s)\n", displayName) 175 + fmt.Printf("✅ Created Bluesky profile record (displayName=%s)\n", displayName) 175 176 } else { 176 - fmt.Printf("✅ Profile record already exists, skipping\n") 177 + fmt.Printf("✅ Bluesky profile record already exists, skipping\n") 178 + } 179 + 180 + // Create Tangled profile record (idempotent - check if exists first) 181 + _, _, err = p.GetTangledProfileRecord(ctx) 182 + if err != nil { 183 + // Tangled profile doesn't exist, create it 184 + description := "ahoy from the cargo hold" 185 + links := []string{"https://atcr.io"} 186 + 187 + _, err = p.CreateTangledProfileRecord(ctx, links, description) 188 + if err != nil { 189 + return fmt.Errorf("failed to create tangled profile record: %w", err) 190 + } 191 + fmt.Printf("✅ Created Tangled profile record\n") 192 + } else { 193 + fmt.Printf("✅ Tangled profile record already exists, skipping\n") 177 194 } 178 195 } 179 196