Monorepo for Tangled

[WIP] appview, knotserver: DIDs as git repo IDs

+3287 -978
+2 -1
api/tangled/actorprofile.go
··· 28 // location: Free-form location text. 29 Location *string `json:"location,omitempty" cborgen:"location,omitempty"` 30 // pinnedRepositories: Any ATURI, it is up to appviews to validate these fields. 31 - PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"` 32 // pronouns: Preferred gender pronouns. 33 Pronouns *string `json:"pronouns,omitempty" cborgen:"pronouns,omitempty"` 34 Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"`
··· 28 // location: Free-form location text. 29 Location *string `json:"location,omitempty" cborgen:"location,omitempty"` 30 // pinnedRepositories: Any ATURI, it is up to appviews to validate these fields. 31 + PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"` 32 + PinnedRepositoryDids []string `json:"pinnedRepositoryDids,omitempty" cborgen:"pinnedRepositoryDids,omitempty"` 33 // pronouns: Preferred gender pronouns. 34 Pronouns *string `json:"pronouns,omitempty" cborgen:"pronouns,omitempty"` 35 Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"`
+891 -110
api/tangled/cbor_gen.go
··· 26 } 27 28 cw := cbg.NewCborWriter(w) 29 - fieldCount := 9 30 31 if t.Avatar == nil { 32 fieldCount-- ··· 45 } 46 47 if t.PinnedRepositories == nil { 48 fieldCount-- 49 } 50 ··· 317 318 } 319 } 320 return nil 321 } 322 ··· 345 346 n := extra 347 348 - nameBuf := make([]byte, 18) 349 for i := uint64(0); i < n; i++ { 350 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 351 if err != nil { ··· 592 } 593 594 } 595 } 596 597 default: ··· 611 } 612 613 cw := cbg.NewCborWriter(w) 614 615 - if _, err := cw.Write([]byte{164}); err != nil { 616 return err 617 } 618 ··· 703 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 704 return err 705 } 706 return nil 707 } 708 ··· 731 732 n := extra 733 734 - nameBuf := make([]byte, 9) 735 for i := uint64(0); i < n; i++ { 736 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 737 if err != nil { ··· 791 792 t.CreatedAt = string(sval) 793 } 794 795 default: 796 // Field doesn't exist on this type, so ignore it ··· 809 } 810 811 cw := cbg.NewCborWriter(w) 812 813 - if _, err := cw.Write([]byte{163}); err != nil { 814 return err 815 } 816 ··· 834 } 835 836 // t.Subject (string) (string) 837 - if len("subject") > 1000000 { 838 - return xerrors.Errorf("Value in field \"subject\" was too long") 839 - } 840 841 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 842 - return err 843 - } 844 - if _, err := cw.WriteString(string("subject")); err != nil { 845 - return err 846 - } 847 848 - if len(t.Subject) > 1000000 { 849 - return xerrors.Errorf("Value in field t.Subject was too long") 850 - } 851 852 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 853 - return err 854 - } 855 - if _, err := cw.WriteString(string(t.Subject)); err != nil { 856 - return err 857 } 858 859 // t.CreatedAt (string) (string) ··· 878 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 879 return err 880 } 881 return nil 882 } 883 ··· 906 907 n := extra 908 909 - nameBuf := make([]byte, 9) 910 for i := uint64(0); i < n; i++ { 911 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 912 if err != nil { ··· 937 case "subject": 938 939 { 940 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 941 if err != nil { 942 return err 943 } 944 945 - t.Subject = string(sval) 946 } 947 // t.CreatedAt (string) (string) 948 case "createdAt": ··· 955 956 t.CreatedAt = string(sval) 957 } 958 959 default: 960 // Field doesn't exist on this type, so ignore it ··· 974 975 cw := cbg.NewCborWriter(w) 976 977 - if _, err := cw.Write([]byte{168}); err != nil { 978 return err 979 } 980 ··· 1105 return err 1106 } 1107 1108 // t.RepoName (string) (string) 1109 if len("repoName") > 1000000 { 1110 return xerrors.Errorf("Value in field \"repoName\" was too long") ··· 1268 } 1269 1270 t.RepoDid = string(sval) 1271 } 1272 // t.RepoName (string) (string) 1273 case "repoName": ··· 3184 } 3185 3186 cw := cbg.NewCborWriter(w) 3187 3188 - if _, err := cw.Write([]byte{165}); err != nil { 3189 return err 3190 } 3191 ··· 3283 return err 3284 } 3285 3286 // t.PerformedAt (string) (string) 3287 if len("performedAt") > 1000000 { 3288 return xerrors.Errorf("Value in field \"performedAt\" was too long") ··· 3468 } 3469 3470 t.Subject = string(sval) 3471 } 3472 // t.PerformedAt (string) (string) 3473 case "performedAt": ··· 5303 5304 cw := cbg.NewCborWriter(w) 5305 5306 - if _, err := cw.Write([]byte{164}); err != nil { 5307 return err 5308 } 5309 ··· 5376 return err 5377 } 5378 5379 // t.DefaultBranch (string) (string) 5380 if len("defaultBranch") > 1000000 { 5381 return xerrors.Errorf("Value in field \"defaultBranch\" was too long") ··· 5474 } 5475 5476 t.Repo = string(sval) 5477 } 5478 // t.DefaultBranch (string) (string) 5479 case "defaultBranch": ··· 5906 } 5907 5908 cw := cbg.NewCborWriter(w) 5909 - fieldCount := 10 5910 5911 if t.Description == nil { 5912 fieldCount-- 5913 } 5914 5915 if t.Labels == nil { 5916 fieldCount-- 5917 } 5918 ··· 6102 return err 6103 } 6104 6105 } 6106 } 6107 ··· 6401 6402 } 6403 } 6404 // t.Spindle (string) (string) 6405 case "spindle": 6406 ··· 6493 } 6494 6495 cw := cbg.NewCborWriter(w) 6496 - fieldCount := 6 6497 6498 if t.Tag == nil { 6499 fieldCount-- ··· 6555 } 6556 6557 // t.Repo (string) (string) 6558 - if len("repo") > 1000000 { 6559 - return xerrors.Errorf("Value in field \"repo\" was too long") 6560 - } 6561 6562 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 6563 - return err 6564 - } 6565 - if _, err := cw.WriteString(string("repo")); err != nil { 6566 - return err 6567 - } 6568 6569 - if len(t.Repo) > 1000000 { 6570 - return xerrors.Errorf("Value in field t.Repo was too long") 6571 - } 6572 6573 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 6574 - return err 6575 - } 6576 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 6577 - return err 6578 } 6579 6580 // t.LexiconTypeID (string) (string) ··· 6596 return err 6597 } 6598 6599 // t.Artifact (util.LexBlob) (struct) 6600 if len("artifact") > 1000000 { 6601 return xerrors.Errorf("Value in field \"artifact\" was too long") ··· 6716 case "repo": 6717 6718 { 6719 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 6720 if err != nil { 6721 return err 6722 } 6723 6724 - t.Repo = string(sval) 6725 } 6726 // t.LexiconTypeID (string) (string) 6727 case "$type": ··· 6733 } 6734 6735 t.LexiconTypeID = string(sval) 6736 } 6737 // t.Artifact (util.LexBlob) (struct) 6738 case "artifact": ··· 6783 } 6784 6785 cw := cbg.NewCborWriter(w) 6786 6787 - if _, err := cw.Write([]byte{164}); err != nil { 6788 - return err 6789 } 6790 6791 - // t.Repo (string) (string) 6792 - if len("repo") > 1000000 { 6793 - return xerrors.Errorf("Value in field \"repo\" was too long") 6794 } 6795 6796 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 6797 - return err 6798 - } 6799 - if _, err := cw.WriteString(string("repo")); err != nil { 6800 return err 6801 } 6802 6803 - if len(t.Repo) > 1000000 { 6804 - return xerrors.Errorf("Value in field t.Repo was too long") 6805 - } 6806 6807 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 6808 - return err 6809 - } 6810 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 6811 - return err 6812 } 6813 6814 // t.LexiconTypeID (string) (string) ··· 6830 return err 6831 } 6832 6833 // t.Subject (string) (string) 6834 if len("subject") > 1000000 { 6835 return xerrors.Errorf("Value in field \"subject\" was too long") ··· 6923 case "repo": 6924 6925 { 6926 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 6927 if err != nil { 6928 return err 6929 } 6930 6931 - t.Repo = string(sval) 6932 } 6933 // t.LexiconTypeID (string) (string) 6934 case "$type": ··· 6941 6942 t.LexiconTypeID = string(sval) 6943 } 6944 // t.Subject (string) (string) 6945 case "subject": 6946 ··· 6981 } 6982 6983 cw := cbg.NewCborWriter(w) 6984 - fieldCount := 7 6985 6986 if t.Body == nil { 6987 fieldCount-- ··· 6992 } 6993 6994 if t.References == nil { 6995 fieldCount-- 6996 } 6997 ··· 7032 } 7033 7034 // t.Repo (string) (string) 7035 - if len("repo") > 1000000 { 7036 - return xerrors.Errorf("Value in field \"repo\" was too long") 7037 - } 7038 7039 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7040 - return err 7041 - } 7042 - if _, err := cw.WriteString(string("repo")); err != nil { 7043 - return err 7044 - } 7045 7046 - if len(t.Repo) > 1000000 { 7047 - return xerrors.Errorf("Value in field t.Repo was too long") 7048 - } 7049 7050 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 7051 - return err 7052 - } 7053 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 7054 - return err 7055 } 7056 7057 // t.LexiconTypeID (string) (string) ··· 7094 } 7095 if _, err := cw.WriteString(string(t.Title)); err != nil { 7096 return err 7097 } 7098 7099 // t.Mentions ([]string) (slice) ··· 7259 case "repo": 7260 7261 { 7262 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7263 if err != nil { 7264 return err 7265 } 7266 7267 - t.Repo = string(sval) 7268 } 7269 // t.LexiconTypeID (string) (string) 7270 case "$type": ··· 7287 } 7288 7289 t.Title = string(sval) 7290 } 7291 // t.Mentions ([]string) (slice) 7292 case "mentions": ··· 8890 } 8891 8892 cw := cbg.NewCborWriter(w) 8893 - fieldCount := 3 8894 8895 if t.Repo == nil { 8896 fieldCount-- 8897 } 8898 ··· 8977 if _, err := cw.WriteString(string(t.Branch)); err != nil { 8978 return err 8979 } 8980 return nil 8981 } 8982 ··· 9005 9006 n := extra 9007 9008 - nameBuf := make([]byte, 6) 9009 for i := uint64(0); i < n; i++ { 9010 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 9011 if err != nil { ··· 9064 9065 t.Branch = string(sval) 9066 } 9067 9068 default: 9069 // Field doesn't exist on this type, so ignore it ··· 9246 } 9247 9248 cw := cbg.NewCborWriter(w) 9249 9250 - if _, err := cw.Write([]byte{162}); err != nil { 9251 - return err 9252 } 9253 9254 - // t.Repo (string) (string) 9255 - if len("repo") > 1000000 { 9256 - return xerrors.Errorf("Value in field \"repo\" was too long") 9257 } 9258 9259 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 9260 - return err 9261 - } 9262 - if _, err := cw.WriteString(string("repo")); err != nil { 9263 return err 9264 } 9265 9266 - if len(t.Repo) > 1000000 { 9267 - return xerrors.Errorf("Value in field t.Repo was too long") 9268 - } 9269 9270 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 9271 - return err 9272 - } 9273 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 9274 - return err 9275 } 9276 9277 // t.Branch (string) (string) ··· 9296 if _, err := cw.WriteString(string(t.Branch)); err != nil { 9297 return err 9298 } 9299 return nil 9300 } 9301 ··· 9324 9325 n := extra 9326 9327 - nameBuf := make([]byte, 6) 9328 for i := uint64(0); i < n; i++ { 9329 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 9330 if err != nil { ··· 9344 case "repo": 9345 9346 { 9347 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 9348 if err != nil { 9349 return err 9350 } 9351 9352 - t.Repo = string(sval) 9353 } 9354 // t.Branch (string) (string) 9355 case "branch": ··· 9361 } 9362 9363 t.Branch = string(sval) 9364 } 9365 9366 default:
··· 26 } 27 28 cw := cbg.NewCborWriter(w) 29 + fieldCount := 10 30 31 if t.Avatar == nil { 32 fieldCount-- ··· 45 } 46 47 if t.PinnedRepositories == nil { 48 + fieldCount-- 49 + } 50 + 51 + if t.PinnedRepositoryDids == nil { 52 fieldCount-- 53 } 54 ··· 321 322 } 323 } 324 + 325 + // t.PinnedRepositoryDids ([]string) (slice) 326 + if t.PinnedRepositoryDids != nil { 327 + 328 + if len("pinnedRepositoryDids") > 1000000 { 329 + return xerrors.Errorf("Value in field \"pinnedRepositoryDids\" was too long") 330 + } 331 + 332 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedRepositoryDids"))); err != nil { 333 + return err 334 + } 335 + if _, err := cw.WriteString(string("pinnedRepositoryDids")); err != nil { 336 + return err 337 + } 338 + 339 + if len(t.PinnedRepositoryDids) > 8192 { 340 + return xerrors.Errorf("Slice value in field t.PinnedRepositoryDids was too long") 341 + } 342 + 343 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PinnedRepositoryDids))); err != nil { 344 + return err 345 + } 346 + for _, v := range t.PinnedRepositoryDids { 347 + if len(v) > 1000000 { 348 + return xerrors.Errorf("Value in field v was too long") 349 + } 350 + 351 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 352 + return err 353 + } 354 + if _, err := cw.WriteString(string(v)); err != nil { 355 + return err 356 + } 357 + 358 + } 359 + } 360 return nil 361 } 362 ··· 385 386 n := extra 387 388 + nameBuf := make([]byte, 20) 389 for i := uint64(0); i < n; i++ { 390 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 391 if err != nil { ··· 632 } 633 634 } 635 + } 636 + // t.PinnedRepositoryDids ([]string) (slice) 637 + case "pinnedRepositoryDids": 638 + 639 + maj, extra, err = cr.ReadHeader() 640 + if err != nil { 641 + return err 642 + } 643 + 644 + if extra > 8192 { 645 + return fmt.Errorf("t.PinnedRepositoryDids: array too large (%d)", extra) 646 + } 647 + 648 + if maj != cbg.MajArray { 649 + return fmt.Errorf("expected cbor array") 650 + } 651 + 652 + if extra > 0 { 653 + t.PinnedRepositoryDids = make([]string, extra) 654 + } 655 + 656 + for i := 0; i < int(extra); i++ { 657 + { 658 + var maj byte 659 + var extra uint64 660 + var err error 661 + _ = maj 662 + _ = extra 663 + _ = err 664 + 665 + { 666 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 667 + if err != nil { 668 + return err 669 + } 670 + 671 + t.PinnedRepositoryDids[i] = string(sval) 672 + } 673 + 674 + } 675 } 676 677 default: ··· 691 } 692 693 cw := cbg.NewCborWriter(w) 694 + fieldCount := 5 695 696 + if t.SubjectDid == nil { 697 + fieldCount-- 698 + } 699 + 700 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 701 return err 702 } 703 ··· 788 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 789 return err 790 } 791 + 792 + // t.SubjectDid (string) (string) 793 + if t.SubjectDid != nil { 794 + 795 + if len("subjectDid") > 1000000 { 796 + return xerrors.Errorf("Value in field \"subjectDid\" was too long") 797 + } 798 + 799 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subjectDid"))); err != nil { 800 + return err 801 + } 802 + if _, err := cw.WriteString(string("subjectDid")); err != nil { 803 + return err 804 + } 805 + 806 + if t.SubjectDid == nil { 807 + if _, err := cw.Write(cbg.CborNull); err != nil { 808 + return err 809 + } 810 + } else { 811 + if len(*t.SubjectDid) > 1000000 { 812 + return xerrors.Errorf("Value in field t.SubjectDid was too long") 813 + } 814 + 815 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SubjectDid))); err != nil { 816 + return err 817 + } 818 + if _, err := cw.WriteString(string(*t.SubjectDid)); err != nil { 819 + return err 820 + } 821 + } 822 + } 823 return nil 824 } 825 ··· 848 849 n := extra 850 851 + nameBuf := make([]byte, 10) 852 for i := uint64(0); i < n; i++ { 853 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 854 if err != nil { ··· 908 909 t.CreatedAt = string(sval) 910 } 911 + // t.SubjectDid (string) (string) 912 + case "subjectDid": 913 + 914 + { 915 + b, err := cr.ReadByte() 916 + if err != nil { 917 + return err 918 + } 919 + if b != cbg.CborNull[0] { 920 + if err := cr.UnreadByte(); err != nil { 921 + return err 922 + } 923 + 924 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 925 + if err != nil { 926 + return err 927 + } 928 + 929 + t.SubjectDid = (*string)(&sval) 930 + } 931 + } 932 933 default: 934 // Field doesn't exist on this type, so ignore it ··· 947 } 948 949 cw := cbg.NewCborWriter(w) 950 + fieldCount := 4 951 952 + if t.Subject == nil { 953 + fieldCount-- 954 + } 955 + 956 + if t.SubjectDid == nil { 957 + fieldCount-- 958 + } 959 + 960 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 961 return err 962 } 963 ··· 981 } 982 983 // t.Subject (string) (string) 984 + if t.Subject != nil { 985 986 + if len("subject") > 1000000 { 987 + return xerrors.Errorf("Value in field \"subject\" was too long") 988 + } 989 + 990 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 991 + return err 992 + } 993 + if _, err := cw.WriteString(string("subject")); err != nil { 994 + return err 995 + } 996 997 + if t.Subject == nil { 998 + if _, err := cw.Write(cbg.CborNull); err != nil { 999 + return err 1000 + } 1001 + } else { 1002 + if len(*t.Subject) > 1000000 { 1003 + return xerrors.Errorf("Value in field t.Subject was too long") 1004 + } 1005 1006 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Subject))); err != nil { 1007 + return err 1008 + } 1009 + if _, err := cw.WriteString(string(*t.Subject)); err != nil { 1010 + return err 1011 + } 1012 + } 1013 } 1014 1015 // t.CreatedAt (string) (string) ··· 1034 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 1035 return err 1036 } 1037 + 1038 + // t.SubjectDid (string) (string) 1039 + if t.SubjectDid != nil { 1040 + 1041 + if len("subjectDid") > 1000000 { 1042 + return xerrors.Errorf("Value in field \"subjectDid\" was too long") 1043 + } 1044 + 1045 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subjectDid"))); err != nil { 1046 + return err 1047 + } 1048 + if _, err := cw.WriteString(string("subjectDid")); err != nil { 1049 + return err 1050 + } 1051 + 1052 + if t.SubjectDid == nil { 1053 + if _, err := cw.Write(cbg.CborNull); err != nil { 1054 + return err 1055 + } 1056 + } else { 1057 + if len(*t.SubjectDid) > 1000000 { 1058 + return xerrors.Errorf("Value in field t.SubjectDid was too long") 1059 + } 1060 + 1061 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SubjectDid))); err != nil { 1062 + return err 1063 + } 1064 + if _, err := cw.WriteString(string(*t.SubjectDid)); err != nil { 1065 + return err 1066 + } 1067 + } 1068 + } 1069 return nil 1070 } 1071 ··· 1094 1095 n := extra 1096 1097 + nameBuf := make([]byte, 10) 1098 for i := uint64(0); i < n; i++ { 1099 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1100 if err != nil { ··· 1125 case "subject": 1126 1127 { 1128 + b, err := cr.ReadByte() 1129 if err != nil { 1130 return err 1131 } 1132 + if b != cbg.CborNull[0] { 1133 + if err := cr.UnreadByte(); err != nil { 1134 + return err 1135 + } 1136 1137 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1138 + if err != nil { 1139 + return err 1140 + } 1141 + 1142 + t.Subject = (*string)(&sval) 1143 + } 1144 } 1145 // t.CreatedAt (string) (string) 1146 case "createdAt": ··· 1153 1154 t.CreatedAt = string(sval) 1155 } 1156 + // t.SubjectDid (string) (string) 1157 + case "subjectDid": 1158 + 1159 + { 1160 + b, err := cr.ReadByte() 1161 + if err != nil { 1162 + return err 1163 + } 1164 + if b != cbg.CborNull[0] { 1165 + if err := cr.UnreadByte(); err != nil { 1166 + return err 1167 + } 1168 + 1169 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1170 + if err != nil { 1171 + return err 1172 + } 1173 + 1174 + t.SubjectDid = (*string)(&sval) 1175 + } 1176 + } 1177 1178 default: 1179 // Field doesn't exist on this type, so ignore it ··· 1193 1194 cw := cbg.NewCborWriter(w) 1195 1196 + if _, err := cw.Write([]byte{169}); err != nil { 1197 return err 1198 } 1199 ··· 1324 return err 1325 } 1326 1327 + // t.OwnerDid (string) (string) 1328 + if len("ownerDid") > 1000000 { 1329 + return xerrors.Errorf("Value in field \"ownerDid\" was too long") 1330 + } 1331 + 1332 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("ownerDid"))); err != nil { 1333 + return err 1334 + } 1335 + if _, err := cw.WriteString(string("ownerDid")); err != nil { 1336 + return err 1337 + } 1338 + 1339 + if len(t.OwnerDid) > 1000000 { 1340 + return xerrors.Errorf("Value in field t.OwnerDid was too long") 1341 + } 1342 + 1343 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.OwnerDid))); err != nil { 1344 + return err 1345 + } 1346 + if _, err := cw.WriteString(string(t.OwnerDid)); err != nil { 1347 + return err 1348 + } 1349 + 1350 // t.RepoName (string) (string) 1351 if len("repoName") > 1000000 { 1352 return xerrors.Errorf("Value in field \"repoName\" was too long") ··· 1510 } 1511 1512 t.RepoDid = string(sval) 1513 + } 1514 + // t.OwnerDid (string) (string) 1515 + case "ownerDid": 1516 + 1517 + { 1518 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1519 + if err != nil { 1520 + return err 1521 + } 1522 + 1523 + t.OwnerDid = string(sval) 1524 } 1525 // t.RepoName (string) (string) 1526 case "repoName": ··· 3437 } 3438 3439 cw := cbg.NewCborWriter(w) 3440 + fieldCount := 6 3441 3442 + if t.SubjectDid == nil { 3443 + fieldCount-- 3444 + } 3445 + 3446 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 3447 return err 3448 } 3449 ··· 3541 return err 3542 } 3543 3544 + // t.SubjectDid (string) (string) 3545 + if t.SubjectDid != nil { 3546 + 3547 + if len("subjectDid") > 1000000 { 3548 + return xerrors.Errorf("Value in field \"subjectDid\" was too long") 3549 + } 3550 + 3551 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subjectDid"))); err != nil { 3552 + return err 3553 + } 3554 + if _, err := cw.WriteString(string("subjectDid")); err != nil { 3555 + return err 3556 + } 3557 + 3558 + if t.SubjectDid == nil { 3559 + if _, err := cw.Write(cbg.CborNull); err != nil { 3560 + return err 3561 + } 3562 + } else { 3563 + if len(*t.SubjectDid) > 1000000 { 3564 + return xerrors.Errorf("Value in field t.SubjectDid was too long") 3565 + } 3566 + 3567 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SubjectDid))); err != nil { 3568 + return err 3569 + } 3570 + if _, err := cw.WriteString(string(*t.SubjectDid)); err != nil { 3571 + return err 3572 + } 3573 + } 3574 + } 3575 + 3576 // t.PerformedAt (string) (string) 3577 if len("performedAt") > 1000000 { 3578 return xerrors.Errorf("Value in field \"performedAt\" was too long") ··· 3758 } 3759 3760 t.Subject = string(sval) 3761 + } 3762 + // t.SubjectDid (string) (string) 3763 + case "subjectDid": 3764 + 3765 + { 3766 + b, err := cr.ReadByte() 3767 + if err != nil { 3768 + return err 3769 + } 3770 + if b != cbg.CborNull[0] { 3771 + if err := cr.UnreadByte(); err != nil { 3772 + return err 3773 + } 3774 + 3775 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3776 + if err != nil { 3777 + return err 3778 + } 3779 + 3780 + t.SubjectDid = (*string)(&sval) 3781 + } 3782 } 3783 // t.PerformedAt (string) (string) 3784 case "performedAt": ··· 5614 5615 cw := cbg.NewCborWriter(w) 5616 5617 + if _, err := cw.Write([]byte{165}); err != nil { 5618 return err 5619 } 5620 ··· 5687 return err 5688 } 5689 5690 + // t.RepoDid (string) (string) 5691 + if len("repoDid") > 1000000 { 5692 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 5693 + } 5694 + 5695 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 5696 + return err 5697 + } 5698 + if _, err := cw.WriteString(string("repoDid")); err != nil { 5699 + return err 5700 + } 5701 + 5702 + if len(t.RepoDid) > 1000000 { 5703 + return xerrors.Errorf("Value in field t.RepoDid was too long") 5704 + } 5705 + 5706 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.RepoDid))); err != nil { 5707 + return err 5708 + } 5709 + if _, err := cw.WriteString(string(t.RepoDid)); err != nil { 5710 + return err 5711 + } 5712 + 5713 // t.DefaultBranch (string) (string) 5714 if len("defaultBranch") > 1000000 { 5715 return xerrors.Errorf("Value in field \"defaultBranch\" was too long") ··· 5808 } 5809 5810 t.Repo = string(sval) 5811 + } 5812 + // t.RepoDid (string) (string) 5813 + case "repoDid": 5814 + 5815 + { 5816 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 5817 + if err != nil { 5818 + return err 5819 + } 5820 + 5821 + t.RepoDid = string(sval) 5822 } 5823 // t.DefaultBranch (string) (string) 5824 case "defaultBranch": ··· 6251 } 6252 6253 cw := cbg.NewCborWriter(w) 6254 + fieldCount := 11 6255 6256 if t.Description == nil { 6257 fieldCount-- 6258 } 6259 6260 if t.Labels == nil { 6261 + fieldCount-- 6262 + } 6263 + 6264 + if t.RepoDid == nil { 6265 fieldCount-- 6266 } 6267 ··· 6451 return err 6452 } 6453 6454 + } 6455 + } 6456 + 6457 + // t.RepoDid (string) (string) 6458 + if t.RepoDid != nil { 6459 + 6460 + if len("repoDid") > 1000000 { 6461 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 6462 + } 6463 + 6464 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 6465 + return err 6466 + } 6467 + if _, err := cw.WriteString(string("repoDid")); err != nil { 6468 + return err 6469 + } 6470 + 6471 + if t.RepoDid == nil { 6472 + if _, err := cw.Write(cbg.CborNull); err != nil { 6473 + return err 6474 + } 6475 + } else { 6476 + if len(*t.RepoDid) > 1000000 { 6477 + return xerrors.Errorf("Value in field t.RepoDid was too long") 6478 + } 6479 + 6480 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.RepoDid))); err != nil { 6481 + return err 6482 + } 6483 + if _, err := cw.WriteString(string(*t.RepoDid)); err != nil { 6484 + return err 6485 + } 6486 } 6487 } 6488 ··· 6782 6783 } 6784 } 6785 + // t.RepoDid (string) (string) 6786 + case "repoDid": 6787 + 6788 + { 6789 + b, err := cr.ReadByte() 6790 + if err != nil { 6791 + return err 6792 + } 6793 + if b != cbg.CborNull[0] { 6794 + if err := cr.UnreadByte(); err != nil { 6795 + return err 6796 + } 6797 + 6798 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 6799 + if err != nil { 6800 + return err 6801 + } 6802 + 6803 + t.RepoDid = (*string)(&sval) 6804 + } 6805 + } 6806 // t.Spindle (string) (string) 6807 case "spindle": 6808 ··· 6895 } 6896 6897 cw := cbg.NewCborWriter(w) 6898 + fieldCount := 7 6899 + 6900 + if t.Repo == nil { 6901 + fieldCount-- 6902 + } 6903 + 6904 + if t.RepoDid == nil { 6905 + fieldCount-- 6906 + } 6907 6908 if t.Tag == nil { 6909 fieldCount-- ··· 6965 } 6966 6967 // t.Repo (string) (string) 6968 + if t.Repo != nil { 6969 6970 + if len("repo") > 1000000 { 6971 + return xerrors.Errorf("Value in field \"repo\" was too long") 6972 + } 6973 + 6974 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 6975 + return err 6976 + } 6977 + if _, err := cw.WriteString(string("repo")); err != nil { 6978 + return err 6979 + } 6980 6981 + if t.Repo == nil { 6982 + if _, err := cw.Write(cbg.CborNull); err != nil { 6983 + return err 6984 + } 6985 + } else { 6986 + if len(*t.Repo) > 1000000 { 6987 + return xerrors.Errorf("Value in field t.Repo was too long") 6988 + } 6989 6990 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 6991 + return err 6992 + } 6993 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 6994 + return err 6995 + } 6996 + } 6997 } 6998 6999 // t.LexiconTypeID (string) (string) ··· 7015 return err 7016 } 7017 7018 + // t.RepoDid (string) (string) 7019 + if t.RepoDid != nil { 7020 + 7021 + if len("repoDid") > 1000000 { 7022 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 7023 + } 7024 + 7025 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 7026 + return err 7027 + } 7028 + if _, err := cw.WriteString(string("repoDid")); err != nil { 7029 + return err 7030 + } 7031 + 7032 + if t.RepoDid == nil { 7033 + if _, err := cw.Write(cbg.CborNull); err != nil { 7034 + return err 7035 + } 7036 + } else { 7037 + if len(*t.RepoDid) > 1000000 { 7038 + return xerrors.Errorf("Value in field t.RepoDid was too long") 7039 + } 7040 + 7041 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.RepoDid))); err != nil { 7042 + return err 7043 + } 7044 + if _, err := cw.WriteString(string(*t.RepoDid)); err != nil { 7045 + return err 7046 + } 7047 + } 7048 + } 7049 + 7050 // t.Artifact (util.LexBlob) (struct) 7051 if len("artifact") > 1000000 { 7052 return xerrors.Errorf("Value in field \"artifact\" was too long") ··· 7167 case "repo": 7168 7169 { 7170 + b, err := cr.ReadByte() 7171 if err != nil { 7172 return err 7173 } 7174 + if b != cbg.CborNull[0] { 7175 + if err := cr.UnreadByte(); err != nil { 7176 + return err 7177 + } 7178 7179 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7180 + if err != nil { 7181 + return err 7182 + } 7183 + 7184 + t.Repo = (*string)(&sval) 7185 + } 7186 } 7187 // t.LexiconTypeID (string) (string) 7188 case "$type": ··· 7194 } 7195 7196 t.LexiconTypeID = string(sval) 7197 + } 7198 + // t.RepoDid (string) (string) 7199 + case "repoDid": 7200 + 7201 + { 7202 + b, err := cr.ReadByte() 7203 + if err != nil { 7204 + return err 7205 + } 7206 + if b != cbg.CborNull[0] { 7207 + if err := cr.UnreadByte(); err != nil { 7208 + return err 7209 + } 7210 + 7211 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7212 + if err != nil { 7213 + return err 7214 + } 7215 + 7216 + t.RepoDid = (*string)(&sval) 7217 + } 7218 } 7219 // t.Artifact (util.LexBlob) (struct) 7220 case "artifact": ··· 7265 } 7266 7267 cw := cbg.NewCborWriter(w) 7268 + fieldCount := 5 7269 7270 + if t.Repo == nil { 7271 + fieldCount-- 7272 } 7273 7274 + if t.RepoDid == nil { 7275 + fieldCount-- 7276 } 7277 7278 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 7279 return err 7280 } 7281 7282 + // t.Repo (string) (string) 7283 + if t.Repo != nil { 7284 7285 + if len("repo") > 1000000 { 7286 + return xerrors.Errorf("Value in field \"repo\" was too long") 7287 + } 7288 + 7289 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7290 + return err 7291 + } 7292 + if _, err := cw.WriteString(string("repo")); err != nil { 7293 + return err 7294 + } 7295 + 7296 + if t.Repo == nil { 7297 + if _, err := cw.Write(cbg.CborNull); err != nil { 7298 + return err 7299 + } 7300 + } else { 7301 + if len(*t.Repo) > 1000000 { 7302 + return xerrors.Errorf("Value in field t.Repo was too long") 7303 + } 7304 + 7305 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 7306 + return err 7307 + } 7308 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 7309 + return err 7310 + } 7311 + } 7312 } 7313 7314 // t.LexiconTypeID (string) (string) ··· 7330 return err 7331 } 7332 7333 + // t.RepoDid (string) (string) 7334 + if t.RepoDid != nil { 7335 + 7336 + if len("repoDid") > 1000000 { 7337 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 7338 + } 7339 + 7340 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 7341 + return err 7342 + } 7343 + if _, err := cw.WriteString(string("repoDid")); err != nil { 7344 + return err 7345 + } 7346 + 7347 + if t.RepoDid == nil { 7348 + if _, err := cw.Write(cbg.CborNull); err != nil { 7349 + return err 7350 + } 7351 + } else { 7352 + if len(*t.RepoDid) > 1000000 { 7353 + return xerrors.Errorf("Value in field t.RepoDid was too long") 7354 + } 7355 + 7356 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.RepoDid))); err != nil { 7357 + return err 7358 + } 7359 + if _, err := cw.WriteString(string(*t.RepoDid)); err != nil { 7360 + return err 7361 + } 7362 + } 7363 + } 7364 + 7365 // t.Subject (string) (string) 7366 if len("subject") > 1000000 { 7367 return xerrors.Errorf("Value in field \"subject\" was too long") ··· 7455 case "repo": 7456 7457 { 7458 + b, err := cr.ReadByte() 7459 if err != nil { 7460 return err 7461 } 7462 + if b != cbg.CborNull[0] { 7463 + if err := cr.UnreadByte(); err != nil { 7464 + return err 7465 + } 7466 7467 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7468 + if err != nil { 7469 + return err 7470 + } 7471 + 7472 + t.Repo = (*string)(&sval) 7473 + } 7474 } 7475 // t.LexiconTypeID (string) (string) 7476 case "$type": ··· 7483 7484 t.LexiconTypeID = string(sval) 7485 } 7486 + // t.RepoDid (string) (string) 7487 + case "repoDid": 7488 + 7489 + { 7490 + b, err := cr.ReadByte() 7491 + if err != nil { 7492 + return err 7493 + } 7494 + if b != cbg.CborNull[0] { 7495 + if err := cr.UnreadByte(); err != nil { 7496 + return err 7497 + } 7498 + 7499 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7500 + if err != nil { 7501 + return err 7502 + } 7503 + 7504 + t.RepoDid = (*string)(&sval) 7505 + } 7506 + } 7507 // t.Subject (string) (string) 7508 case "subject": 7509 ··· 7544 } 7545 7546 cw := cbg.NewCborWriter(w) 7547 + fieldCount := 8 7548 7549 if t.Body == nil { 7550 fieldCount-- ··· 7555 } 7556 7557 if t.References == nil { 7558 + fieldCount-- 7559 + } 7560 + 7561 + if t.Repo == nil { 7562 + fieldCount-- 7563 + } 7564 + 7565 + if t.RepoDid == nil { 7566 fieldCount-- 7567 } 7568 ··· 7603 } 7604 7605 // t.Repo (string) (string) 7606 + if t.Repo != nil { 7607 7608 + if len("repo") > 1000000 { 7609 + return xerrors.Errorf("Value in field \"repo\" was too long") 7610 + } 7611 + 7612 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7613 + return err 7614 + } 7615 + if _, err := cw.WriteString(string("repo")); err != nil { 7616 + return err 7617 + } 7618 7619 + if t.Repo == nil { 7620 + if _, err := cw.Write(cbg.CborNull); err != nil { 7621 + return err 7622 + } 7623 + } else { 7624 + if len(*t.Repo) > 1000000 { 7625 + return xerrors.Errorf("Value in field t.Repo was too long") 7626 + } 7627 7628 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 7629 + return err 7630 + } 7631 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 7632 + return err 7633 + } 7634 + } 7635 } 7636 7637 // t.LexiconTypeID (string) (string) ··· 7674 } 7675 if _, err := cw.WriteString(string(t.Title)); err != nil { 7676 return err 7677 + } 7678 + 7679 + // t.RepoDid (string) (string) 7680 + if t.RepoDid != nil { 7681 + 7682 + if len("repoDid") > 1000000 { 7683 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 7684 + } 7685 + 7686 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 7687 + return err 7688 + } 7689 + if _, err := cw.WriteString(string("repoDid")); err != nil { 7690 + return err 7691 + } 7692 + 7693 + if t.RepoDid == nil { 7694 + if _, err := cw.Write(cbg.CborNull); err != nil { 7695 + return err 7696 + } 7697 + } else { 7698 + if len(*t.RepoDid) > 1000000 { 7699 + return xerrors.Errorf("Value in field t.RepoDid was too long") 7700 + } 7701 + 7702 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.RepoDid))); err != nil { 7703 + return err 7704 + } 7705 + if _, err := cw.WriteString(string(*t.RepoDid)); err != nil { 7706 + return err 7707 + } 7708 + } 7709 } 7710 7711 // t.Mentions ([]string) (slice) ··· 7871 case "repo": 7872 7873 { 7874 + b, err := cr.ReadByte() 7875 if err != nil { 7876 return err 7877 } 7878 + if b != cbg.CborNull[0] { 7879 + if err := cr.UnreadByte(); err != nil { 7880 + return err 7881 + } 7882 7883 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7884 + if err != nil { 7885 + return err 7886 + } 7887 + 7888 + t.Repo = (*string)(&sval) 7889 + } 7890 } 7891 // t.LexiconTypeID (string) (string) 7892 case "$type": ··· 7909 } 7910 7911 t.Title = string(sval) 7912 + } 7913 + // t.RepoDid (string) (string) 7914 + case "repoDid": 7915 + 7916 + { 7917 + b, err := cr.ReadByte() 7918 + if err != nil { 7919 + return err 7920 + } 7921 + if b != cbg.CborNull[0] { 7922 + if err := cr.UnreadByte(); err != nil { 7923 + return err 7924 + } 7925 + 7926 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7927 + if err != nil { 7928 + return err 7929 + } 7930 + 7931 + t.RepoDid = (*string)(&sval) 7932 + } 7933 } 7934 // t.Mentions ([]string) (slice) 7935 case "mentions": ··· 9533 } 9534 9535 cw := cbg.NewCborWriter(w) 9536 + fieldCount := 4 9537 9538 if t.Repo == nil { 9539 + fieldCount-- 9540 + } 9541 + 9542 + if t.RepoDid == nil { 9543 fieldCount-- 9544 } 9545 ··· 9624 if _, err := cw.WriteString(string(t.Branch)); err != nil { 9625 return err 9626 } 9627 + 9628 + // t.RepoDid (string) (string) 9629 + if t.RepoDid != nil { 9630 + 9631 + if len("repoDid") > 1000000 { 9632 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 9633 + } 9634 + 9635 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 9636 + return err 9637 + } 9638 + if _, err := cw.WriteString(string("repoDid")); err != nil { 9639 + return err 9640 + } 9641 + 9642 + if t.RepoDid == nil { 9643 + if _, err := cw.Write(cbg.CborNull); err != nil { 9644 + return err 9645 + } 9646 + } else { 9647 + if len(*t.RepoDid) > 1000000 { 9648 + return xerrors.Errorf("Value in field t.RepoDid was too long") 9649 + } 9650 + 9651 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.RepoDid))); err != nil { 9652 + return err 9653 + } 9654 + if _, err := cw.WriteString(string(*t.RepoDid)); err != nil { 9655 + return err 9656 + } 9657 + } 9658 + } 9659 return nil 9660 } 9661 ··· 9684 9685 n := extra 9686 9687 + nameBuf := make([]byte, 7) 9688 for i := uint64(0); i < n; i++ { 9689 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 9690 if err != nil { ··· 9743 9744 t.Branch = string(sval) 9745 } 9746 + // t.RepoDid (string) (string) 9747 + case "repoDid": 9748 + 9749 + { 9750 + b, err := cr.ReadByte() 9751 + if err != nil { 9752 + return err 9753 + } 9754 + if b != cbg.CborNull[0] { 9755 + if err := cr.UnreadByte(); err != nil { 9756 + return err 9757 + } 9758 + 9759 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 9760 + if err != nil { 9761 + return err 9762 + } 9763 + 9764 + t.RepoDid = (*string)(&sval) 9765 + } 9766 + } 9767 9768 default: 9769 // Field doesn't exist on this type, so ignore it ··· 9946 } 9947 9948 cw := cbg.NewCborWriter(w) 9949 + fieldCount := 3 9950 9951 + if t.Repo == nil { 9952 + fieldCount-- 9953 } 9954 9955 + if t.RepoDid == nil { 9956 + fieldCount-- 9957 } 9958 9959 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 9960 return err 9961 } 9962 9963 + // t.Repo (string) (string) 9964 + if t.Repo != nil { 9965 + 9966 + if len("repo") > 1000000 { 9967 + return xerrors.Errorf("Value in field \"repo\" was too long") 9968 + } 9969 9970 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 9971 + return err 9972 + } 9973 + if _, err := cw.WriteString(string("repo")); err != nil { 9974 + return err 9975 + } 9976 + 9977 + if t.Repo == nil { 9978 + if _, err := cw.Write(cbg.CborNull); err != nil { 9979 + return err 9980 + } 9981 + } else { 9982 + if len(*t.Repo) > 1000000 { 9983 + return xerrors.Errorf("Value in field t.Repo was too long") 9984 + } 9985 + 9986 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 9987 + return err 9988 + } 9989 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 9990 + return err 9991 + } 9992 + } 9993 } 9994 9995 // t.Branch (string) (string) ··· 10014 if _, err := cw.WriteString(string(t.Branch)); err != nil { 10015 return err 10016 } 10017 + 10018 + // t.RepoDid (string) (string) 10019 + if t.RepoDid != nil { 10020 + 10021 + if len("repoDid") > 1000000 { 10022 + return xerrors.Errorf("Value in field \"repoDid\" was too long") 10023 + } 10024 + 10025 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repoDid"))); err != nil { 10026 + return err 10027 + } 10028 + if _, err := cw.WriteString(string("repoDid")); err != nil { 10029 + return err 10030 + } 10031 + 10032 + if t.RepoDid == nil { 10033 + if _, err := cw.Write(cbg.CborNull); err != nil { 10034 + return err 10035 + } 10036 + } else { 10037 + if len(*t.RepoDid) > 1000000 { 10038 + return xerrors.Errorf("Value in field t.RepoDid was too long") 10039 + } 10040 + 10041 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.RepoDid))); err != nil { 10042 + return err 10043 + } 10044 + if _, err := cw.WriteString(string(*t.RepoDid)); err != nil { 10045 + return err 10046 + } 10047 + } 10048 + } 10049 return nil 10050 } 10051 ··· 10074 10075 n := extra 10076 10077 + nameBuf := make([]byte, 7) 10078 for i := uint64(0); i < n; i++ { 10079 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 10080 if err != nil { ··· 10094 case "repo": 10095 10096 { 10097 + b, err := cr.ReadByte() 10098 if err != nil { 10099 return err 10100 } 10101 + if b != cbg.CborNull[0] { 10102 + if err := cr.UnreadByte(); err != nil { 10103 + return err 10104 + } 10105 10106 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 10107 + if err != nil { 10108 + return err 10109 + } 10110 + 10111 + t.Repo = (*string)(&sval) 10112 + } 10113 } 10114 // t.Branch (string) (string) 10115 case "branch": ··· 10121 } 10122 10123 t.Branch = string(sval) 10124 + } 10125 + // t.RepoDid (string) (string) 10126 + case "repoDid": 10127 + 10128 + { 10129 + b, err := cr.ReadByte() 10130 + if err != nil { 10131 + return err 10132 + } 10133 + if b != cbg.CborNull[0] { 10134 + if err := cr.UnreadByte(); err != nil { 10135 + return err 10136 + } 10137 + 10138 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 10139 + if err != nil { 10140 + return err 10141 + } 10142 + 10143 + t.RepoDid = (*string)(&sval) 10144 + } 10145 } 10146 10147 default:
+5 -4
api/tangled/feedreaction.go
··· 17 } // 18 // RECORDTYPE: FeedReaction 19 type FeedReaction struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.feed.reaction" cborgen:"$type,const=sh.tangled.feed.reaction"` 21 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 - Reaction string `json:"reaction" cborgen:"reaction"` 23 - Subject string `json:"subject" cborgen:"subject"` 24 }
··· 17 } // 18 // RECORDTYPE: FeedReaction 19 type FeedReaction struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.feed.reaction" cborgen:"$type,const=sh.tangled.feed.reaction"` 21 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 + Reaction string `json:"reaction" cborgen:"reaction"` 23 + Subject string `json:"subject" cborgen:"subject"` 24 + SubjectDid *string `json:"subjectDid,omitempty" cborgen:"subjectDid,omitempty"` 25 }
+4 -3
api/tangled/feedstar.go
··· 17 } // 18 // RECORDTYPE: FeedStar 19 type FeedStar struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.feed.star" cborgen:"$type,const=sh.tangled.feed.star"` 21 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 - Subject string `json:"subject" cborgen:"subject"` 23 }
··· 17 } // 18 // RECORDTYPE: FeedStar 19 type FeedStar struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.feed.star" cborgen:"$type,const=sh.tangled.feed.star"` 21 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 + Subject *string `json:"subject,omitempty" cborgen:"subject,omitempty"` 23 + SubjectDid *string `json:"subjectDid,omitempty" cborgen:"subjectDid,omitempty"` 24 }
+3 -1
api/tangled/gitrefUpdate.go
··· 25 NewSha string `json:"newSha" cborgen:"newSha"` 26 // oldSha: old SHA of this ref 27 OldSha string `json:"oldSha" cborgen:"oldSha"` 28 // ref: Ref being updated 29 Ref string `json:"ref" cborgen:"ref"` 30 - // repoDid: did of the owner of the repo 31 RepoDid string `json:"repoDid" cborgen:"repoDid"` 32 // repoName: name of the repo 33 RepoName string `json:"repoName" cborgen:"repoName"`
··· 25 NewSha string `json:"newSha" cborgen:"newSha"` 26 // oldSha: old SHA of this ref 27 OldSha string `json:"oldSha" cborgen:"oldSha"` 28 + // ownerDid: did of the owner of the repo 29 + OwnerDid string `json:"ownerDid" cborgen:"ownerDid"` 30 // ref: Ref being updated 31 Ref string `json:"ref" cborgen:"ref"` 32 + // repoDid: DID of the repo itself 33 RepoDid string `json:"repoDid" cborgen:"repoDid"` 34 // repoName: name of the repo 35 RepoName string `json:"repoName" cborgen:"repoName"`
+2 -1
api/tangled/labelop.go
··· 22 Delete []*LabelOp_Operand `json:"delete" cborgen:"delete"` 23 PerformedAt string `json:"performedAt" cborgen:"performedAt"` 24 // subject: The subject (task, pull or discussion) of this label. Appviews may apply a `scope` check and refuse this op. 25 - Subject string `json:"subject" cborgen:"subject"` 26 } 27 28 // LabelOp_Operand is a "operand" in the sh.tangled.label.op schema.
··· 22 Delete []*LabelOp_Operand `json:"delete" cborgen:"delete"` 23 PerformedAt string `json:"performedAt" cborgen:"performedAt"` 24 // subject: The subject (task, pull or discussion) of this label. Appviews may apply a `scope` check and refuse this op. 25 + Subject string `json:"subject" cborgen:"subject"` 26 + SubjectDid *string `json:"subjectDid,omitempty" cborgen:"subjectDid,omitempty"` 27 } 28 29 // LabelOp_Operand is a "operand" in the sh.tangled.label.op schema.
+2 -1
api/tangled/repoartifact.go
··· 25 // name: name of the artifact 26 Name string `json:"name" cborgen:"name"` 27 // repo: repo that this artifact is being uploaded to 28 - Repo string `json:"repo" cborgen:"repo"` 29 // tag: hash of the tag object that this artifact is attached to (only annotated tags are supported) 30 Tag util.LexBytes `json:"tag,omitempty" cborgen:"tag,omitempty"` 31 }
··· 25 // name: name of the artifact 26 Name string `json:"name" cborgen:"name"` 27 // repo: repo that this artifact is being uploaded to 28 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 29 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 30 // tag: hash of the tag object that this artifact is attached to (only annotated tags are supported) 31 Tag util.LexBytes `json:"tag,omitempty" cborgen:"tag,omitempty"` 32 }
+3 -2
api/tangled/repocollaborator.go
··· 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.collaborator" cborgen:"$type,const=sh.tangled.repo.collaborator"` 21 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 // repo: repo to add this user to 23 - Repo string `json:"repo" cborgen:"repo"` 24 - Subject string `json:"subject" cborgen:"subject"` 25 }
··· 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.collaborator" cborgen:"$type,const=sh.tangled.repo.collaborator"` 21 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 // repo: repo to add this user to 23 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 24 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 25 + Subject string `json:"subject" cborgen:"subject"` 26 }
+14 -4
api/tangled/repocreate.go
··· 18 type RepoCreate_Input struct { 19 // defaultBranch: Default branch to push to 20 DefaultBranch *string `json:"defaultBranch,omitempty" cborgen:"defaultBranch,omitempty"` 21 // rkey: Rkey of the repository record 22 Rkey string `json:"rkey" cborgen:"rkey"` 23 // source: A source URL to clone from, populate this when forking or importing a repository. 24 Source *string `json:"source,omitempty" cborgen:"source,omitempty"` 25 } 26 27 // RepoCreate calls the XRPC method "sh.tangled.repo.create". 28 - func RepoCreate(ctx context.Context, c util.LexClient, input *RepoCreate_Input) error { 29 - if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.repo.create", nil, input, nil); err != nil { 30 - return err 31 } 32 33 - return nil 34 }
··· 18 type RepoCreate_Input struct { 19 // defaultBranch: Default branch to push to 20 DefaultBranch *string `json:"defaultBranch,omitempty" cborgen:"defaultBranch,omitempty"` 21 + // name: Name of the repository 22 + Name string `json:"name" cborgen:"name"` 23 + // repoDid: Optional user-provided did:web to use as the repo identity instead of minting a did:plc. 24 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 25 // rkey: Rkey of the repository record 26 Rkey string `json:"rkey" cborgen:"rkey"` 27 // source: A source URL to clone from, populate this when forking or importing a repository. 28 Source *string `json:"source,omitempty" cborgen:"source,omitempty"` 29 + } 30 + 31 + // RepoCreate_Output is the output of a sh.tangled.repo.create call. 32 + type RepoCreate_Output struct { 33 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 34 } 35 36 // RepoCreate calls the XRPC method "sh.tangled.repo.create". 37 + func RepoCreate(ctx context.Context, c util.LexClient, input *RepoCreate_Input) (*RepoCreate_Output, error) { 38 + var out RepoCreate_Output 39 + if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.repo.create", nil, input, &out); err != nil { 40 + return nil, err 41 } 42 43 + return &out, nil 44 }
+2 -1
api/tangled/repoissue.go
··· 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 References []string `json:"references,omitempty" cborgen:"references,omitempty"` 25 - Repo string `json:"repo" cborgen:"repo"` 26 Title string `json:"title" cborgen:"title"` 27 }
··· 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 References []string `json:"references,omitempty" cborgen:"references,omitempty"` 25 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 26 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 27 Title string `json:"title" cborgen:"title"` 28 }
+7 -5
api/tangled/repopull.go
··· 33 34 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema. 35 type RepoPull_Source struct { 36 - Branch string `json:"branch" cborgen:"branch"` 37 - Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 38 - Sha string `json:"sha" cborgen:"sha"` 39 } 40 41 // RepoPull_Target is a "target" in the sh.tangled.repo.pull schema. 42 type RepoPull_Target struct { 43 - Branch string `json:"branch" cborgen:"branch"` 44 - Repo string `json:"repo" cborgen:"repo"` 45 }
··· 33 34 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema. 35 type RepoPull_Source struct { 36 + Branch string `json:"branch" cborgen:"branch"` 37 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 38 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 39 + Sha string `json:"sha" cborgen:"sha"` 40 } 41 42 // RepoPull_Target is a "target" in the sh.tangled.repo.pull schema. 43 type RepoPull_Target struct { 44 + Branch string `json:"branch" cborgen:"branch"` 45 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 46 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 47 }
+2
api/tangled/tangledpipeline.go
··· 70 Did string `json:"did" cborgen:"did"` 71 Knot string `json:"knot" cborgen:"knot"` 72 Repo string `json:"repo" cborgen:"repo"` 73 } 74 75 // Pipeline_Workflow is a "workflow" in the sh.tangled.pipeline schema.
··· 70 Did string `json:"did" cborgen:"did"` 71 Knot string `json:"knot" cborgen:"knot"` 72 Repo string `json:"repo" cborgen:"repo"` 73 + // repoDid: DID of the repo itself 74 + RepoDid string `json:"repoDid" cborgen:"repoDid"` 75 } 76 77 // Pipeline_Workflow is a "workflow" in the sh.tangled.pipeline schema.
+2
api/tangled/tangledrepo.go
··· 26 Labels []string `json:"labels,omitempty" cborgen:"labels,omitempty"` 27 // name: name of the repo 28 Name string `json:"name" cborgen:"name"` 29 // source: source of the repo 30 Source *string `json:"source,omitempty" cborgen:"source,omitempty"` 31 // spindle: CI runner to send jobs to and receive results from
··· 26 Labels []string `json:"labels,omitempty" cborgen:"labels,omitempty"` 27 // name: name of the repo 28 Name string `json:"name" cborgen:"name"` 29 + // repoDid: DID of the repo itself, if assigned 30 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 31 // source: source of the repo 32 Source *string `json:"source,omitempty" cborgen:"source,omitempty"` 33 // spindle: CI runner to send jobs to and receive results from
+14 -1
appview/db/artifact.go
··· 1 package db 2 3 import ( 4 "fmt" 5 "strings" 6 "time" ··· 12 ) 13 14 func AddArtifact(e Execer, artifact models.Artifact) error { 15 _, err := e.Exec( 16 `insert or ignore into artifacts ( 17 did, 18 rkey, 19 repo_at, 20 tag, 21 created, 22 blob_cid, ··· 24 size, 25 mimetype 26 ) 27 - values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 28 artifact.Did, 29 artifact.Rkey, 30 artifact.RepoAt, 31 artifact.Tag[:], 32 artifact.CreatedAt.Format(time.RFC3339), 33 artifact.BlobCid.String(), ··· 57 did, 58 rkey, 59 repo_at, 60 tag, 61 created, 62 blob_cid, ··· 78 var createdAt string 79 var tag []byte 80 var blobCid string 81 82 if err := rows.Scan( 83 &artifact.Did, 84 &artifact.Rkey, 85 &artifact.RepoAt, 86 &tag, 87 &createdAt, 88 &blobCid, ··· 91 &artifact.MimeType, 92 ); err != nil { 93 return nil, err 94 } 95 96 artifact.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
··· 1 package db 2 3 import ( 4 + "database/sql" 5 "fmt" 6 "strings" 7 "time" ··· 13 ) 14 15 func AddArtifact(e Execer, artifact models.Artifact) error { 16 + var repoDid *string 17 + if artifact.RepoDid != "" { 18 + repoDid = &artifact.RepoDid 19 + } 20 _, err := e.Exec( 21 `insert or ignore into artifacts ( 22 did, 23 rkey, 24 repo_at, 25 + repo_did, 26 tag, 27 created, 28 blob_cid, ··· 30 size, 31 mimetype 32 ) 33 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 34 artifact.Did, 35 artifact.Rkey, 36 artifact.RepoAt, 37 + repoDid, 38 artifact.Tag[:], 39 artifact.CreatedAt.Format(time.RFC3339), 40 artifact.BlobCid.String(), ··· 64 did, 65 rkey, 66 repo_at, 67 + repo_did, 68 tag, 69 created, 70 blob_cid, ··· 86 var createdAt string 87 var tag []byte 88 var blobCid string 89 + var repoDid sql.NullString 90 91 if err := rows.Scan( 92 &artifact.Did, 93 &artifact.Rkey, 94 &artifact.RepoAt, 95 + &repoDid, 96 &tag, 97 &createdAt, 98 &blobCid, ··· 101 &artifact.MimeType, 102 ); err != nil { 103 return nil, err 104 + } 105 + if repoDid.Valid { 106 + artifact.RepoDid = repoDid.String 107 } 108 109 artifact.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
+15 -3
appview/db/collaborators.go
··· 1 package db 2 3 import ( 4 "fmt" 5 "strings" 6 "time" ··· 10 ) 11 12 func AddCollaborator(e Execer, c models.Collaborator) error { 13 _, err := e.Exec( 14 - `insert into collaborators (did, rkey, subject_did, repo_at) values (?, ?, ?, ?);`, 15 - c.Did, c.Rkey, c.SubjectDid, c.RepoAt, 16 ) 17 return err 18 } ··· 80 rkey, 81 subject_did, 82 repo_at, 83 - created 84 from collaborators %s`, 85 whereClause, 86 ) ··· 92 for rows.Next() { 93 var collaborator models.Collaborator 94 var createdAt string 95 if err := rows.Scan( 96 &collaborator.Id, 97 &collaborator.Did, ··· 99 &collaborator.SubjectDid, 100 &collaborator.RepoAt, 101 &createdAt, 102 ); err != nil { 103 return nil, err 104 } 105 collaborator.Created, err = time.Parse(time.RFC3339, createdAt) 106 if err != nil { 107 collaborator.Created = time.Now() 108 } 109 collaborators = append(collaborators, collaborator) 110 }
··· 1 package db 2 3 import ( 4 + "database/sql" 5 "fmt" 6 "strings" 7 "time" ··· 11 ) 12 13 func AddCollaborator(e Execer, c models.Collaborator) error { 14 + var repoDid *string 15 + if c.RepoDid != "" { 16 + repoDid = &c.RepoDid 17 + } 18 + 19 _, err := e.Exec( 20 + `insert into collaborators (did, rkey, subject_did, repo_at, repo_did) values (?, ?, ?, ?, ?);`, 21 + c.Did, c.Rkey, c.SubjectDid, c.RepoAt, repoDid, 22 ) 23 return err 24 } ··· 86 rkey, 87 subject_did, 88 repo_at, 89 + created, 90 + repo_did 91 from collaborators %s`, 92 whereClause, 93 ) ··· 99 for rows.Next() { 100 var collaborator models.Collaborator 101 var createdAt string 102 + var collabRepoDid sql.NullString 103 if err := rows.Scan( 104 &collaborator.Id, 105 &collaborator.Did, ··· 107 &collaborator.SubjectDid, 108 &collaborator.RepoAt, 109 &createdAt, 110 + &collabRepoDid, 111 ); err != nil { 112 return nil, err 113 } 114 collaborator.Created, err = time.Parse(time.RFC3339, createdAt) 115 if err != nil { 116 collaborator.Created = time.Now() 117 + } 118 + if collabRepoDid.Valid { 119 + collaborator.RepoDid = collabRepoDid.String 120 } 121 collaborators = append(collaborators, collaborator) 122 }
+318
appview/db/db.go
··· 1255 return err 1256 }) 1257 1258 return &DB{ 1259 db, 1260 logger,
··· 1255 return err 1256 }) 1257 1258 + orm.RunMigration(conn, logger, "add-repo-did-column", func(tx *sql.Tx) error { 1259 + _, err := tx.Exec(` 1260 + alter table repos add column repo_did text; 1261 + create unique index if not exists idx_repos_repo_did on repos(repo_did); 1262 + 1263 + alter table issues add column repo_did text; 1264 + alter table pulls add column repo_did text; 1265 + alter table artifacts add column repo_did text; 1266 + alter table webhooks add column repo_did text; 1267 + alter table collaborators add column repo_did text; 1268 + alter table pull_comments add column repo_did text; 1269 + alter table profile_pinned_repositories add column repo_did text; 1270 + alter table repo_issue_seqs add column repo_did text; 1271 + alter table repo_pull_seqs add column repo_did text; 1272 + alter table repo_languages add column repo_did text; 1273 + alter table repo_labels add column repo_did text; 1274 + alter table stars add column subject_did text; 1275 + `) 1276 + return err 1277 + }) 1278 + 1279 + conn.ExecContext(ctx, "pragma foreign_keys = off;") 1280 + orm.RunMigration(conn, logger, "add-repo-did-fk-constraints", func(tx *sql.Tx) error { 1281 + _, err := tx.Exec(` 1282 + create table repo_issue_seqs_new ( 1283 + repo_at text primary key, 1284 + next_issue_id integer not null default 1, 1285 + repo_did text, 1286 + foreign key (repo_did) references repos(repo_did) on delete cascade 1287 + ); 1288 + insert into repo_issue_seqs_new select repo_at, next_issue_id, repo_did from repo_issue_seqs; 1289 + drop table repo_issue_seqs; 1290 + alter table repo_issue_seqs_new rename to repo_issue_seqs; 1291 + 1292 + create table repo_pull_seqs_new ( 1293 + repo_at text primary key, 1294 + next_pull_id integer not null default 1, 1295 + repo_did text, 1296 + foreign key (repo_did) references repos(repo_did) on delete cascade 1297 + ); 1298 + insert into repo_pull_seqs_new select repo_at, next_pull_id, repo_did from repo_pull_seqs; 1299 + drop table repo_pull_seqs; 1300 + alter table repo_pull_seqs_new rename to repo_pull_seqs; 1301 + 1302 + create table repo_languages_new ( 1303 + id integer primary key autoincrement, 1304 + repo_at text not null, 1305 + ref text not null, 1306 + is_default_ref integer not null default 0, 1307 + language text not null, 1308 + bytes integer not null check (bytes >= 0), 1309 + repo_did text, 1310 + unique(repo_at, ref, language), 1311 + foreign key (repo_did) references repos(repo_did) on delete cascade 1312 + ); 1313 + insert into repo_languages_new select id, repo_at, ref, is_default_ref, language, bytes, repo_did from repo_languages; 1314 + drop table repo_languages; 1315 + alter table repo_languages_new rename to repo_languages; 1316 + 1317 + create table repo_labels_new ( 1318 + id integer primary key autoincrement, 1319 + repo_at text not null, 1320 + label_at text not null, 1321 + repo_did text, 1322 + unique (repo_at, label_at), 1323 + foreign key (repo_did) references repos(repo_did) on delete cascade 1324 + ); 1325 + insert into repo_labels_new select id, repo_at, label_at, repo_did from repo_labels; 1326 + drop table repo_labels; 1327 + alter table repo_labels_new rename to repo_labels; 1328 + `) 1329 + return err 1330 + }) 1331 + conn.ExecContext(ctx, "pragma foreign_keys = on;") 1332 + 1333 + orm.RunMigration(conn, logger, "add-repo-did-indexes", func(tx *sql.Tx) error { 1334 + _, err := tx.Exec(` 1335 + create index if not exists idx_issues_repo_did on issues(repo_did); 1336 + create index if not exists idx_pulls_repo_did on pulls(repo_did); 1337 + create index if not exists idx_artifacts_repo_did on artifacts(repo_did); 1338 + create index if not exists idx_collaborators_repo_did on collaborators(repo_did); 1339 + create index if not exists idx_pull_comments_repo_did on pull_comments(repo_did); 1340 + create index if not exists idx_stars_subject_did on stars(subject_did); 1341 + `) 1342 + return err 1343 + }) 1344 + 1345 + orm.RunMigration(conn, logger, "add-repo-did-indexes-2", func(tx *sql.Tx) error { 1346 + _, err := tx.Exec(` 1347 + create index if not exists idx_repo_issue_seqs_repo_did on repo_issue_seqs(repo_did); 1348 + create index if not exists idx_repo_pull_seqs_repo_did on repo_pull_seqs(repo_did); 1349 + create index if not exists idx_repo_languages_repo_did on repo_languages(repo_did); 1350 + create index if not exists idx_repo_labels_repo_did on repo_labels(repo_did); 1351 + create index if not exists idx_webhooks_repo_did on webhooks(repo_did); 1352 + `) 1353 + return err 1354 + }) 1355 + 1356 + orm.RunMigration(conn, logger, "add-pds-rewrite-status", func(tx *sql.Tx) error { 1357 + _, err := tx.Exec(` 1358 + create table if not exists pds_rewrite_status ( 1359 + id integer primary key autoincrement, 1360 + user_did text not null, 1361 + repo_did text not null, 1362 + record_nsid text not null, 1363 + record_rkey text not null, 1364 + old_repo_at text not null, 1365 + status text not null default 'pending', 1366 + updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1367 + unique(user_did, record_nsid, record_rkey) 1368 + ); 1369 + create index if not exists idx_pds_rewrite_user on pds_rewrite_status(user_did, status); 1370 + `) 1371 + return err 1372 + }) 1373 + 1374 + orm.RunMigration(conn, logger, "add-pipelines-repo-did", func(tx *sql.Tx) error { 1375 + _, err := tx.Exec(` 1376 + alter table pipelines add column repo_did text; 1377 + create index if not exists idx_pipelines_repo_did on pipelines(repo_did); 1378 + `) 1379 + return err 1380 + }) 1381 + 1382 + conn.ExecContext(ctx, "pragma foreign_keys = off;") 1383 + orm.RunMigration(conn, logger, "add-repo-did-fk-content-tables", func(tx *sql.Tx) error { 1384 + _, err := tx.Exec(` 1385 + -- issues: add FK on repo_did 1386 + create table issues_fk ( 1387 + id integer primary key autoincrement, 1388 + did text not null, 1389 + rkey text not null, 1390 + at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo.issue' || '/' || rkey) stored, 1391 + repo_at text not null, 1392 + issue_id integer not null, 1393 + title text not null, 1394 + body text not null, 1395 + open integer not null default 1, 1396 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1397 + edited text, 1398 + deleted text, 1399 + repo_did text, 1400 + unique(did, rkey), 1401 + unique(repo_at, issue_id), 1402 + unique(at_uri), 1403 + foreign key (repo_at) references repos(at_uri) on delete cascade, 1404 + foreign key (repo_did) references repos(repo_did) on delete set null 1405 + ); 1406 + insert into issues_fk (id, did, rkey, repo_at, issue_id, title, body, open, created, edited, deleted, repo_did) 1407 + select id, did, rkey, repo_at, issue_id, title, body, open, created, edited, deleted, repo_did from issues; 1408 + drop table issues; 1409 + alter table issues_fk rename to issues; 1410 + create index if not exists idx_issues_repo_did on issues(repo_did); 1411 + 1412 + -- pulls: add FK on repo_did 1413 + create table pulls_fk ( 1414 + id integer primary key autoincrement, 1415 + pull_id integer not null, 1416 + at_uri text generated always as ('at://' || owner_did || '/' || 'sh.tangled.repo.pull' || '/' || rkey) stored, 1417 + repo_at text not null, 1418 + owner_did text not null, 1419 + rkey text not null, 1420 + title text not null, 1421 + body text not null, 1422 + target_branch text not null, 1423 + state integer not null default 0 check (state in (0, 1, 2, 3)), 1424 + source_branch text, 1425 + source_repo_at text, 1426 + stack_id text, 1427 + change_id text, 1428 + parent_change_id text, 1429 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1430 + repo_did text, 1431 + unique(repo_at, pull_id), 1432 + unique(at_uri), 1433 + foreign key (repo_at) references repos(at_uri) on delete cascade, 1434 + foreign key (repo_did) references repos(repo_did) on delete set null 1435 + ); 1436 + insert into pulls_fk (id, pull_id, repo_at, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_at, stack_id, change_id, parent_change_id, created, repo_did) 1437 + select id, pull_id, repo_at, owner_did, rkey, title, body, target_branch, state, source_branch, source_repo_at, stack_id, change_id, parent_change_id, created, repo_did from pulls; 1438 + drop table pulls; 1439 + alter table pulls_fk rename to pulls; 1440 + create index if not exists idx_pulls_repo_did on pulls(repo_did); 1441 + 1442 + -- artifacts: add FK on repo_did 1443 + create table artifacts_fk ( 1444 + id integer primary key autoincrement, 1445 + did text not null, 1446 + rkey text not null, 1447 + repo_at text not null, 1448 + tag binary(20) not null, 1449 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1450 + blob_cid text not null, 1451 + name text not null, 1452 + size integer not null default 0, 1453 + mimetype string not null default '*/*', 1454 + repo_did text, 1455 + unique(did, rkey), 1456 + unique(repo_at, tag, name), 1457 + foreign key (repo_at) references repos(at_uri) on delete cascade, 1458 + foreign key (repo_did) references repos(repo_did) on delete set null 1459 + ); 1460 + insert into artifacts_fk (id, did, rkey, repo_at, tag, created, blob_cid, name, size, mimetype, repo_did) 1461 + select id, did, rkey, repo_at, tag, created, blob_cid, name, size, mimetype, repo_did from artifacts; 1462 + drop table artifacts; 1463 + alter table artifacts_fk rename to artifacts; 1464 + create index if not exists idx_artifacts_repo_did on artifacts(repo_did); 1465 + 1466 + -- webhooks: add FK on repo_did 1467 + create table webhooks_fk ( 1468 + id integer primary key autoincrement, 1469 + repo_at text not null, 1470 + url text not null, 1471 + secret text, 1472 + active integer not null default 1, 1473 + events text not null, 1474 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1475 + updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1476 + repo_did text, 1477 + foreign key (repo_at) references repos(at_uri) on delete cascade, 1478 + foreign key (repo_did) references repos(repo_did) on delete set null 1479 + ); 1480 + insert into webhooks_fk (id, repo_at, url, secret, active, events, created_at, updated_at, repo_did) 1481 + select id, repo_at, url, secret, active, events, created_at, updated_at, repo_did from webhooks; 1482 + drop table webhooks; 1483 + alter table webhooks_fk rename to webhooks; 1484 + create index if not exists idx_webhooks_repo_did on webhooks(repo_did); 1485 + 1486 + -- collaborators: add FK on repo_did 1487 + create table collaborators_fk ( 1488 + id integer primary key autoincrement, 1489 + did text not null, 1490 + rkey text, 1491 + subject_did text not null, 1492 + repo_at text not null, 1493 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1494 + repo_did text, 1495 + foreign key (repo_at) references repos(at_uri) on delete cascade, 1496 + foreign key (repo_did) references repos(repo_did) on delete set null 1497 + ); 1498 + insert into collaborators_fk (id, did, rkey, subject_did, repo_at, created, repo_did) 1499 + select id, did, rkey, subject_did, repo_at, created, repo_did from collaborators; 1500 + drop table collaborators; 1501 + alter table collaborators_fk rename to collaborators; 1502 + create index if not exists idx_collaborators_repo_did on collaborators(repo_did); 1503 + 1504 + -- pull_comments: add FK on repo_did 1505 + create table pull_comments_fk ( 1506 + id integer primary key autoincrement, 1507 + pull_id integer not null, 1508 + submission_id integer not null, 1509 + repo_at text not null, 1510 + owner_did text not null, 1511 + comment_at text not null, 1512 + body text not null, 1513 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1514 + repo_did text, 1515 + foreign key (repo_at, pull_id) references pulls(repo_at, pull_id) on delete cascade, 1516 + foreign key (submission_id) references pull_submissions(id) on delete cascade, 1517 + foreign key (repo_did) references repos(repo_did) on delete set null 1518 + ); 1519 + insert into pull_comments_fk (id, pull_id, submission_id, repo_at, owner_did, comment_at, body, created, repo_did) 1520 + select id, pull_id, submission_id, repo_at, owner_did, comment_at, body, created, repo_did from pull_comments; 1521 + drop table pull_comments; 1522 + alter table pull_comments_fk rename to pull_comments; 1523 + create index if not exists idx_pull_comments_repo_did on pull_comments(repo_did); 1524 + 1525 + -- pipelines: add FK on repo_did 1526 + create table pipelines_fk ( 1527 + id integer primary key autoincrement, 1528 + knot text not null, 1529 + rkey text not null, 1530 + repo_owner text not null, 1531 + repo_name text not null, 1532 + sha text not null check (length(sha) = 40), 1533 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 1534 + trigger_id integer not null, 1535 + repo_did text, 1536 + unique(knot, rkey), 1537 + foreign key (trigger_id) references triggers(id) on delete cascade, 1538 + foreign key (repo_did) references repos(repo_did) on delete set null 1539 + ); 1540 + insert into pipelines_fk (id, knot, rkey, repo_owner, repo_name, sha, created, trigger_id, repo_did) 1541 + select id, knot, rkey, repo_owner, repo_name, sha, created, trigger_id, repo_did from pipelines; 1542 + drop table pipelines; 1543 + alter table pipelines_fk rename to pipelines; 1544 + create index if not exists idx_pipelines_repo_did on pipelines(repo_did); 1545 + 1546 + -- profile_pinned_repositories: add FK on repo_did 1547 + create table profile_pinned_repositories_fk ( 1548 + id integer primary key autoincrement, 1549 + did text not null, 1550 + at_uri text not null, 1551 + repo_did text, 1552 + unique(did, at_uri), 1553 + foreign key (did) references profile(did) on delete cascade, 1554 + foreign key (at_uri) references repos(at_uri) on delete cascade, 1555 + foreign key (repo_did) references repos(repo_did) on delete set null 1556 + ); 1557 + insert into profile_pinned_repositories_fk (id, did, at_uri, repo_did) 1558 + select id, did, at_uri, repo_did from profile_pinned_repositories; 1559 + drop table profile_pinned_repositories; 1560 + alter table profile_pinned_repositories_fk rename to profile_pinned_repositories; 1561 + `) 1562 + return err 1563 + }) 1564 + conn.ExecContext(ctx, "pragma foreign_keys = on;") 1565 + 1566 + orm.RunMigration(conn, logger, "add-source-repo-did-to-pulls", func(tx *sql.Tx) error { 1567 + _, err := tx.Exec(`alter table pulls add column source_repo_did text;`) 1568 + return err 1569 + }) 1570 + 1571 + orm.RunMigration(conn, logger, "migrate-knots-to-repo-dids", func(tx *sql.Tx) error { 1572 + _, err := tx.Exec(`update registrations set needs_upgrade = 1`) 1573 + return err 1574 + }) 1575 + 1576 return &DB{ 1577 db, 1578 logger,
+28 -10
appview/db/issues.go
··· 17 ) 18 19 func PutIssue(tx *sql.Tx, issue *models.Issue) error { 20 - // ensure sequence exists 21 _, err := tx.Exec(` 22 - insert or ignore into repo_issue_seqs (repo_at, next_issue_id) 23 - values (?, 1) 24 - `, issue.RepoAt) 25 if err != nil { 26 return err 27 } ··· 64 return err 65 } 66 67 // insert new issue 68 row := tx.QueryRow(` 69 - insert into issues (repo_at, did, rkey, issue_id, title, body) 70 - values (?, ?, ?, ?, ?, ?) 71 returning rowid, issue_id 72 - `, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body) 73 74 err = row.Scan(&issue.Id, &issue.IssueId) 75 if err != nil { ··· 83 } 84 85 func updateIssue(tx *sql.Tx, issue *models.Issue) error { 86 - // update existing issue 87 _, err := tx.Exec(` 88 update issues 89 - set title = ?, body = ?, edited = ? 90 where did = ? and rkey = ? 91 - `, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey) 92 if err != nil { 93 return err 94 } ··· 133 did, 134 rkey, 135 repo_at, 136 issue_id, 137 title, 138 body, ··· 161 var issue models.Issue 162 var createdAt string 163 var editedAt, deletedAt sql.Null[string] 164 var rowNum int64 165 err := rows.Scan( 166 &issue.Id, 167 &issue.Did, 168 &issue.Rkey, 169 &issue.RepoAt, 170 &issue.IssueId, 171 &issue.Title, 172 &issue.Body, ··· 178 ) 179 if err != nil { 180 return nil, fmt.Errorf("failed to scan issue: %w", err) 181 } 182 183 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
··· 17 ) 18 19 func PutIssue(tx *sql.Tx, issue *models.Issue) error { 20 + var seqRepoDid *string 21 + if issue.RepoDid != "" { 22 + seqRepoDid = &issue.RepoDid 23 + } 24 _, err := tx.Exec(` 25 + insert into repo_issue_seqs (repo_at, next_issue_id, repo_did) 26 + values (?, 1, ?) 27 + on conflict(repo_at) do update set repo_did = coalesce(excluded.repo_did, repo_did) 28 + `, issue.RepoAt, seqRepoDid) 29 if err != nil { 30 return err 31 } ··· 68 return err 69 } 70 71 + var repoDid *string 72 + if issue.RepoDid != "" { 73 + repoDid = &issue.RepoDid 74 + } 75 + 76 // insert new issue 77 row := tx.QueryRow(` 78 + insert into issues (repo_at, repo_did, did, rkey, issue_id, title, body) 79 + values (?, ?, ?, ?, ?, ?, ?) 80 returning rowid, issue_id 81 + `, issue.RepoAt, repoDid, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body) 82 83 err = row.Scan(&issue.Id, &issue.IssueId) 84 if err != nil { ··· 92 } 93 94 func updateIssue(tx *sql.Tx, issue *models.Issue) error { 95 + var repoDid *string 96 + if issue.RepoDid != "" { 97 + repoDid = &issue.RepoDid 98 + } 99 _, err := tx.Exec(` 100 update issues 101 + set title = ?, body = ?, edited = ?, repo_did = coalesce(?, repo_did) 102 where did = ? and rkey = ? 103 + `, issue.Title, issue.Body, time.Now().Format(time.RFC3339), repoDid, issue.Did, issue.Rkey) 104 if err != nil { 105 return err 106 } ··· 145 did, 146 rkey, 147 repo_at, 148 + repo_did, 149 issue_id, 150 title, 151 body, ··· 174 var issue models.Issue 175 var createdAt string 176 var editedAt, deletedAt sql.Null[string] 177 + var nullableRepoDid sql.NullString 178 var rowNum int64 179 err := rows.Scan( 180 &issue.Id, 181 &issue.Did, 182 &issue.Rkey, 183 &issue.RepoAt, 184 + &nullableRepoDid, 185 &issue.IssueId, 186 &issue.Title, 187 &issue.Body, ··· 193 ) 194 if err != nil { 195 return nil, fmt.Errorf("failed to scan issue: %w", err) 196 + } 197 + if nullableRepoDid.Valid { 198 + issue.RepoDid = nullableRepoDid.String 199 } 200 201 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
+13 -3
appview/db/language.go
··· 24 } 25 26 query := fmt.Sprintf( 27 - `select id, repo_at, ref, is_default_ref, language, bytes from repo_languages %s`, 28 whereClause, 29 ) 30 rows, err := e.Query(query, args...) ··· 37 for rows.Next() { 38 var rl models.RepoLanguage 39 var isDefaultRef int 40 41 err := rows.Scan( 42 &rl.Id, ··· 45 &isDefaultRef, 46 &rl.Language, 47 &rl.Bytes, 48 ) 49 if err != nil { 50 return nil, fmt.Errorf("failed to scan: %w ", err) ··· 53 if isDefaultRef != 0 { 54 rl.IsDefaultRef = true 55 } 56 57 langs = append(langs, rl) 58 } ··· 65 66 func InsertRepoLanguages(e Execer, langs []models.RepoLanguage) error { 67 stmt, err := e.Prepare( 68 - "insert or replace into repo_languages (repo_at, ref, is_default_ref, language, bytes) values (?, ?, ?, ?, ?)", 69 ) 70 if err != nil { 71 return err ··· 77 isDefaultRef = 1 78 } 79 80 - _, err := stmt.Exec(l.RepoAt, l.Ref, isDefaultRef, l.Language, l.Bytes) 81 if err != nil { 82 return err 83 }
··· 24 } 25 26 query := fmt.Sprintf( 27 + `select id, repo_at, ref, is_default_ref, language, bytes, repo_did from repo_languages %s`, 28 whereClause, 29 ) 30 rows, err := e.Query(query, args...) ··· 37 for rows.Next() { 38 var rl models.RepoLanguage 39 var isDefaultRef int 40 + var langRepoDid sql.NullString 41 42 err := rows.Scan( 43 &rl.Id, ··· 46 &isDefaultRef, 47 &rl.Language, 48 &rl.Bytes, 49 + &langRepoDid, 50 ) 51 if err != nil { 52 return nil, fmt.Errorf("failed to scan: %w ", err) ··· 55 if isDefaultRef != 0 { 56 rl.IsDefaultRef = true 57 } 58 + if langRepoDid.Valid { 59 + rl.RepoDid = langRepoDid.String 60 + } 61 62 langs = append(langs, rl) 63 } ··· 70 71 func InsertRepoLanguages(e Execer, langs []models.RepoLanguage) error { 72 stmt, err := e.Prepare( 73 + "insert or replace into repo_languages (repo_at, ref, is_default_ref, language, bytes, repo_did) values (?, ?, ?, ?, ?, ?)", 74 ) 75 if err != nil { 76 return err ··· 82 isDefaultRef = 1 83 } 84 85 + var repoDid *string 86 + if l.RepoDid != "" { 87 + repoDid = &l.RepoDid 88 + } 89 + 90 + _, err := stmt.Exec(l.RepoAt, l.Ref, isDefaultRef, l.Language, l.Bytes, repoDid) 91 if err != nil { 92 return err 93 }
+21 -2
appview/db/pipeline.go
··· 1 package db 2 3 import ( 4 "fmt" 5 "slices" 6 "strings" ··· 26 whereClause = " where " + strings.Join(conditions, " and ") 27 } 28 29 - query := fmt.Sprintf(`select id, rkey, knot, repo_owner, repo_name, sha, created from pipelines %s`, whereClause) 30 31 rows, err := e.Query(query, args...) 32 ··· 38 for rows.Next() { 39 var pipeline models.Pipeline 40 var createdAt string 41 err = rows.Scan( 42 &pipeline.Id, 43 &pipeline.Rkey, ··· 46 &pipeline.RepoName, 47 &pipeline.Sha, 48 &createdAt, 49 ) 50 if err != nil { 51 return nil, err ··· 54 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 55 pipeline.Created = t 56 } 57 58 pipelines = append(pipelines, pipeline) 59 } ··· 66 } 67 68 func AddPipeline(e Execer, pipeline models.Pipeline) error { 69 args := []any{ 70 pipeline.Rkey, 71 pipeline.Knot, ··· 73 pipeline.RepoName, 74 pipeline.TriggerId, 75 pipeline.Sha, 76 } 77 78 placeholders := make([]string, len(args)) ··· 87 repo_owner, 88 repo_name, 89 trigger_id, 90 - sha 91 ) values (%s) 92 `, strings.Join(placeholders, ",")) 93 ··· 195 p.repo_name, 196 p.sha, 197 p.created, 198 t.id, 199 t.kind, 200 t.push_ref, ··· 224 var p models.Pipeline 225 var t models.Trigger 226 var created string 227 228 err := rows.Scan( 229 &p.Id, ··· 233 &p.RepoName, 234 &p.Sha, 235 &created, 236 &p.TriggerId, 237 &t.Kind, 238 &t.PushRef, ··· 250 p.Created, err = time.Parse(time.RFC3339, created) 251 if err != nil { 252 return nil, fmt.Errorf("invalid pipeline created timestamp %q: %w", created, err) 253 } 254 255 t.Id = p.TriggerId
··· 1 package db 2 3 import ( 4 + "database/sql" 5 "fmt" 6 "slices" 7 "strings" ··· 27 whereClause = " where " + strings.Join(conditions, " and ") 28 } 29 30 + query := fmt.Sprintf(`select id, rkey, knot, repo_owner, repo_name, sha, created, repo_did from pipelines %s`, whereClause) 31 32 rows, err := e.Query(query, args...) 33 ··· 39 for rows.Next() { 40 var pipeline models.Pipeline 41 var createdAt string 42 + var repoDid sql.NullString 43 err = rows.Scan( 44 &pipeline.Id, 45 &pipeline.Rkey, ··· 48 &pipeline.RepoName, 49 &pipeline.Sha, 50 &createdAt, 51 + &repoDid, 52 ) 53 if err != nil { 54 return nil, err ··· 57 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 58 pipeline.Created = t 59 } 60 + if repoDid.Valid { 61 + pipeline.RepoDid = repoDid.String 62 + } 63 64 pipelines = append(pipelines, pipeline) 65 } ··· 72 } 73 74 func AddPipeline(e Execer, pipeline models.Pipeline) error { 75 + var repoDid *string 76 + if pipeline.RepoDid != "" { 77 + repoDid = &pipeline.RepoDid 78 + } 79 + 80 args := []any{ 81 pipeline.Rkey, 82 pipeline.Knot, ··· 84 pipeline.RepoName, 85 pipeline.TriggerId, 86 pipeline.Sha, 87 + repoDid, 88 } 89 90 placeholders := make([]string, len(args)) ··· 99 repo_owner, 100 repo_name, 101 trigger_id, 102 + sha, 103 + repo_did 104 ) values (%s) 105 `, strings.Join(placeholders, ",")) 106 ··· 208 p.repo_name, 209 p.sha, 210 p.created, 211 + p.repo_did, 212 t.id, 213 t.kind, 214 t.push_ref, ··· 238 var p models.Pipeline 239 var t models.Trigger 240 var created string 241 + var repoDid sql.NullString 242 243 err := rows.Scan( 244 &p.Id, ··· 248 &p.RepoName, 249 &p.Sha, 250 &created, 251 + &repoDid, 252 &p.TriggerId, 253 &t.Kind, 254 &t.PushRef, ··· 266 p.Created, err = time.Parse(time.RFC3339, created) 267 if err != nil { 268 return nil, fmt.Errorf("invalid pipeline created timestamp %q: %w", created, err) 269 + } 270 + if repoDid.Valid { 271 + p.RepoDid = repoDid.String 272 } 273 274 t.Id = p.TriggerId
+6 -3
appview/db/profile.go
··· 75 // TODO: get this in the original query; requires COALESCE because nullable 76 var sourceRepo *models.Repo 77 if repo.Source != "" { 78 - sourceRepo, err = GetRepoByAtUri(e, repo.Source) 79 if err != nil { 80 - // the source repo was not found, skip this bit 81 log.Println("profile", "err", err) 82 } 83 } ··· 449 query = `select count(id) from repos where did = ?` 450 args = append(args, did) 451 case models.VanityStatStarCount: 452 - query = `select count(id) from stars where subject_at like 'at://' || ? || '%'` 453 args = append(args, did) 454 case models.VanityStatNone: 455 return 0, nil
··· 75 // TODO: get this in the original query; requires COALESCE because nullable 76 var sourceRepo *models.Repo 77 if repo.Source != "" { 78 + if strings.HasPrefix(repo.Source, "did:") { 79 + sourceRepo, err = GetRepoByDid(e, repo.Source) 80 + } else { 81 + sourceRepo, err = GetRepoByAtUri(e, repo.Source) 82 + } 83 if err != nil { 84 log.Println("profile", "err", err) 85 } 86 } ··· 452 query = `select count(id) from repos where did = ?` 453 args = append(args, did) 454 case models.VanityStatStarCount: 455 + query = `select count(s.id) from stars s join repos r on (s.subject_at = r.at_uri or (s.subject_did is not null and s.subject_did = r.repo_did)) where r.did = ?` 456 args = append(args, did) 457 case models.VanityStatNone: 458 return 0, nil
+57 -12
appview/db/pulls.go
··· 18 ) 19 20 func NewPull(tx *sql.Tx, pull *models.Pull) error { 21 _, err := tx.Exec(` 22 - insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 23 - values (?, 1) 24 - `, pull.RepoAt) 25 if err != nil { 26 return err 27 } ··· 40 pull.PullId = nextId 41 pull.State = models.PullOpen 42 43 - var sourceBranch, sourceRepoAt *string 44 if pull.PullSource != nil { 45 sourceBranch = &pull.PullSource.Branch 46 if pull.PullSource.RepoAt != nil { 47 x := pull.PullSource.RepoAt.String() 48 sourceRepoAt = &x 49 } 50 } 51 52 var stackId, changeId, parentChangeId *string ··· 63 result, err := tx.Exec( 64 ` 65 insert into pulls ( 66 - repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at, stack_id, change_id, parent_change_id 67 ) 68 - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 69 pull.RepoAt, 70 pull.OwnerDid, 71 pull.PullId, ··· 79 stackId, 80 changeId, 81 parentChangeId, 82 ) 83 if err != nil { 84 return err ··· 159 source_repo_at, 160 stack_id, 161 change_id, 162 - parent_change_id 163 from 164 pulls 165 %s ··· 177 for rows.Next() { 178 var pull models.Pull 179 var createdAt string 180 - var sourceBranch, sourceRepoAt, stackId, changeId, parentChangeId sql.NullString 181 err := rows.Scan( 182 &pull.ID, 183 &pull.OwnerDid, ··· 194 &stackId, 195 &changeId, 196 &parentChangeId, 197 ) 198 if err != nil { 199 return nil, err ··· 216 } 217 pull.PullSource.RepoAt = &sourceRepoAtParsed 218 } 219 } 220 221 if stackId.Valid { ··· 227 if parentChangeId.Valid { 228 pull.ParentChangeId = parentChangeId.String 229 } 230 231 pulls[pull.AtUri()] = &pull 232 } ··· 441 owner_did, 442 comment_at, 443 body, 444 - created 445 from 446 pull_comments 447 %s ··· 459 for rows.Next() { 460 var comment models.PullComment 461 var createdAt string 462 err := rows.Scan( 463 &comment.ID, 464 &comment.PullId, ··· 468 &comment.CommentAt, 469 &comment.Body, 470 &createdAt, 471 ) 472 if err != nil { 473 return nil, err ··· 475 476 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 477 comment.Created = t 478 } 479 480 atUri := comment.AtUri().String() ··· 522 p.created, 523 p.title, 524 p.state, 525 r.did, 526 r.name, 527 r.knot, 528 r.rkey, 529 - r.created 530 from 531 pulls p 532 join ··· 544 var pull models.Pull 545 var repo models.Repo 546 var pullCreatedAt, repoCreatedAt string 547 err := rows.Scan( 548 &pull.OwnerDid, 549 &pull.RepoAt, ··· 551 &pullCreatedAt, 552 &pull.Title, 553 &pull.State, 554 &repo.Did, 555 &repo.Name, 556 &repo.Knot, 557 &repo.Rkey, 558 &repoCreatedAt, 559 ) 560 if err != nil { 561 return nil, err 562 } 563 564 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 565 if err != nil { 566 return nil, err ··· 572 return nil, err 573 } 574 repo.Created = repoCreatedTime 575 576 pull.Repo = &repo 577 ··· 586 } 587 588 func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) { 589 - query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)` 590 res, err := tx.Exec( 591 query, 592 comment.OwnerDid, ··· 595 comment.CommentAt, 596 comment.PullId, 597 comment.Body, 598 ) 599 if err != nil { 600 return 0, err ··· 614 615 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error { 616 _, err := e.Exec( 617 - `update pulls set state = ? where repo_at = ? and pull_id = ? and (state <> ? or state <> ?)`, 618 pullState, 619 repoAt, 620 pullId,
··· 18 ) 19 20 func NewPull(tx *sql.Tx, pull *models.Pull) error { 21 + var repoDid *string 22 + if pull.RepoDid != "" { 23 + repoDid = &pull.RepoDid 24 + } 25 + 26 _, err := tx.Exec(` 27 + insert into repo_pull_seqs (repo_at, repo_did, next_pull_id) 28 + values (?, ?, 1) 29 + on conflict(repo_at) do update set repo_did = coalesce(excluded.repo_did, repo_did) 30 + `, pull.RepoAt, repoDid) 31 if err != nil { 32 return err 33 } ··· 46 pull.PullId = nextId 47 pull.State = models.PullOpen 48 49 + var sourceBranch, sourceRepoAt, sourceRepoDid *string 50 if pull.PullSource != nil { 51 sourceBranch = &pull.PullSource.Branch 52 if pull.PullSource.RepoAt != nil { 53 x := pull.PullSource.RepoAt.String() 54 sourceRepoAt = &x 55 } 56 + if pull.PullSource.RepoDid != "" { 57 + sourceRepoDid = &pull.PullSource.RepoDid 58 + } 59 } 60 61 var stackId, changeId, parentChangeId *string ··· 72 result, err := tx.Exec( 73 ` 74 insert into pulls ( 75 + repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at, stack_id, change_id, parent_change_id, repo_did, source_repo_did 76 ) 77 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 78 pull.RepoAt, 79 pull.OwnerDid, 80 pull.PullId, ··· 88 stackId, 89 changeId, 90 parentChangeId, 91 + repoDid, 92 + sourceRepoDid, 93 ) 94 if err != nil { 95 return err ··· 170 source_repo_at, 171 stack_id, 172 change_id, 173 + parent_change_id, 174 + repo_did, 175 + source_repo_did 176 from 177 pulls 178 %s ··· 190 for rows.Next() { 191 var pull models.Pull 192 var createdAt string 193 + var sourceBranch, sourceRepoAt, stackId, changeId, parentChangeId, repoDid, sourceRepoDid sql.NullString 194 err := rows.Scan( 195 &pull.ID, 196 &pull.OwnerDid, ··· 207 &stackId, 208 &changeId, 209 &parentChangeId, 210 + &repoDid, 211 + &sourceRepoDid, 212 ) 213 if err != nil { 214 return nil, err ··· 231 } 232 pull.PullSource.RepoAt = &sourceRepoAtParsed 233 } 234 + if sourceRepoDid.Valid { 235 + pull.PullSource.RepoDid = sourceRepoDid.String 236 + } 237 } 238 239 if stackId.Valid { ··· 245 if parentChangeId.Valid { 246 pull.ParentChangeId = parentChangeId.String 247 } 248 + if repoDid.Valid { 249 + pull.RepoDid = repoDid.String 250 + } 251 252 pulls[pull.AtUri()] = &pull 253 } ··· 462 owner_did, 463 comment_at, 464 body, 465 + created, 466 + repo_did 467 from 468 pull_comments 469 %s ··· 481 for rows.Next() { 482 var comment models.PullComment 483 var createdAt string 484 + var commentRepoDid sql.NullString 485 err := rows.Scan( 486 &comment.ID, 487 &comment.PullId, ··· 491 &comment.CommentAt, 492 &comment.Body, 493 &createdAt, 494 + &commentRepoDid, 495 ) 496 if err != nil { 497 return nil, err ··· 499 500 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 501 comment.Created = t 502 + } 503 + if commentRepoDid.Valid { 504 + comment.RepoDid = commentRepoDid.String 505 } 506 507 atUri := comment.AtUri().String() ··· 549 p.created, 550 p.title, 551 p.state, 552 + p.repo_did, 553 r.did, 554 r.name, 555 r.knot, 556 r.rkey, 557 + r.created, 558 + r.repo_did 559 from 560 pulls p 561 join ··· 573 var pull models.Pull 574 var repo models.Repo 575 var pullCreatedAt, repoCreatedAt string 576 + var pullRepoDid, repoRepoDid sql.NullString 577 err := rows.Scan( 578 &pull.OwnerDid, 579 &pull.RepoAt, ··· 581 &pullCreatedAt, 582 &pull.Title, 583 &pull.State, 584 + &pullRepoDid, 585 &repo.Did, 586 &repo.Name, 587 &repo.Knot, 588 &repo.Rkey, 589 &repoCreatedAt, 590 + &repoRepoDid, 591 ) 592 if err != nil { 593 return nil, err 594 } 595 596 + if pullRepoDid.Valid { 597 + pull.RepoDid = pullRepoDid.String 598 + } 599 + 600 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 601 if err != nil { 602 return nil, err ··· 608 return nil, err 609 } 610 repo.Created = repoCreatedTime 611 + if repoRepoDid.Valid { 612 + repo.RepoDid = repoRepoDid.String 613 + } 614 615 pull.Repo = &repo 616 ··· 625 } 626 627 func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) { 628 + var repoDid *string 629 + if comment.RepoDid != "" { 630 + repoDid = &comment.RepoDid 631 + } 632 + 633 + query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body, repo_did) values (?, ?, ?, ?, ?, ?, ?)` 634 res, err := tx.Exec( 635 query, 636 comment.OwnerDid, ··· 639 comment.CommentAt, 640 comment.PullId, 641 comment.Body, 642 + repoDid, 643 ) 644 if err != nil { 645 return 0, err ··· 659 660 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error { 661 _, err := e.Exec( 662 + `update pulls set state = ? where repo_at = ? and pull_id = ? and (state <> ? and state <> ?)`, 663 pullState, 664 repoAt, 665 pullId,
+36 -12
appview/db/reference.go
··· 60 on r.did = inp.owner_did 61 and r.name = inp.name 62 join issues i 63 - on i.repo_at = r.at_uri 64 and i.issue_id = inp.issue_id 65 left join issue_comments c 66 on inp.comment_id is not null ··· 131 on r.did = inp.owner_did 132 and r.name = inp.name 133 join pulls p 134 - on p.repo_at = r.at_uri 135 and p.pull_id = inp.pull_id 136 left join pull_comments c 137 on inp.comment_id is not null 138 - and c.repo_at = r.at_uri and c.pull_id = p.pull_id 139 and c.id = inp.comment_id 140 `, 141 strings.Join(vals, ","), ··· 316 } 317 rows, err := e.Query( 318 fmt.Sprintf( 319 - `select r.did, r.name, i.issue_id, i.title, i.open 320 from issues i 321 join repos r 322 - on r.at_uri = i.repo_at 323 where (i.did, i.rkey) in (%s)`, 324 strings.Join(vals, ","), 325 ), ··· 351 filter := orm.FilterIn("c.at_uri", aturis) 352 rows, err := e.Query( 353 fmt.Sprintf( 354 - `select r.did, r.name, i.issue_id, c.id, i.title, i.open 355 from issue_comments c 356 join issues i 357 on i.at_uri = c.issue_at 358 join repos r 359 - on r.at_uri = i.repo_at 360 where %s`, 361 filter.Condition(), 362 ), ··· 396 } 397 rows, err := e.Query( 398 fmt.Sprintf( 399 - `select r.did, r.name, p.pull_id, p.title, p.state 400 from pulls p 401 join repos r 402 - on r.at_uri = p.repo_at 403 where (p.owner_did, p.rkey) in (%s)`, 404 strings.Join(vals, ","), 405 ), ··· 431 filter := orm.FilterIn("c.comment_at", aturis) 432 rows, err := e.Query( 433 fmt.Sprintf( 434 - `select r.did, r.name, p.pull_id, c.id, p.title, p.state 435 from repos r 436 join pulls p 437 - on r.at_uri = p.repo_at 438 join pull_comments c 439 - on r.at_uri = c.repo_at and p.pull_id = c.pull_id 440 where %s`, 441 filter.Condition(), 442 ),
··· 60 on r.did = inp.owner_did 61 and r.name = inp.name 62 join issues i 63 + on coalesce( 64 + nullif(i.repo_did, '') = nullif(r.repo_did, ''), 65 + i.repo_at = r.at_uri 66 + ) 67 and i.issue_id = inp.issue_id 68 left join issue_comments c 69 on inp.comment_id is not null ··· 134 on r.did = inp.owner_did 135 and r.name = inp.name 136 join pulls p 137 + on coalesce( 138 + nullif(p.repo_did, '') = nullif(r.repo_did, ''), 139 + p.repo_at = r.at_uri 140 + ) 141 and p.pull_id = inp.pull_id 142 left join pull_comments c 143 on inp.comment_id is not null 144 + and coalesce( 145 + nullif(c.repo_did, '') = nullif(r.repo_did, ''), 146 + c.repo_at = r.at_uri 147 + ) and c.pull_id = p.pull_id 148 and c.id = inp.comment_id 149 `, 150 strings.Join(vals, ","), ··· 325 } 326 rows, err := e.Query( 327 fmt.Sprintf( 328 + `select distinct r.did, r.name, i.issue_id, i.title, i.open 329 from issues i 330 join repos r 331 + on coalesce( 332 + nullif(i.repo_did, '') = nullif(r.repo_did, ''), 333 + i.repo_at = r.at_uri 334 + ) 335 where (i.did, i.rkey) in (%s)`, 336 strings.Join(vals, ","), 337 ), ··· 363 filter := orm.FilterIn("c.at_uri", aturis) 364 rows, err := e.Query( 365 fmt.Sprintf( 366 + `select distinct r.did, r.name, i.issue_id, c.id, i.title, i.open 367 from issue_comments c 368 join issues i 369 on i.at_uri = c.issue_at 370 join repos r 371 + on coalesce( 372 + nullif(i.repo_did, '') = nullif(r.repo_did, ''), 373 + i.repo_at = r.at_uri 374 + ) 375 where %s`, 376 filter.Condition(), 377 ), ··· 411 } 412 rows, err := e.Query( 413 fmt.Sprintf( 414 + `select distinct r.did, r.name, p.pull_id, p.title, p.state 415 from pulls p 416 join repos r 417 + on coalesce( 418 + nullif(p.repo_did, '') = nullif(r.repo_did, ''), 419 + p.repo_at = r.at_uri 420 + ) 421 where (p.owner_did, p.rkey) in (%s)`, 422 strings.Join(vals, ","), 423 ), ··· 449 filter := orm.FilterIn("c.comment_at", aturis) 450 rows, err := e.Query( 451 fmt.Sprintf( 452 + `select distinct r.did, r.name, p.pull_id, c.id, p.title, p.state 453 from repos r 454 join pulls p 455 + on coalesce( 456 + nullif(p.repo_did, '') = nullif(r.repo_did, ''), 457 + p.repo_at = r.at_uri 458 + ) 459 join pull_comments c 460 + on coalesce( 461 + nullif(c.repo_did, '') = nullif(r.repo_did, ''), 462 + c.repo_at = r.at_uri 463 + ) and p.pull_id = c.pull_id 464 where %s`, 465 filter.Condition(), 466 ),
+107 -26
appview/db/repos.go
··· 46 website, 47 topics, 48 source, 49 - spindle 50 from 51 repos r 52 %s ··· 64 for rows.Next() { 65 var repo models.Repo 66 var createdAt string 67 - var description, website, topicStr, source, spindle sql.NullString 68 69 err := rows.Scan( 70 &repo.Id, ··· 78 &topicStr, 79 &source, 80 &spindle, 81 ) 82 if err != nil { 83 return nil, fmt.Errorf("failed to execute repo query: %w ", err) ··· 100 } 101 if spindle.Valid { 102 repo.Spindle = spindle.String 103 } 104 105 repo.RepoStats = &models.RepoStats{} ··· 184 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 185 } 186 187 starCountQuery := fmt.Sprintf( 188 - `select 189 - subject_at, count(1) 190 from stars 191 - where subject_at in (%s) 192 - group by subject_at`, 193 - inClause, 194 ) 195 - rows, err = e.Query(starCountQuery, args...) 196 if err != nil { 197 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 198 } 199 defer rows.Close() 200 201 for rows.Next() { 202 - var repoat string 203 var count int 204 - if err := rows.Scan(&repoat, &count); err != nil { 205 log.Println("err", "err", err) 206 continue 207 } 208 - if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 209 r.RepoStats.StarCount = count 210 } 211 } 212 if err = rows.Err(); err != nil { ··· 352 var nullableDescription sql.NullString 353 var nullableWebsite sql.NullString 354 var nullableTopicStr sql.NullString 355 356 - row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics from repos where at_uri = ?`, atUri) 357 358 var createdAt string 359 - if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr); err != nil { 360 return nil, err 361 } 362 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 371 if nullableTopicStr.Valid { 372 repo.Topics = strings.Fields(nullableTopicStr.String) 373 } 374 375 return &repo, nil 376 } 377 378 func PutRepo(tx *sql.Tx, repo models.Repo) error { 379 _, err := tx.Exec( 380 `update repos 381 - set knot = ?, description = ?, website = ?, topics = ? 382 where did = ? and rkey = ? 383 `, 384 - repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repo.Did, repo.Rkey, 385 ) 386 return err 387 } 388 389 func AddRepo(tx *sql.Tx, repo *models.Repo) error { 390 _, err := tx.Exec( 391 `insert into repos 392 - (did, name, knot, rkey, at_uri, description, website, topics, source) 393 - values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 394 - repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Website, repo.TopicStr(), repo.Source, 395 ) 396 if err != nil { 397 return fmt.Errorf("failed to insert repo: %w", err) ··· 401 if err := SubscribeLabel(tx, &models.RepoLabel{ 402 RepoAt: repo.RepoAt(), 403 LabelAt: syntax.ATURI(dl), 404 }); err != nil { 405 return fmt.Errorf("failed to subscribe to label: %w", err) 406 } ··· 431 if err != nil { 432 return nil, err 433 } 434 return GetRepoByAtUri(e, source) 435 } 436 ··· 438 var repos []models.Repo 439 440 rows, err := e.Query( 441 - `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source 442 from repos r 443 left join collaborators c on r.at_uri = c.repo_at 444 where (r.did = ? or c.subject_did = ?) ··· 458 var nullableDescription sql.NullString 459 var nullableWebsite sql.NullString 460 var nullableSource sql.NullString 461 462 - err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource) 463 if err != nil { 464 return nil, err 465 } ··· 467 if nullableDescription.Valid { 468 repo.Description = nullableDescription.String 469 } 470 471 if nullableSource.Valid { 472 repo.Source = nullableSource.String 473 } 474 475 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 476 if err != nil { ··· 496 var nullableWebsite sql.NullString 497 var nullableTopicStr sql.NullString 498 var nullableSource sql.NullString 499 500 row := e.QueryRow( 501 - `select id, did, name, knot, rkey, description, website, topics, created, source 502 from repos 503 where did = ? and name = ? and source is not null and source != ''`, 504 did, name, 505 ) 506 507 - err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &createdAt, &nullableSource) 508 if err != nil { 509 return nil, err 510 } ··· 524 if nullableSource.Valid { 525 repo.Source = nullableSource.String 526 } 527 528 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 529 if err != nil { ··· 535 return &repo, nil 536 } 537 538 func UpdateDescription(e Execer, repoAt, newDescription string) error { 539 _, err := e.Exec( 540 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) ··· 548 } 549 550 func SubscribeLabel(e Execer, rl *models.RepoLabel) error { 551 - query := `insert or ignore into repo_labels (repo_at, label_at) values (?, ?)` 552 553 - _, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String()) 554 return err 555 } 556 ··· 585 whereClause = " where " + strings.Join(conditions, " and ") 586 } 587 588 - query := fmt.Sprintf(`select id, repo_at, label_at from repo_labels %s`, whereClause) 589 590 rows, err := e.Query(query, args...) 591 if err != nil { ··· 596 var labels []models.RepoLabel 597 for rows.Next() { 598 var label models.RepoLabel 599 600 - err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt) 601 if err != nil { 602 return nil, err 603 } 604 605 labels = append(labels, label)
··· 46 website, 47 topics, 48 source, 49 + spindle, 50 + repo_did 51 from 52 repos r 53 %s ··· 65 for rows.Next() { 66 var repo models.Repo 67 var createdAt string 68 + var description, website, topicStr, source, spindle, repoDid sql.NullString 69 70 err := rows.Scan( 71 &repo.Id, ··· 79 &topicStr, 80 &source, 81 &spindle, 82 + &repoDid, 83 ) 84 if err != nil { 85 return nil, fmt.Errorf("failed to execute repo query: %w ", err) ··· 102 } 103 if spindle.Valid { 104 repo.Spindle = spindle.String 105 + } 106 + if repoDid.Valid { 107 + repo.RepoDid = repoDid.String 108 } 109 110 repo.RepoStats = &models.RepoStats{} ··· 189 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 190 } 191 192 + var repoDids []any 193 + repoDidToAt := make(map[string]syntax.ATURI) 194 + for atUri, r := range repoMap { 195 + if r.RepoDid != "" { 196 + repoDids = append(repoDids, r.RepoDid) 197 + repoDidToAt[r.RepoDid] = atUri 198 + } 199 + } 200 + 201 + didInClause := "''" 202 + if len(repoDids) > 0 { 203 + didInClause = strings.TrimSuffix(strings.Repeat("?, ", len(repoDids)), ", ") 204 + } 205 starCountQuery := fmt.Sprintf( 206 + `select coalesce(subject_did, subject_at) as key, count(1) 207 from stars 208 + where subject_at in (%s) or subject_did in (%s) 209 + group by key`, 210 + inClause, didInClause, 211 ) 212 + starArgs := append(append([]any{}, args...), repoDids...) 213 + rows, err = e.Query(starCountQuery, starArgs...) 214 if err != nil { 215 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 216 } 217 defer rows.Close() 218 219 for rows.Next() { 220 + var key string 221 var count int 222 + if err := rows.Scan(&key, &count); err != nil { 223 log.Println("err", "err", err) 224 continue 225 } 226 + if r, ok := repoMap[syntax.ATURI(key)]; ok { 227 r.RepoStats.StarCount = count 228 + } else if atUri, ok := repoDidToAt[key]; ok { 229 + if r, ok := repoMap[atUri]; ok { 230 + r.RepoStats.StarCount = count 231 + } 232 } 233 } 234 if err = rows.Err(); err != nil { ··· 374 var nullableDescription sql.NullString 375 var nullableWebsite sql.NullString 376 var nullableTopicStr sql.NullString 377 + var nullableRepoDid sql.NullString 378 + var nullableSource sql.NullString 379 + var nullableSpindle sql.NullString 380 381 + row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, source, spindle, repo_did from repos where at_uri = ?`, atUri) 382 383 var createdAt string 384 + if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableSource, &nullableSpindle, &nullableRepoDid); err != nil { 385 return nil, err 386 } 387 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 396 if nullableTopicStr.Valid { 397 repo.Topics = strings.Fields(nullableTopicStr.String) 398 } 399 + if nullableSource.Valid { 400 + repo.Source = nullableSource.String 401 + } 402 + if nullableSpindle.Valid { 403 + repo.Spindle = nullableSpindle.String 404 + } 405 + if nullableRepoDid.Valid { 406 + repo.RepoDid = nullableRepoDid.String 407 + } 408 409 return &repo, nil 410 } 411 412 func PutRepo(tx *sql.Tx, repo models.Repo) error { 413 + var repoDid *string 414 + if repo.RepoDid != "" { 415 + repoDid = &repo.RepoDid 416 + } 417 _, err := tx.Exec( 418 `update repos 419 + set knot = ?, description = ?, website = ?, topics = ?, repo_did = coalesce(?, repo_did) 420 where did = ? and rkey = ? 421 `, 422 + repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repoDid, repo.Did, repo.Rkey, 423 ) 424 return err 425 } 426 427 func AddRepo(tx *sql.Tx, repo *models.Repo) error { 428 + var repoDid *string 429 + if repo.RepoDid != "" { 430 + repoDid = &repo.RepoDid 431 + } 432 _, err := tx.Exec( 433 `insert into repos 434 + (did, name, knot, rkey, at_uri, description, website, topics, source, repo_did) 435 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 436 + repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.RepoAt().String(), repo.Description, repo.Website, repo.TopicStr(), repo.Source, repoDid, 437 ) 438 if err != nil { 439 return fmt.Errorf("failed to insert repo: %w", err) ··· 443 if err := SubscribeLabel(tx, &models.RepoLabel{ 444 RepoAt: repo.RepoAt(), 445 LabelAt: syntax.ATURI(dl), 446 + RepoDid: repo.RepoDid, 447 }); err != nil { 448 return fmt.Errorf("failed to subscribe to label: %w", err) 449 } ··· 474 if err != nil { 475 return nil, err 476 } 477 + if strings.HasPrefix(source, "did:") { 478 + return GetRepoByDid(e, source) 479 + } 480 return GetRepoByAtUri(e, source) 481 } 482 ··· 484 var repos []models.Repo 485 486 rows, err := e.Query( 487 + `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source, r.repo_did 488 from repos r 489 left join collaborators c on r.at_uri = c.repo_at 490 where (r.did = ? or c.subject_did = ?) ··· 504 var nullableDescription sql.NullString 505 var nullableWebsite sql.NullString 506 var nullableSource sql.NullString 507 + var nullableRepoDid sql.NullString 508 509 + err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource, &nullableRepoDid) 510 if err != nil { 511 return nil, err 512 } ··· 514 if nullableDescription.Valid { 515 repo.Description = nullableDescription.String 516 } 517 + if nullableWebsite.Valid { 518 + repo.Website = nullableWebsite.String 519 + } 520 521 if nullableSource.Valid { 522 repo.Source = nullableSource.String 523 } 524 + if nullableRepoDid.Valid { 525 + repo.RepoDid = nullableRepoDid.String 526 + } 527 528 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 529 if err != nil { ··· 549 var nullableWebsite sql.NullString 550 var nullableTopicStr sql.NullString 551 var nullableSource sql.NullString 552 + var nullableRepoDid sql.NullString 553 554 row := e.QueryRow( 555 + `select id, did, name, knot, rkey, description, website, topics, created, source, repo_did 556 from repos 557 where did = ? and name = ? and source is not null and source != ''`, 558 did, name, 559 ) 560 561 + err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &createdAt, &nullableSource, &nullableRepoDid) 562 if err != nil { 563 return nil, err 564 } ··· 578 if nullableSource.Valid { 579 repo.Source = nullableSource.String 580 } 581 + if nullableRepoDid.Valid { 582 + repo.RepoDid = nullableRepoDid.String 583 + } 584 585 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 586 if err != nil { ··· 592 return &repo, nil 593 } 594 595 + func GetRepoByDid(e Execer, repoDid string) (*models.Repo, error) { 596 + return GetRepo(e, orm.FilterEq("repo_did", repoDid)) 597 + } 598 + 599 + func EnqueuePdsRewrite(e Execer, userDid, repoDid, recordNsid, recordRkey, oldRepoAt string) error { 600 + _, err := e.Exec( 601 + `INSERT OR IGNORE INTO pds_rewrite_status 602 + (user_did, repo_did, record_nsid, record_rkey, old_repo_at, status) 603 + VALUES (?, ?, ?, ?, ?, 'pending')`, 604 + userDid, repoDid, recordNsid, recordRkey, oldRepoAt, 605 + ) 606 + return err 607 + } 608 + 609 func UpdateDescription(e Execer, repoAt, newDescription string) error { 610 _, err := e.Exec( 611 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) ··· 619 } 620 621 func SubscribeLabel(e Execer, rl *models.RepoLabel) error { 622 + var repoDid *string 623 + if rl.RepoDid != "" { 624 + repoDid = &rl.RepoDid 625 + } 626 + query := `insert into repo_labels (repo_at, label_at, repo_did) 627 + values (?, ?, ?) 628 + on conflict(repo_at, label_at) do update set repo_did = coalesce(excluded.repo_did, repo_did)` 629 630 + _, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String(), repoDid) 631 return err 632 } 633 ··· 662 whereClause = " where " + strings.Join(conditions, " and ") 663 } 664 665 + query := fmt.Sprintf(`select id, repo_at, label_at, repo_did from repo_labels %s`, whereClause) 666 667 rows, err := e.Query(query, args...) 668 if err != nil { ··· 673 var labels []models.RepoLabel 674 for rows.Next() { 675 var label models.RepoLabel 676 + var labelRepoDid sql.NullString 677 678 + err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt, &labelRepoDid) 679 if err != nil { 680 return nil, err 681 + } 682 + if labelRepoDid.Valid { 683 + label.RepoDid = labelRepoDid.String 684 } 685 686 labels = append(labels, label)
+29 -5
appview/db/star.go
··· 15 ) 16 17 func AddStar(e Execer, star *models.Star) error { 18 - query := `insert or ignore into stars (did, subject_at, rkey) values (?, ?, ?)` 19 _, err := e.Exec( 20 query, 21 star.Did, 22 star.RepoAt.String(), 23 star.Rkey, 24 ) 25 return err 26 } ··· 28 // Get a star record 29 func GetStar(e Execer, did string, subjectAt syntax.ATURI) (*models.Star, error) { 30 query := ` 31 - select did, subject_at, created, rkey 32 from stars 33 where did = ? and subject_at = ?` 34 row := e.QueryRow(query, did, subjectAt) 35 36 var star models.Star 37 var created string 38 - err := row.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey) 39 if err != nil { 40 return nil, err 41 } 42 43 createdAtTime, err := time.Parse(time.RFC3339, created) ··· 73 return stars, nil 74 } 75 76 // getStarStatuses returns a map of repo URIs to star status for a given user 77 // This is an internal helper function to avoid N+1 queries 78 func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { ··· 153 } 154 155 repoQuery := fmt.Sprintf( 156 - `select did, subject_at, created, rkey 157 from stars 158 %s 159 order by created desc ··· 171 for rows.Next() { 172 var star models.Star 173 var created string 174 - err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey) 175 if err != nil { 176 return nil, err 177 } 178 179 star.Created = time.Now()
··· 15 ) 16 17 func AddStar(e Execer, star *models.Star) error { 18 + query := `insert or ignore into stars (did, subject_at, rkey, subject_did) values (?, ?, ?, ?)` 19 + var subjectDid *string 20 + if star.SubjectDid != "" { 21 + subjectDid = &star.SubjectDid 22 + } 23 _, err := e.Exec( 24 query, 25 star.Did, 26 star.RepoAt.String(), 27 star.Rkey, 28 + subjectDid, 29 ) 30 return err 31 } ··· 33 // Get a star record 34 func GetStar(e Execer, did string, subjectAt syntax.ATURI) (*models.Star, error) { 35 query := ` 36 + select did, subject_at, created, rkey, subject_did 37 from stars 38 where did = ? and subject_at = ?` 39 row := e.QueryRow(query, did, subjectAt) 40 41 var star models.Star 42 var created string 43 + var nullableSubjectDid sql.NullString 44 + err := row.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey, &nullableSubjectDid) 45 if err != nil { 46 return nil, err 47 + } 48 + if nullableSubjectDid.Valid { 49 + star.SubjectDid = nullableSubjectDid.String 50 } 51 52 createdAtTime, err := time.Parse(time.RFC3339, created) ··· 82 return stars, nil 83 } 84 85 + func GetStarCountByRepoDid(e Execer, repoDid string, repoAt syntax.ATURI) (int, error) { 86 + stars := 0 87 + err := e.QueryRow( 88 + `select count(did) from stars where subject_did = ? or subject_at = ?`, 89 + repoDid, repoAt.String()).Scan(&stars) 90 + if err != nil { 91 + return 0, err 92 + } 93 + return stars, nil 94 + } 95 + 96 // getStarStatuses returns a map of repo URIs to star status for a given user 97 // This is an internal helper function to avoid N+1 queries 98 func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { ··· 173 } 174 175 repoQuery := fmt.Sprintf( 176 + `select did, subject_at, created, rkey, subject_did 177 from stars 178 %s 179 order by created desc ··· 191 for rows.Next() { 192 var star models.Star 193 var created string 194 + var nullableSubjectDid sql.NullString 195 + err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey, &nullableSubjectDid) 196 if err != nil { 197 return nil, err 198 + } 199 + if nullableSubjectDid.Valid { 200 + star.SubjectDid = nullableSubjectDid.String 201 } 202 203 star.Created = time.Now()
+15 -5
appview/db/webhooks.go
··· 34 active, 35 events, 36 created_at, 37 - updated_at 38 from webhooks 39 %s 40 order by created_at desc ··· 50 for rows.Next() { 51 var wh models.Webhook 52 var createdAt, updatedAt, eventsStr string 53 - var secret sql.NullString 54 var active int 55 56 err := rows.Scan( ··· 62 &eventsStr, 63 &createdAt, 64 &updatedAt, 65 ) 66 if err != nil { 67 return nil, fmt.Errorf("failed to scan webhook: %w", err) ··· 80 } 81 if t, err := time.Parse(time.RFC3339, updatedAt); err == nil { 82 wh.UpdatedAt = t 83 } 84 85 webhooks = append(webhooks, wh) ··· 118 active = 1 119 } 120 121 result, err := e.Exec(` 122 - insert into webhooks (repo_at, url, secret, active, events) 123 - values (?, ?, ?, ?, ?) 124 - `, webhook.RepoAt.String(), webhook.Url, webhook.Secret, active, eventsStr) 125 126 if err != nil { 127 return fmt.Errorf("failed to insert webhook: %w", err)
··· 34 active, 35 events, 36 created_at, 37 + updated_at, 38 + repo_did 39 from webhooks 40 %s 41 order by created_at desc ··· 51 for rows.Next() { 52 var wh models.Webhook 53 var createdAt, updatedAt, eventsStr string 54 + var secret, whRepoDid sql.NullString 55 var active int 56 57 err := rows.Scan( ··· 63 &eventsStr, 64 &createdAt, 65 &updatedAt, 66 + &whRepoDid, 67 ) 68 if err != nil { 69 return nil, fmt.Errorf("failed to scan webhook: %w", err) ··· 82 } 83 if t, err := time.Parse(time.RFC3339, updatedAt); err == nil { 84 wh.UpdatedAt = t 85 + } 86 + if whRepoDid.Valid { 87 + wh.RepoDid = whRepoDid.String 88 } 89 90 webhooks = append(webhooks, wh) ··· 123 active = 1 124 } 125 126 + var repoDid *string 127 + if webhook.RepoDid != "" { 128 + repoDid = &webhook.RepoDid 129 + } 130 + 131 result, err := e.Exec(` 132 + insert into webhooks (repo_at, url, secret, active, events, repo_did) 133 + values (?, ?, ?, ?, ?, ?) 134 + `, webhook.RepoAt.String(), webhook.Url, webhook.Secret, active, eventsStr, repoDid) 135 136 if err != nil { 137 return fmt.Errorf("failed to insert webhook: %w", err)
+78 -18
appview/ingester.go
··· 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 "maps" ··· 116 return err 117 } 118 119 - subjectUri, err = syntax.ParseATURI(record.Subject) 120 - if err != nil { 121 - l.Error("invalid record", "err", err) 122 - return err 123 } 124 - err = db.AddStar(i.Db, &models.Star{ 125 - Did: did, 126 - RepoAt: subjectUri, 127 - Rkey: e.Commit.RKey, 128 - }) 129 case jmodels.CommitOperationDelete: 130 err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) 131 } ··· 220 return err 221 } 222 223 - repoAt, err := syntax.ParseATURI(record.Repo) 224 - if err != nil { 225 - return err 226 } 227 - 228 - repo, err := db.GetRepoByAtUri(i.Db, repoAt.String()) 229 - if err != nil { 230 - return err 231 } 232 233 - ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 234 if err != nil || !ok { 235 return err 236 } 237 238 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 239 if err != nil { 240 createdAt = time.Now() ··· 243 artifact := models.Artifact{ 244 Did: did, 245 Rkey: e.Commit.RKey, 246 - RepoAt: repoAt, 247 Tag: plumbing.Hash(record.Tag), 248 CreatedAt: createdAt, 249 BlobCid: cid.Cid(record.Artifact.Ref), ··· 822 823 issue := models.IssueFromRecord(did, rkey, record) 824 825 if err := i.Validator.ValidateIssue(&issue); err != nil { 826 return fmt.Errorf("failed to validate issue: %w", err) 827 } 828 829 tx, err := ddb.BeginTx(ctx, nil)
··· 2 3 import ( 4 "context" 5 + "database/sql" 6 "encoding/json" 7 + "errors" 8 "fmt" 9 "log/slog" 10 "maps" ··· 118 return err 119 } 120 121 + star := &models.Star{ 122 + Did: did, 123 + Rkey: e.Commit.RKey, 124 + } 125 + 126 + switch { 127 + case record.SubjectDid != nil: 128 + star.SubjectDid = *record.SubjectDid 129 + repo, repoErr := db.GetRepo(i.Db, orm.FilterEq("repo_did", *record.SubjectDid)) 130 + if repoErr == nil { 131 + subjectUri = repo.RepoAt() 132 + star.RepoAt = subjectUri 133 + } 134 + case record.Subject != nil: 135 + subjectUri, err = syntax.ParseATURI(*record.Subject) 136 + if err != nil { 137 + l.Error("invalid record", "err", err) 138 + return err 139 + } 140 + star.RepoAt = subjectUri 141 + repo, repoErr := db.GetRepoByAtUri(i.Db, subjectUri.String()) 142 + if repoErr == nil && repo.RepoDid != "" { 143 + star.SubjectDid = repo.RepoDid 144 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.FeedStarNSID, e.Commit.RKey, *record.Subject); enqErr != nil { 145 + l.Warn("failed to enqueue PDS rewrite for star", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 146 + } 147 + } 148 + default: 149 + l.Error("star record has neither subject nor subjectDid") 150 + return fmt.Errorf("star record has neither subject nor subjectDid") 151 } 152 + err = db.AddStar(i.Db, star) 153 case jmodels.CommitOperationDelete: 154 err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) 155 } ··· 244 return err 245 } 246 247 + var repo *models.Repo 248 + if record.RepoDid != nil && *record.RepoDid != "" { 249 + repo, err = db.GetRepoByDid(i.Db, *record.RepoDid) 250 + if err != nil && !errors.Is(err, sql.ErrNoRows) { 251 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, err) 252 + } 253 } 254 + if repo == nil && record.Repo != nil { 255 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 256 + if parseErr != nil { 257 + return parseErr 258 + } 259 + repo, err = db.GetRepoByAtUri(i.Db, repoAt.String()) 260 + if err != nil { 261 + return err 262 + } 263 + } 264 + if repo == nil { 265 + return fmt.Errorf("artifact record has neither valid repoDid nor repo field") 266 } 267 268 + ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.RepoIdentifier(), "repo:push") 269 if err != nil || !ok { 270 return err 271 } 272 273 + repoDid := repo.RepoDid 274 + if repoDid == "" && record.RepoDid != nil { 275 + repoDid = *record.RepoDid 276 + } 277 + if repoDid != "" && (record.RepoDid == nil || *record.RepoDid == "") && record.Repo != nil { 278 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repoDid, tangled.RepoArtifactNSID, e.Commit.RKey, *record.Repo); enqErr != nil { 279 + l.Warn("failed to enqueue PDS rewrite for artifact", "err", enqErr, "did", did, "repoDid", repoDid) 280 + } 281 + } 282 + 283 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 284 if err != nil { 285 createdAt = time.Now() ··· 288 artifact := models.Artifact{ 289 Did: did, 290 Rkey: e.Commit.RKey, 291 + RepoAt: repo.RepoAt(), 292 + RepoDid: repoDid, 293 Tag: plumbing.Hash(record.Tag), 294 CreatedAt: createdAt, 295 BlobCid: cid.Cid(record.Artifact.Ref), ··· 868 869 issue := models.IssueFromRecord(did, rkey, record) 870 871 + if issue.RepoDid == "" && issue.RepoAt == "" { 872 + return fmt.Errorf("issue record has neither repo nor repoDid") 873 + } 874 + 875 if err := i.Validator.ValidateIssue(&issue); err != nil { 876 return fmt.Errorf("failed to validate issue: %w", err) 877 + } 878 + 879 + if issue.RepoDid == "" && record.Repo != nil { 880 + repo, repoErr := db.GetRepoByAtUri(i.Db, *record.Repo) 881 + if repoErr == nil && repo.RepoDid != "" { 882 + issue.RepoDid = repo.RepoDid 883 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.RepoIssueNSID, rkey, *record.Repo); enqErr != nil { 884 + l.Warn("failed to enqueue PDS rewrite for issue", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 885 + } 886 + } 887 } 888 889 tx, err := ddb.BeginTx(ctx, nil)
+3 -2
appview/issues/issues.go
··· 306 return 307 } 308 309 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 310 isRepoOwner := roles.IsOwner() 311 isCollaborator := roles.IsCollaborator() 312 isIssueOwner := user.Active.Did == issue.Did ··· 354 return 355 } 356 357 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 358 isRepoOwner := roles.IsOwner() 359 isCollaborator := roles.IsCollaborator() 360 isIssueOwner := user.Active.Did == issue.Did ··· 1011 1012 issue := &models.Issue{ 1013 RepoAt: f.RepoAt(), 1014 Rkey: tid.TID(), 1015 Title: r.FormValue("title"), 1016 Body: body,
··· 306 return 307 } 308 309 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 310 isRepoOwner := roles.IsOwner() 311 isCollaborator := roles.IsCollaborator() 312 isIssueOwner := user.Active.Did == issue.Did ··· 354 return 355 } 356 357 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 358 isRepoOwner := roles.IsOwner() 359 isCollaborator := roles.IsCollaborator() 360 isIssueOwner := user.Active.Did == issue.Did ··· 1011 1012 issue := &models.Issue{ 1013 RepoAt: f.RepoAt(), 1014 + RepoDid: f.RepoDid, 1015 Rkey: tid.TID(), 1016 Title: r.FormValue("title"), 1017 Body: body,
+29 -5
appview/middleware/middleware.go
··· 17 "tangled.org/core/appview/pages" 18 "tangled.org/core/appview/pagination" 19 "tangled.org/core/appview/reporesolver" 20 "tangled.org/core/idresolver" 21 "tangled.org/core/orm" 22 "tangled.org/core/rbac" ··· 161 return 162 } 163 164 - ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 165 if err != nil || !ok { 166 - log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo()) 167 http.Error(w, "Forbiden", http.StatusUnauthorized) 168 return 169 } ··· 188 189 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 190 if err != nil { 191 - // invalid did or handle 192 log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 193 mw.pages.Error404(w) 194 return ··· 226 return 227 } 228 229 ctx := context.WithValue(req.Context(), "repo", repo) 230 next.ServeHTTP(w, req.WithContext(ctx)) 231 }) ··· 334 335 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 336 if r.URL.Query().Get("go-get") == "1" { 337 html := fmt.Sprintf( 338 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/> 339 <meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`, 340 - fullName, fullName, 341 - fullName, fullName, 342 ) 343 w.Header().Set("Content-Type", "text/html") 344 w.Write([]byte(html))
··· 17 "tangled.org/core/appview/pages" 18 "tangled.org/core/appview/pagination" 19 "tangled.org/core/appview/reporesolver" 20 + "tangled.org/core/appview/state/userutil" 21 "tangled.org/core/idresolver" 22 "tangled.org/core/orm" 23 "tangled.org/core/rbac" ··· 162 return 163 } 164 165 + ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.RepoIdentifier(), requiredPerm) 166 if err != nil || !ok { 167 + log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.RepoIdentifier()) 168 http.Error(w, "Forbiden", http.StatusUnauthorized) 169 return 170 } ··· 189 190 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 191 if err != nil { 192 log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 193 mw.pages.Error404(w) 194 return ··· 226 return 227 } 228 229 + if repo.RepoDid != "" && req.Context().Value("repoDidCanonical") == nil { 230 + gitPaths := []string{"/info/refs", "/git-upload-pack", "/git-receive-pack", "/git-upload-archive"} 231 + user := chi.URLParam(req, "user") 232 + repoParam := chi.URLParam(req, "repo") 233 + remaining := strings.TrimPrefix(req.URL.Path, "/"+user+"/"+repoParam) 234 + 235 + isGitPath := slices.ContainsFunc(gitPaths, func(p string) bool { 236 + return strings.HasSuffix(remaining, p) 237 + }) 238 + 239 + if !isGitPath && req.URL.Query().Get("go-get") != "1" { 240 + target := "/" + repo.RepoDid + remaining 241 + if req.URL.RawQuery != "" { 242 + target += "?" + req.URL.RawQuery 243 + } 244 + http.Redirect(w, req, target, http.StatusFound) 245 + return 246 + } 247 + } 248 + 249 ctx := context.WithValue(req.Context(), "repo", repo) 250 next.ServeHTTP(w, req.WithContext(ctx)) 251 }) ··· 354 355 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 356 if r.URL.Query().Get("go-get") == "1" { 357 + modulePath := userutil.FlattenDid(fullName) 358 + if strings.Contains(modulePath, ":") { 359 + modulePath = userutil.FlattenDid(f.Did) + "/" + f.Name 360 + } 361 html := fmt.Sprintf( 362 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/> 363 <meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`, 364 + modulePath, fullName, 365 + modulePath, fullName, 366 ) 367 w.Header().Set("Content-Type", "text/html") 368 w.Write([]byte(html))
+1
appview/models/artifact.go
··· 16 Rkey string 17 18 RepoAt syntax.ATURI 19 Tag plumbing.Hash 20 CreatedAt time.Time 21
··· 16 Rkey string 17 18 RepoAt syntax.ATURI 19 + RepoDid string 20 Tag plumbing.Hash 21 CreatedAt time.Time 22
+1
appview/models/collaborator.go
··· 15 // content 16 SubjectDid syntax.DID 17 RepoAt syntax.ATURI 18 19 // meta 20 Created time.Time
··· 15 // content 16 SubjectDid syntax.DID 17 RepoAt syntax.ATURI 18 + RepoDid string 19 20 // meta 21 Created time.Time
+23 -2
appview/models/issue.go
··· 14 Did string 15 Rkey string 16 RepoAt syntax.ATURI 17 IssueId int 18 Created time.Time 19 Edited *time.Time ··· 44 for i, uri := range i.References { 45 references[i] = string(uri) 46 } 47 return tangled.RepoIssue{ 48 - Repo: i.RepoAt.String(), 49 Title: i.Title, 50 Body: &i.Body, 51 Mentions: mentions, ··· 161 body = *record.Body 162 } 163 164 return Issue{ 165 - RepoAt: syntax.ATURI(record.Repo), 166 Did: did, 167 Rkey: rkey, 168 Created: created,
··· 14 Did string 15 Rkey string 16 RepoAt syntax.ATURI 17 + RepoDid string 18 IssueId int 19 Created time.Time 20 Edited *time.Time ··· 45 for i, uri := range i.References { 46 references[i] = string(uri) 47 } 48 + var repo *string 49 + var repoDid *string 50 + if i.RepoDid != "" { 51 + repoDid = &i.RepoDid 52 + } else { 53 + s := i.RepoAt.String() 54 + repo = &s 55 + } 56 return tangled.RepoIssue{ 57 + Repo: repo, 58 + RepoDid: repoDid, 59 Title: i.Title, 60 Body: &i.Body, 61 Mentions: mentions, ··· 171 body = *record.Body 172 } 173 174 + var repoAt syntax.ATURI 175 + if record.Repo != nil { 176 + repoAt = syntax.ATURI(*record.Repo) 177 + } 178 + 179 + repoDid := "" 180 + if record.RepoDid != nil { 181 + repoDid = *record.RepoDid 182 + } 183 + 184 return Issue{ 185 + RepoAt: repoAt, 186 + RepoDid: repoDid, 187 Did: did, 188 Rkey: rkey, 189 Created: created,
+1
appview/models/language.go
··· 7 type RepoLanguage struct { 8 Id int64 9 RepoAt syntax.ATURI 10 Ref string 11 IsDefaultRef bool 12 Language string
··· 7 type RepoLanguage struct { 8 Id int64 9 RepoAt syntax.ATURI 10 + RepoDid string 11 Ref string 12 IsDefaultRef bool 13 Language string
+1
appview/models/pipeline.go
··· 19 Knot string 20 RepoOwner syntax.DID 21 RepoName string 22 TriggerId int 23 Sha string 24 Created time.Time
··· 19 Knot string 20 RepoOwner syntax.DID 21 RepoName string 22 + RepoDid string 23 TriggerId int 24 Sha string 25 Created time.Time
+19 -5
appview/models/pull.go
··· 59 RepoAt syntax.ATURI 60 OwnerDid string 61 Rkey string 62 63 // content 64 Title string ··· 90 source = &tangled.RepoPull_Source{} 91 source.Branch = p.PullSource.Branch 92 source.Sha = p.LatestSha() 93 - if p.PullSource.RepoAt != nil { 94 s := p.PullSource.RepoAt.String() 95 source.Repo = &s 96 } ··· 104 references[i] = string(uri) 105 } 106 107 record := tangled.RepoPull{ 108 Title: p.Title, 109 Body: &p.Body, ··· 111 References: references, 112 CreatedAt: p.Created.Format(time.RFC3339), 113 Target: &tangled.RepoPull_Target{ 114 - Repo: p.RepoAt.String(), 115 - Branch: p.TargetBranch, 116 }, 117 Source: source, 118 } ··· 120 } 121 122 type PullSource struct { 123 - Branch string 124 - RepoAt *syntax.ATURI 125 126 // optionally populate this for reverse mappings 127 Repo *Repo ··· 155 RepoAt string 156 OwnerDid string 157 CommentAt string 158 159 // content 160 Body string
··· 59 RepoAt syntax.ATURI 60 OwnerDid string 61 Rkey string 62 + RepoDid string 63 64 // content 65 Title string ··· 91 source = &tangled.RepoPull_Source{} 92 source.Branch = p.PullSource.Branch 93 source.Sha = p.LatestSha() 94 + if p.PullSource.RepoDid != "" { 95 + source.RepoDid = &p.PullSource.RepoDid 96 + } else if p.PullSource.RepoAt != nil { 97 s := p.PullSource.RepoAt.String() 98 source.Repo = &s 99 } ··· 107 references[i] = string(uri) 108 } 109 110 + var targetRepo *string 111 + var targetRepoDid *string 112 + if p.RepoDid != "" { 113 + targetRepoDid = &p.RepoDid 114 + } else { 115 + s := p.RepoAt.String() 116 + targetRepo = &s 117 + } 118 record := tangled.RepoPull{ 119 Title: p.Title, 120 Body: &p.Body, ··· 122 References: references, 123 CreatedAt: p.Created.Format(time.RFC3339), 124 Target: &tangled.RepoPull_Target{ 125 + Repo: targetRepo, 126 + RepoDid: targetRepoDid, 127 + Branch: p.TargetBranch, 128 }, 129 Source: source, 130 } ··· 132 } 133 134 type PullSource struct { 135 + Branch string 136 + RepoAt *syntax.ATURI 137 + RepoDid string 138 139 // optionally populate this for reverse mappings 140 Repo *Repo ··· 168 RepoAt string 169 OwnerDid string 170 CommentAt string 171 + RepoDid string 172 173 // content 174 Body string
+12 -1
appview/models/repo.go
··· 22 Topics []string 23 Spindle string 24 Labels []string 25 26 // optionally, populate this when querying for reverse mappings 27 RepoStats *RepoStats ··· 49 website = &r.Website 50 } 51 52 return tangled.Repo{ 53 Knot: r.Knot, 54 Name: r.Name, ··· 59 Source: source, 60 Spindle: spindle, 61 Labels: r.Labels, 62 } 63 } 64 ··· 66 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 67 } 68 69 - func (r Repo) DidSlashRepo() string { 70 p, _ := securejoin.SecureJoin(r.Did, r.Name) 71 return p 72 } ··· 98 Id int64 99 RepoAt syntax.ATURI 100 LabelAt syntax.ATURI 101 } 102 103 type RepoGroup struct {
··· 22 Topics []string 23 Spindle string 24 Labels []string 25 + RepoDid string 26 27 // optionally, populate this when querying for reverse mappings 28 RepoStats *RepoStats ··· 50 website = &r.Website 51 } 52 53 + var repoDid *string 54 + if r.RepoDid != "" { 55 + repoDid = &r.RepoDid 56 + } 57 + 58 return tangled.Repo{ 59 Knot: r.Knot, 60 Name: r.Name, ··· 65 Source: source, 66 Spindle: spindle, 67 Labels: r.Labels, 68 + RepoDid: repoDid, 69 } 70 } 71 ··· 73 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 74 } 75 76 + func (r Repo) RepoIdentifier() string { 77 + if r.RepoDid != "" { 78 + return r.RepoDid 79 + } 80 p, _ := securejoin.SecureJoin(r.Did, r.Name) 81 return p 82 } ··· 108 Id int64 109 RepoAt syntax.ATURI 110 LabelAt syntax.ATURI 111 + RepoDid string 112 } 113 114 type RepoGroup struct {
+5 -4
appview/models/star.go
··· 7 ) 8 9 type Star struct { 10 - Did string 11 - RepoAt syntax.ATURI 12 - Created time.Time 13 - Rkey string 14 } 15 16 // RepoStar is used for reverse mapping to repos
··· 7 ) 8 9 type Star struct { 10 + Did string 11 + RepoAt syntax.ATURI 12 + SubjectDid string 13 + Created time.Time 14 + Rkey string 15 } 16 17 // RepoStar is used for reverse mapping to repos
+1
appview/models/webhook.go
··· 16 type Webhook struct { 17 Id int64 18 RepoAt syntax.ATURI 19 Url string 20 Secret string 21 Active bool
··· 16 type Webhook struct { 17 Id int64 18 RepoAt syntax.ATURI 19 + RepoDid string 20 Url string 21 Secret string 22 Active bool
+5 -2
appview/pages/funcmap.go
··· 78 return identity.Handle.String() 79 }, 80 "ownerSlashRepo": func(repo *models.Repo) string { 81 ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did) 82 if err != nil { 83 - return repo.DidSlashRepo() 84 } 85 handle := ownerId.Handle 86 if handle != "" && !handle.IsInvalidHandle() { 87 return string(handle) + "/" + repo.Name 88 } 89 - return repo.DidSlashRepo() 90 }, 91 "truncateAt30": func(s string) string { 92 if len(s) <= 30 {
··· 78 return identity.Handle.String() 79 }, 80 "ownerSlashRepo": func(repo *models.Repo) string { 81 + if repo.RepoDid != "" { 82 + return repo.RepoDid 83 + } 84 ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did) 85 if err != nil { 86 + return repo.RepoIdentifier() 87 } 88 handle := ownerId.Handle 89 if handle != "" && !handle.IsInvalidHandle() { 90 return string(handle) + "/" + repo.Name 91 } 92 + return repo.RepoIdentifier() 93 }, 94 "truncateAt30": func(s string) string { 95 if len(s) <= 30 {
+7
appview/pages/repoinfo/repoinfo.go
··· 20 } 21 22 func (r RepoInfo) FullName() string { 23 return path.Join(r.owner(), r.Name) 24 } 25 ··· 32 } 33 34 func (r RepoInfo) FullNameWithoutAt() string { 35 return path.Join(r.ownerWithoutAt(), r.Name) 36 } 37 ··· 59 Rkey string 60 OwnerDid string 61 OwnerHandle string 62 Description string 63 Website string 64 Topics []string
··· 20 } 21 22 func (r RepoInfo) FullName() string { 23 + if r.RepoDid != "" { 24 + return r.RepoDid 25 + } 26 return path.Join(r.owner(), r.Name) 27 } 28 ··· 35 } 36 37 func (r RepoInfo) FullNameWithoutAt() string { 38 + if r.RepoDid != "" { 39 + return userutil.FlattenDid(r.RepoDid) 40 + } 41 return path.Join(r.ownerWithoutAt(), r.Name) 42 } 43 ··· 65 Rkey string 66 OwnerDid string 67 OwnerHandle string 68 + RepoDid string 69 Description string 70 Website string 71 Topics []string
+21
appview/pages/templates/repo/fork.html
··· 37 <p class="text-sm text-gray-500 dark:text-gray-400">A knot hosts repository data. <a href="/settings/knots" class="underline">Learn how to register your own knot.</a></p> 38 </fieldset> 39 40 <div class="space-y-2"> 41 <button type="submit" class="btn-create flex items-center gap-2"> 42 {{ i "git-fork" "w-4 h-4" }}
··· 37 <p class="text-sm text-gray-500 dark:text-gray-400">A knot hosts repository data. <a href="/settings/knots" class="underline">Learn how to register your own knot.</a></p> 38 </fieldset> 39 40 + <fieldset class="space-y-3"> 41 + <details> 42 + <summary class="dark:text-white cursor-pointer select-none">Bring your own DID</summary> 43 + <div class="mt-2"> 44 + <input 45 + type="text" 46 + id="repo_did" 47 + name="repo_did" 48 + class="w-full p-2 border rounded bg-gray-100 dark:bg-gray-700 dark:text-white dark:border-gray-600" 49 + placeholder="did:web:example.com" 50 + /> 51 + <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"> 52 + Provide a <code>did:web</code> you control to use as this fork's identity. 53 + You must serve a DID doc on your domain with an <code>atproto_pds</code> service 54 + endpoint pointing to the selected knot. If left empty, a <code>did:plc</code> will be 55 + automatically created for you! 56 + </p> 57 + </div> 58 + </details> 59 + </fieldset> 60 + 61 <div class="space-y-2"> 62 <button type="submit" class="btn-create flex items-center gap-2"> 63 {{ i "git-fork" "w-4 h-4" }}
+26
appview/pages/templates/repo/new.html
··· 70 <div class="space-y-2"> 71 {{ template "defaultBranch" . }} 72 {{ template "knot" . }} 73 </div> 74 </div> 75 </div> ··· 168 A knot hosts repository data and handles Git operations. 169 You can also <a href="/settings/knots" class="underline">register your own knot</a>. 170 </p> 171 </div> 172 {{ end }} 173
··· 70 <div class="space-y-2"> 71 {{ template "defaultBranch" . }} 72 {{ template "knot" . }} 73 + {{ template "repoDid" . }} 74 </div> 75 </div> 76 </div> ··· 169 A knot hosts repository data and handles Git operations. 170 You can also <a href="/settings/knots" class="underline">register your own knot</a>. 171 </p> 172 + </div> 173 + {{ end }} 174 + 175 + {{ define "repoDid" }} 176 + <div> 177 + <details> 178 + <summary class="text-sm font-bold uppercase dark:text-white mb-1 cursor-pointer select-none"> 179 + Bring your own DID 180 + </summary> 181 + <div class="mt-2"> 182 + <input 183 + type="text" 184 + id="repo_did" 185 + name="repo_did" 186 + class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 border border-gray-300 rounded px-3 py-2" 187 + placeholder="did:web:example.com" 188 + /> 189 + <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"> 190 + Provide a <code>did:web</code> you control to use as this repo's identity. 191 + You must serve a DID doc on your domain with an <code>atproto_pds</code> service 192 + endpoint pointing to the selected knot. If left empty, a <code>did:plc</code> will be 193 + automatically created for you! 194 + </p> 195 + </div> 196 + </details> 197 </div> 198 {{ end }} 199
+44 -45
appview/pulls/pulls.go
··· 406 } 407 408 // user can only delete branch if they are a collaborator in the repo that the branch belongs to 409 - perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 410 if !slices.Contains(perms, "repo:push") { 411 return nil 412 } ··· 420 Host: host, 421 } 422 423 - resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, fmt.Sprintf("%s/%s", repo.Did, repo.Name)) 424 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 425 return nil 426 } ··· 436 return pages.Unknown 437 } 438 439 - var knot, ownerDid, repoName string 440 - 441 if pull.PullSource.RepoAt != nil { 442 - // fork-based pulls 443 - sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 444 if err != nil { 445 log.Println("failed to get source repo", err) 446 return pages.Unknown 447 } 448 - 449 - knot = sourceRepo.Knot 450 - ownerDid = sourceRepo.Did 451 - repoName = sourceRepo.Name 452 } else { 453 - // pulls within the same repo 454 - knot = repo.Knot 455 - ownerDid = repo.Did 456 - repoName = repo.Name 457 } 458 459 scheme := "http" 460 if !s.config.Core.Dev { 461 scheme = "https" 462 } 463 - host := fmt.Sprintf("%s://%s", scheme, knot) 464 xrpcc := &indigoxrpc.Client{ 465 Host: host, 466 } 467 468 - didSlashName := fmt.Sprintf("%s/%s", ownerDid, repoName) 469 - branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, didSlashName) 470 if err != nil { 471 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 472 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 863 comment := &models.PullComment{ 864 OwnerDid: user.Active.Did, 865 RepoAt: f.RepoAt().String(), 866 PullId: pull.PullId, 867 Body: body, 868 CommentAt: atResp.Uri, ··· 913 Host: host, 914 } 915 916 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 917 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 918 if err != nil { 919 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 920 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 963 } 964 965 // Determine PR type based on input parameters 966 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 967 isPushAllowed := roles.IsPushAllowed() 968 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 969 isForkBased := fromFork != "" && sourceBranch != "" ··· 1079 Host: host, 1080 } 1081 1082 - didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name) 1083 - xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, didSlashRepo, targetBranch, sourceBranch) 1084 if err != nil { 1085 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1086 log.Println("failed to call XRPC repo.compare", xrpcerr) ··· 1189 Host: forkHost, 1190 } 1191 1192 - forkRepoId := fmt.Sprintf("%s/%s", fork.Did, fork.Name) 1193 - forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, forkRepoId, hiddenRef, sourceBranch) 1194 if err != nil { 1195 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1196 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) ··· 1223 forkAtUriStr := forkAtUri.String() 1224 1225 pullSource := &models.PullSource{ 1226 - Branch: sourceBranch, 1227 - RepoAt: &forkAtUri, 1228 } 1229 recordPullSource := &tangled.RepoPull_Source{ 1230 Branch: sourceBranch, 1231 - Repo: &forkAtUriStr, 1232 Sha: sourceRev, 1233 } 1234 1235 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1236 } ··· 1313 TargetBranch: targetBranch, 1314 OwnerDid: user.Active.Did, 1315 RepoAt: repo.RepoAt(), 1316 Rkey: rkey, 1317 Mentions: mentions, 1318 References: references, ··· 1347 Rkey: rkey, 1348 Record: &lexutil.LexiconTypeDecoder{ 1349 Val: &tangled.RepoPull{ 1350 - Title: title, 1351 - Target: &tangled.RepoPull_Target{ 1352 - Repo: string(repo.RepoAt()), 1353 - Branch: targetBranch, 1354 - }, 1355 PatchBlob: blob.Blob, 1356 Source: recordPullSource, 1357 CreatedAt: time.Now().Format(time.RFC3339), ··· 1544 Host: host, 1545 } 1546 1547 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1548 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1549 if err != nil { 1550 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1551 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 1631 Host: sourceHost, 1632 } 1633 1634 - sourceRepo := fmt.Sprintf("%s/%s", forkOwnerDid, repo.Name) 1635 - sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, sourceRepo) 1636 if err != nil { 1637 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1638 log.Println("failed to call XRPC repo.branches for source", xrpcerr) ··· 1660 Host: targetHost, 1661 } 1662 1663 - targetRepo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1664 - targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo) 1665 if err != nil { 1666 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1667 log.Println("failed to call XRPC repo.branches for target", xrpcerr) ··· 1771 return 1772 } 1773 1774 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 1775 if !roles.IsPushAllowed() { 1776 log.Println("unauthorized user") 1777 w.WriteHeader(http.StatusUnauthorized) ··· 1787 Host: host, 1788 } 1789 1790 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1791 - xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch) 1792 if err != nil { 1793 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1794 log.Println("failed to call XRPC repo.compare", xrpcerr) ··· 1881 forkScheme = "https" 1882 } 1883 forkHost := fmt.Sprintf("%s://%s", forkScheme, forkRepo.Knot) 1884 - forkRepoId := fmt.Sprintf("%s/%s", forkRepo.Did, forkRepo.Name) 1885 - forkXrpcBytes, err := tangled.RepoCompare(r.Context(), &indigoxrpc.Client{Host: forkHost}, forkRepoId, hiddenRef, pull.PullSource.Branch) 1886 if err != nil { 1887 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1888 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) ··· 2360 } 2361 2362 // auth filter: only owner or collaborators can close 2363 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2364 isOwner := roles.IsOwner() 2365 isCollaborator := roles.IsCollaborator() 2366 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2434 } 2435 2436 // auth filter: only owner or collaborators can close 2437 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2438 isOwner := roles.IsOwner() 2439 isCollaborator := roles.IsCollaborator() 2440 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2528 TargetBranch: targetBranch, 2529 OwnerDid: user.Active.Did, 2530 RepoAt: repo.RepoAt(), 2531 Rkey: rkey, 2532 Mentions: mentions, 2533 References: references, ··· 2559 } 2560 2561 func ptrPullState(s models.PullState) *models.PullState { return &s }
··· 406 } 407 408 // user can only delete branch if they are a collaborator in the repo that the branch belongs to 409 + perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.RepoIdentifier()) 410 if !slices.Contains(perms, "repo:push") { 411 return nil 412 } ··· 420 Host: host, 421 } 422 423 + resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, repo.RepoIdentifier()) 424 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 425 return nil 426 } ··· 436 return pages.Unknown 437 } 438 439 + var sourceRepo *models.Repo 440 if pull.PullSource.RepoAt != nil { 441 + var err error 442 + sourceRepo, err = db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 443 if err != nil { 444 log.Println("failed to get source repo", err) 445 return pages.Unknown 446 } 447 } else { 448 + sourceRepo = repo 449 } 450 451 scheme := "http" 452 if !s.config.Core.Dev { 453 scheme = "https" 454 } 455 + host := fmt.Sprintf("%s://%s", scheme, sourceRepo.Knot) 456 xrpcc := &indigoxrpc.Client{ 457 Host: host, 458 } 459 460 + branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepo.RepoIdentifier()) 461 if err != nil { 462 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 463 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 854 comment := &models.PullComment{ 855 OwnerDid: user.Active.Did, 856 RepoAt: f.RepoAt().String(), 857 + RepoDid: f.RepoDid, 858 PullId: pull.PullId, 859 Body: body, 860 CommentAt: atResp.Uri, ··· 905 Host: host, 906 } 907 908 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 909 if err != nil { 910 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 911 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 954 } 955 956 // Determine PR type based on input parameters 957 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 958 isPushAllowed := roles.IsPushAllowed() 959 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 960 isForkBased := fromFork != "" && sourceBranch != "" ··· 1070 Host: host, 1071 } 1072 1073 + xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo.RepoIdentifier(), targetBranch, sourceBranch) 1074 if err != nil { 1075 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1076 log.Println("failed to call XRPC repo.compare", xrpcerr) ··· 1179 Host: forkHost, 1180 } 1181 1182 + forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, fork.RepoIdentifier(), hiddenRef, sourceBranch) 1183 if err != nil { 1184 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1185 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) ··· 1212 forkAtUriStr := forkAtUri.String() 1213 1214 pullSource := &models.PullSource{ 1215 + Branch: sourceBranch, 1216 + RepoAt: &forkAtUri, 1217 + RepoDid: fork.RepoDid, 1218 } 1219 recordPullSource := &tangled.RepoPull_Source{ 1220 Branch: sourceBranch, 1221 Sha: sourceRev, 1222 } 1223 + if fork.RepoDid != "" { 1224 + recordPullSource.RepoDid = &fork.RepoDid 1225 + } else { 1226 + recordPullSource.Repo = &forkAtUriStr 1227 + } 1228 1229 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1230 } ··· 1307 TargetBranch: targetBranch, 1308 OwnerDid: user.Active.Did, 1309 RepoAt: repo.RepoAt(), 1310 + RepoDid: repo.RepoDid, 1311 Rkey: rkey, 1312 Mentions: mentions, 1313 References: references, ··· 1342 Rkey: rkey, 1343 Record: &lexutil.LexiconTypeDecoder{ 1344 Val: &tangled.RepoPull{ 1345 + Title: title, 1346 + Target: repoPullTarget(repo, targetBranch), 1347 PatchBlob: blob.Blob, 1348 Source: recordPullSource, 1349 CreatedAt: time.Now().Format(time.RFC3339), ··· 1536 Host: host, 1537 } 1538 1539 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 1540 if err != nil { 1541 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1542 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 1622 Host: sourceHost, 1623 } 1624 1625 + sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, repo.RepoIdentifier()) 1626 if err != nil { 1627 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1628 log.Println("failed to call XRPC repo.branches for source", xrpcerr) ··· 1650 Host: targetHost, 1651 } 1652 1653 + targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, f.RepoIdentifier()) 1654 if err != nil { 1655 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1656 log.Println("failed to call XRPC repo.branches for target", xrpcerr) ··· 1760 return 1761 } 1762 1763 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 1764 if !roles.IsPushAllowed() { 1765 log.Println("unauthorized user") 1766 w.WriteHeader(http.StatusUnauthorized) ··· 1776 Host: host, 1777 } 1778 1779 + xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, f.RepoIdentifier(), pull.TargetBranch, pull.PullSource.Branch) 1780 if err != nil { 1781 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1782 log.Println("failed to call XRPC repo.compare", xrpcerr) ··· 1869 forkScheme = "https" 1870 } 1871 forkHost := fmt.Sprintf("%s://%s", forkScheme, forkRepo.Knot) 1872 + forkXrpcBytes, err := tangled.RepoCompare(r.Context(), &indigoxrpc.Client{Host: forkHost}, forkRepo.RepoIdentifier(), hiddenRef, pull.PullSource.Branch) 1873 if err != nil { 1874 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1875 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) ··· 2347 } 2348 2349 // auth filter: only owner or collaborators can close 2350 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 2351 isOwner := roles.IsOwner() 2352 isCollaborator := roles.IsCollaborator() 2353 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2421 } 2422 2423 // auth filter: only owner or collaborators can close 2424 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 2425 isOwner := roles.IsOwner() 2426 isCollaborator := roles.IsCollaborator() 2427 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2515 TargetBranch: targetBranch, 2516 OwnerDid: user.Active.Did, 2517 RepoAt: repo.RepoAt(), 2518 + RepoDid: repo.RepoDid, 2519 Rkey: rkey, 2520 Mentions: mentions, 2521 References: references, ··· 2547 } 2548 2549 func ptrPullState(s models.PullState) *models.PullState { return &s } 2550 + 2551 + func repoPullTarget(repo *models.Repo, branch string) *tangled.RepoPull_Target { 2552 + target := &tangled.RepoPull_Target{Branch: branch} 2553 + if repo.RepoDid != "" { 2554 + target.RepoDid = &repo.RepoDid 2555 + } else { 2556 + s := string(repo.RepoAt()) 2557 + target.Repo = &s 2558 + } 2559 + return target 2560 + }
+2 -2
appview/repo/archive.go
··· 25 scheme = "https" 26 } 27 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 28 - didSlashRepo := f.DidSlashRepo() 29 30 // build the xrpc url 31 u, err := url.Parse(host) ··· 66 if link := resp.Header.Get("Link"); link != "" { 67 if resolvedRef, err := extractImmutableLink(link); err == nil { 68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 - rp.config.Core.BaseUrl(), f.DidSlashRepo(), resolvedRef) 70 w.Header().Set("Link", newLink) 71 } 72 }
··· 25 scheme = "https" 26 } 27 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 28 + didSlashRepo := f.RepoIdentifier() 29 30 // build the xrpc url 31 u, err := url.Parse(host) ··· 66 if link := resp.Header.Get("Link"); link != "" { 67 if resolvedRef, err := extractImmutableLink(link); err == nil { 68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 + rp.config.Core.BaseUrl(), f.RepoIdentifier(), resolvedRef) 70 w.Header().Set("Link", newLink) 71 } 72 }
+19 -9
appview/repo/artifact.go
··· 80 Repo: user.Active.Did, 81 Rkey: rkey, 82 Record: &lexutil.LexiconTypeDecoder{ 83 - Val: &tangled.RepoArtifact{ 84 - Artifact: uploadBlobResp.Blob, 85 - CreatedAt: createdAt.Format(time.RFC3339), 86 - Name: header.Filename, 87 - Repo: f.RepoAt().String(), 88 - Tag: tag.Tag.Hash[:], 89 - }, 90 }, 91 }) 92 if err != nil { ··· 109 Did: user.Active.Did, 110 Rkey: rkey, 111 RepoAt: f.RepoAt(), 112 Tag: tag.Tag.Hash, 113 CreatedAt: createdAt, 114 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), ··· 322 Host: host, 323 } 324 325 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 326 - xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 327 if err != nil { 328 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 329 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) ··· 358 359 return tag, nil 360 }
··· 80 Repo: user.Active.Did, 81 Rkey: rkey, 82 Record: &lexutil.LexiconTypeDecoder{ 83 + Val: repoArtifactRecord(f, uploadBlobResp.Blob, createdAt, header.Filename, tag.Tag.Hash[:]), 84 }, 85 }) 86 if err != nil { ··· 103 Did: user.Active.Did, 104 Rkey: rkey, 105 RepoAt: f.RepoAt(), 106 + RepoDid: f.RepoDid, 107 Tag: tag.Tag.Hash, 108 CreatedAt: createdAt, 109 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), ··· 317 Host: host, 318 } 319 320 + xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, f.RepoIdentifier()) 321 if err != nil { 322 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 323 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) ··· 352 353 return tag, nil 354 } 355 + 356 + func repoArtifactRecord(f *models.Repo, blob *lexutil.LexBlob, createdAt time.Time, name string, tag []byte) *tangled.RepoArtifact { 357 + rec := &tangled.RepoArtifact{ 358 + Artifact: blob, 359 + CreatedAt: createdAt.Format(time.RFC3339), 360 + Name: name, 361 + Tag: tag, 362 + } 363 + if f.RepoDid != "" { 364 + rec.RepoDid = &f.RepoDid 365 + } else { 366 + s := f.RepoAt().String() 367 + rec.Repo = &s 368 + } 369 + return rec 370 + }
+3 -4
appview/repo/blob.go
··· 58 xrpcc := &indigoxrpc.Client{ 59 Host: host, 60 } 61 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 62 - resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 63 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 64 l.Error("failed to call XRPC repo.blob", "err", xrpcerr) 65 rp.pages.Error503(w) ··· 139 if !rp.config.Core.Dev { 140 scheme = "https" 141 } 142 - repo := f.DidSlashRepo() 143 baseURL := &url.URL{ 144 Scheme: scheme, 145 Host: f.Knot, ··· 290 scheme = "https" 291 } 292 293 - repoName := fmt.Sprintf("%s/%s", repo.Did, repo.Name) 294 baseURL := &url.URL{ 295 Scheme: scheme, 296 Host: repo.Knot,
··· 58 xrpcc := &indigoxrpc.Client{ 59 Host: host, 60 } 61 + resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, f.RepoIdentifier()) 62 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 63 l.Error("failed to call XRPC repo.blob", "err", xrpcerr) 64 rp.pages.Error503(w) ··· 138 if !rp.config.Core.Dev { 139 scheme = "https" 140 } 141 + repo := f.RepoIdentifier() 142 baseURL := &url.URL{ 143 Scheme: scheme, 144 Host: f.Knot, ··· 289 scheme = "https" 290 } 291 292 + repoName := repo.RepoIdentifier() 293 baseURL := &url.URL{ 294 Scheme: scheme, 295 Host: repo.Knot,
+1 -2
appview/repo/branches.go
··· 29 xrpcc := &indigoxrpc.Client{ 30 Host: host, 31 } 32 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 33 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 34 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 35 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 36 rp.pages.Error503(w)
··· 29 xrpcc := &indigoxrpc.Client{ 30 Host: host, 31 } 32 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 33 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 34 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 35 rp.pages.Error503(w)
+7 -7
appview/repo/compare.go
··· 36 Host: host, 37 } 38 39 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 40 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 41 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 42 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 43 rp.pages.Error503(w) ··· 74 head = queryHead 75 } 76 77 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 78 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 79 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 80 rp.pages.Error503(w) ··· 149 Host: host, 150 } 151 152 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 153 154 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 155 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 156 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 157 rp.pages.Error503(w) ··· 165 return 166 } 167 168 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 169 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 170 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 171 rp.pages.Error503(w) ··· 179 return 180 } 181 182 - compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 183 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 184 l.Error("failed to call XRPC repo.compare", "err", xrpcerr) 185 rp.pages.Error503(w)
··· 36 Host: host, 37 } 38 39 + repoId := f.RepoIdentifier() 40 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId) 41 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 42 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 43 rp.pages.Error503(w) ··· 74 head = queryHead 75 } 76 77 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId) 78 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 79 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 80 rp.pages.Error503(w) ··· 149 Host: host, 150 } 151 152 + repoId := f.RepoIdentifier() 153 154 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId) 155 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 156 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 157 rp.pages.Error503(w) ··· 165 return 166 } 167 168 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId) 169 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 170 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 171 rp.pages.Error503(w) ··· 179 return 180 } 181 182 + compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repoId, base, head) 183 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 184 l.Error("failed to call XRPC repo.compare", "err", xrpcerr) 185 rp.pages.Error503(w)
+7 -8
appview/repo/index.go
··· 182 183 if err != nil || langs == nil { 184 // non-fatal, fetch langs from ks via XRPC 185 - didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name) 186 - ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, didSlashRepo) 187 if err != nil { 188 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 189 l.Error("failed to call XRPC repo.languages", "err", xrpcerr) ··· 199 for _, lang := range ls.Languages { 200 langs = append(langs, models.RepoLanguage{ 201 RepoAt: repo.RepoAt(), 202 Ref: currentRef, 203 IsDefaultRef: isDefaultRef, 204 Language: lang.Name, ··· 259 260 // buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 261 func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) { 262 - didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name) 263 264 - // first get branches to determine the ref if not specified 265 - branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, didSlashRepo) 266 if err != nil { 267 return nil, fmt.Errorf("failed to call repoBranches: %w", err) 268 } ··· 304 305 // tags 306 wg.Go(func() { 307 - tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, didSlashRepo) 308 if err != nil { 309 errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err)) 310 return ··· 317 318 // tree/files 319 wg.Go(func() { 320 - resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, didSlashRepo) 321 if err != nil { 322 errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err)) 323 return ··· 327 328 // commits 329 wg.Go(func() { 330 - logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, didSlashRepo) 331 if err != nil { 332 errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err)) 333 return
··· 182 183 if err != nil || langs == nil { 184 // non-fatal, fetch langs from ks via XRPC 185 + ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo.RepoIdentifier()) 186 if err != nil { 187 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 188 l.Error("failed to call XRPC repo.languages", "err", xrpcerr) ··· 198 for _, lang := range ls.Languages { 199 langs = append(langs, models.RepoLanguage{ 200 RepoAt: repo.RepoAt(), 201 + RepoDid: repo.RepoDid, 202 Ref: currentRef, 203 IsDefaultRef: isDefaultRef, 204 Language: lang.Name, ··· 259 260 // buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 261 func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) { 262 + repoId := repo.RepoIdentifier() 263 264 + branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repoId) 265 if err != nil { 266 return nil, fmt.Errorf("failed to call repoBranches: %w", err) 267 } ··· 303 304 // tags 305 wg.Go(func() { 306 + tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repoId) 307 if err != nil { 308 errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err)) 309 return ··· 316 317 // tree/files 318 wg.Go(func() { 319 + resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repoId) 320 if err != nil { 321 errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err)) 322 return ··· 326 327 // commits 328 wg.Go(func() { 329 + logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repoId) 330 if err != nil { 331 errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err)) 332 return
+5 -6
appview/repo/log.go
··· 57 cursor = strconv.Itoa(offset) 58 } 59 60 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 61 - xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 62 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 63 l.Error("failed to call XRPC repo.log", "err", xrpcerr) 64 rp.pages.Error503(w) ··· 72 return 73 } 74 75 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 76 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 77 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 78 rp.pages.Error503(w) ··· 93 } 94 } 95 96 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 97 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 98 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 99 rp.pages.Error503(w) ··· 172 Host: host, 173 } 174 175 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 176 - xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 177 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 178 l.Error("failed to call XRPC repo.diff", "err", xrpcerr) 179 rp.pages.Error503(w)
··· 57 cursor = strconv.Itoa(offset) 58 } 59 60 + xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, f.RepoIdentifier()) 61 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 62 l.Error("failed to call XRPC repo.log", "err", xrpcerr) 63 rp.pages.Error503(w) ··· 71 return 72 } 73 74 + repoId := f.RepoIdentifier() 75 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId) 76 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 77 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 78 rp.pages.Error503(w) ··· 93 } 94 } 95 96 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId) 97 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 98 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 99 rp.pages.Error503(w) ··· 172 Host: host, 173 } 174 175 + xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, f.RepoIdentifier()) 176 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 177 l.Error("failed to call XRPC repo.diff", "err", xrpcerr) 178 rp.pages.Error503(w)
+128 -55
appview/repo/repo.go
··· 33 atpclient "github.com/bluesky-social/indigo/atproto/client" 34 "github.com/bluesky-social/indigo/atproto/syntax" 35 lexutil "github.com/bluesky-social/indigo/lex/util" 36 - securejoin "github.com/cyphar/filepath-securejoin" 37 "github.com/go-chi/chi/v5" 38 ) 39 ··· 309 return 310 } 311 312 - err = db.SubscribeLabel(tx, &models.RepoLabel{ 313 RepoAt: f.RepoAt(), 314 LabelAt: label.AtUri(), 315 - }) 316 317 err = tx.Commit() 318 if err != nil { ··· 504 err = db.SubscribeLabel(tx, &models.RepoLabel{ 505 RepoAt: f.RepoAt(), 506 LabelAt: syntax.ATURI(l), 507 }) 508 if err != nil { 509 fail("Failed to subscribe to label.", err) ··· 746 Repo: currentUser.Active.Did, 747 Rkey: rkey, 748 Record: &lexutil.LexiconTypeDecoder{ 749 - Val: &tangled.RepoCollaborator{ 750 - Subject: collaboratorIdent.DID.String(), 751 - Repo: string(f.RepoAt()), 752 - CreatedAt: createdAt.Format(time.RFC3339), 753 - }}, 754 }) 755 // invalid record 756 if err != nil { ··· 785 } 786 defer rollback() 787 788 - err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 789 if err != nil { 790 fail("Failed to add collaborator permissions.", err) 791 return ··· 796 Rkey: rkey, 797 SubjectDid: collaboratorIdent.DID, 798 RepoAt: f.RepoAt(), 799 Created: createdAt, 800 }) 801 if err != nil { ··· 891 }() 892 893 // remove collaborator RBAC 894 - repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 895 if err != nil { 896 rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 897 return 898 } 899 for _, c := range repoCollaborators { 900 did := c[0] 901 - rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 902 } 903 l.Info("removed collaborators") 904 905 // remove repo RBAC 906 - err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.DidSlashRepo()) 907 if err != nil { 908 rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 909 return ··· 1058 uri = "http" 1059 } 1060 1061 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Name) 1062 l = l.With("cloneUrl", forkSourceUrl) 1063 1064 - sourceAt := f.RepoAt().String() 1065 1066 - // create an atproto record for this fork 1067 - rkey := tid.TID() 1068 repo := &models.Repo{ 1069 Did: user.Active.Did, 1070 Name: forkName, 1071 Knot: targetKnot, 1072 Rkey: rkey, 1073 - Source: sourceAt, 1074 Description: f.Description, 1075 Created: time.Now(), 1076 Labels: rp.config.Label.DefaultLabelDefs, 1077 } 1078 record := repo.AsRecord() 1079 1080 atpClient, err := rp.oauth.AuthorizedClient(r) 1081 if err != nil { 1082 l.Error("failed to create xrpcclient", "err", err) 1083 rp.pages.Notice(w, "repo", "Failed to fork repository.") 1084 return 1085 } ··· 1094 }) 1095 if err != nil { 1096 l.Error("failed to write to PDS", "err", err) 1097 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1098 return 1099 } ··· 1109 return 1110 } 1111 1112 - // The rollback function reverts a few things on failure: 1113 - // - the pending txn 1114 - // - the ACLs 1115 - // - the atproto record created 1116 rollback := func() { 1117 err1 := tx.Rollback() 1118 err2 := rp.enforcer.E.LoadPolicy() 1119 err3 := rollbackRecord(context.Background(), aturi, atpClient) 1120 1121 - // ignore txn complete errors, this is okay 1122 if errors.Is(err1, sql.ErrTxDone) { 1123 err1 = nil 1124 } 1125 1126 if errs := errors.Join(err1, err2, err3); errs != nil { 1127 l.Error("failed to rollback changes", "errs", errs) 1128 - return 1129 } 1130 } 1131 defer rollback() 1132 1133 - // TODO: this could coordinate better with the knot to recieve a clone status 1134 - client, err := rp.oauth.ServiceClient( 1135 - r, 1136 - oauth.WithService(targetKnot), 1137 - oauth.WithLxm(tangled.RepoCreateNSID), 1138 - oauth.WithDev(rp.config.Core.Dev), 1139 - oauth.WithTimeout(time.Second*20), // big repos take time to clone 1140 - ) 1141 - if err != nil { 1142 - l.Error("could not create service client", "err", err) 1143 - rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1144 - return 1145 - } 1146 - 1147 - err = tangled.RepoCreate( 1148 - r.Context(), 1149 - client, 1150 - &tangled.RepoCreate_Input{ 1151 - Rkey: rkey, 1152 - Source: &forkSourceUrl, 1153 - }, 1154 - ) 1155 - if err := xrpcclient.HandleXrpcErr(err); err != nil { 1156 - rp.pages.Notice(w, "repo", err.Error()) 1157 - return 1158 - } 1159 - 1160 err = db.AddRepo(tx, repo) 1161 if err != nil { 1162 l.Error("failed to AddRepo", "err", err) ··· 1164 return 1165 } 1166 1167 - // acls 1168 - p, _ := securejoin.SecureJoin(user.Active.Did, forkName) 1169 - err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p) 1170 if err != nil { 1171 l.Error("failed to add ACLs", "err", err) 1172 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1187 return 1188 } 1189 1190 - // reset the ATURI because the transaction completed successfully 1191 aturi = "" 1192 1193 rp.notifier.NewRepo(r.Context(), repo) 1194 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName)) 1195 } 1196 } 1197 ··· 1216 }) 1217 return err 1218 }
··· 33 atpclient "github.com/bluesky-social/indigo/atproto/client" 34 "github.com/bluesky-social/indigo/atproto/syntax" 35 lexutil "github.com/bluesky-social/indigo/lex/util" 36 + 37 "github.com/go-chi/chi/v5" 38 ) 39 ··· 309 return 310 } 311 312 + if err = db.SubscribeLabel(tx, &models.RepoLabel{ 313 RepoAt: f.RepoAt(), 314 LabelAt: label.AtUri(), 315 + RepoDid: f.RepoDid, 316 + }); err != nil { 317 + fail("Failed to subscribe to label.", err) 318 + return 319 + } 320 321 err = tx.Commit() 322 if err != nil { ··· 508 err = db.SubscribeLabel(tx, &models.RepoLabel{ 509 RepoAt: f.RepoAt(), 510 LabelAt: syntax.ATURI(l), 511 + RepoDid: f.RepoDid, 512 }) 513 if err != nil { 514 fail("Failed to subscribe to label.", err) ··· 751 Repo: currentUser.Active.Did, 752 Rkey: rkey, 753 Record: &lexutil.LexiconTypeDecoder{ 754 + Val: repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt), 755 + }, 756 }) 757 // invalid record 758 if err != nil { ··· 787 } 788 defer rollback() 789 790 + err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.RepoIdentifier()) 791 if err != nil { 792 fail("Failed to add collaborator permissions.", err) 793 return ··· 798 Rkey: rkey, 799 SubjectDid: collaboratorIdent.DID, 800 RepoAt: f.RepoAt(), 801 + RepoDid: f.RepoDid, 802 Created: createdAt, 803 }) 804 if err != nil { ··· 894 }() 895 896 // remove collaborator RBAC 897 + repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.RepoIdentifier(), f.Knot) 898 if err != nil { 899 rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 900 return 901 } 902 for _, c := range repoCollaborators { 903 did := c[0] 904 + rp.enforcer.RemoveCollaborator(did, f.Knot, f.RepoIdentifier()) 905 } 906 l.Info("removed collaborators") 907 908 // remove repo RBAC 909 + err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.RepoIdentifier()) 910 if err != nil { 911 rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 912 return ··· 1061 uri = "http" 1062 } 1063 1064 + forkSourceUrl := fmt.Sprintf("%s://%s/%s", uri, f.Knot, f.RepoIdentifier()) 1065 l = l.With("cloneUrl", forkSourceUrl) 1066 1067 + rkey := tid.TID() 1068 1069 + // TODO: this could coordinate better with the knot to recieve a clone status 1070 + client, err := rp.oauth.ServiceClient( 1071 + r, 1072 + oauth.WithService(targetKnot), 1073 + oauth.WithLxm(tangled.RepoCreateNSID), 1074 + oauth.WithDev(rp.config.Core.Dev), 1075 + oauth.WithTimeout(time.Second*20), 1076 + ) 1077 + if err != nil { 1078 + l.Error("could not create service client", "err", err) 1079 + rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1080 + return 1081 + } 1082 + 1083 + forkInput := &tangled.RepoCreate_Input{ 1084 + Rkey: rkey, 1085 + Name: forkName, 1086 + Source: &forkSourceUrl, 1087 + } 1088 + if rd := strings.TrimSpace(r.FormValue("repo_did")); rd != "" { 1089 + forkInput.RepoDid = &rd 1090 + } 1091 + 1092 + createResp, createErr := tangled.RepoCreate( 1093 + r.Context(), 1094 + client, 1095 + forkInput, 1096 + ) 1097 + if err := xrpcclient.HandleXrpcErr(createErr); err != nil { 1098 + rp.pages.Notice(w, "repo", err.Error()) 1099 + return 1100 + } 1101 + 1102 + var repoDid string 1103 + if createResp != nil && createResp.RepoDid != nil { 1104 + repoDid = *createResp.RepoDid 1105 + } 1106 + if repoDid == "" { 1107 + l.Error("knot returned empty repo DID for fork") 1108 + rp.pages.Notice(w, "repo", "Knot failed to mint a repo DID. The knot may need to be upgraded.") 1109 + return 1110 + } 1111 + 1112 + forkSource := f.RepoAt().String() 1113 + if f.RepoDid != "" { 1114 + forkSource = f.RepoDid 1115 + } 1116 + 1117 repo := &models.Repo{ 1118 Did: user.Active.Did, 1119 Name: forkName, 1120 Knot: targetKnot, 1121 Rkey: rkey, 1122 + Source: forkSource, 1123 Description: f.Description, 1124 Created: time.Now(), 1125 Labels: rp.config.Label.DefaultLabelDefs, 1126 + RepoDid: repoDid, 1127 } 1128 record := repo.AsRecord() 1129 1130 + cleanupKnot := func() { 1131 + go func() { 1132 + delays := []time.Duration{0, 2 * time.Second, 5 * time.Second} 1133 + for attempt, delay := range delays { 1134 + time.Sleep(delay) 1135 + deleteClient, dErr := rp.oauth.ServiceClient( 1136 + r, 1137 + oauth.WithService(targetKnot), 1138 + oauth.WithLxm(tangled.RepoDeleteNSID), 1139 + oauth.WithDev(rp.config.Core.Dev), 1140 + ) 1141 + if dErr != nil { 1142 + l.Error("failed to create delete client for knot cleanup", "attempt", attempt+1, "err", dErr) 1143 + continue 1144 + } 1145 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 1146 + if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{ 1147 + Did: user.Active.Did, 1148 + Name: forkName, 1149 + Rkey: rkey, 1150 + }); dErr != nil { 1151 + cancel() 1152 + l.Error("failed to clean up fork on knot after rollback", "attempt", attempt+1, "err", dErr) 1153 + continue 1154 + } 1155 + cancel() 1156 + l.Info("successfully cleaned up fork on knot after rollback", "attempt", attempt+1) 1157 + return 1158 + } 1159 + l.Error("exhausted retries for knot cleanup, fork may be orphaned", 1160 + "did", user.Active.Did, "fork", forkName, "knot", targetKnot) 1161 + }() 1162 + } 1163 + 1164 atpClient, err := rp.oauth.AuthorizedClient(r) 1165 if err != nil { 1166 l.Error("failed to create xrpcclient", "err", err) 1167 + cleanupKnot() 1168 rp.pages.Notice(w, "repo", "Failed to fork repository.") 1169 return 1170 } ··· 1179 }) 1180 if err != nil { 1181 l.Error("failed to write to PDS", "err", err) 1182 + cleanupKnot() 1183 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1184 return 1185 } ··· 1195 return 1196 } 1197 1198 rollback := func() { 1199 err1 := tx.Rollback() 1200 err2 := rp.enforcer.E.LoadPolicy() 1201 err3 := rollbackRecord(context.Background(), aturi, atpClient) 1202 1203 if errors.Is(err1, sql.ErrTxDone) { 1204 err1 = nil 1205 } 1206 1207 if errs := errors.Join(err1, err2, err3); errs != nil { 1208 l.Error("failed to rollback changes", "errs", errs) 1209 + } 1210 + 1211 + if aturi != "" { 1212 + cleanupKnot() 1213 } 1214 } 1215 defer rollback() 1216 1217 err = db.AddRepo(tx, repo) 1218 if err != nil { 1219 l.Error("failed to AddRepo", "err", err) ··· 1221 return 1222 } 1223 1224 + rbacPath := repo.RepoIdentifier() 1225 + err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, rbacPath) 1226 if err != nil { 1227 l.Error("failed to add ACLs", "err", err) 1228 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1243 return 1244 } 1245 1246 aturi = "" 1247 1248 rp.notifier.NewRepo(r.Context(), repo) 1249 + if repoDid != "" { 1250 + rp.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) 1251 + } else { 1252 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName)) 1253 + } 1254 } 1255 } 1256 ··· 1275 }) 1276 return err 1277 } 1278 + 1279 + func repoCollaboratorRecord(f *models.Repo, subject string, createdAt time.Time) *tangled.RepoCollaborator { 1280 + rec := &tangled.RepoCollaborator{ 1281 + Subject: subject, 1282 + CreatedAt: createdAt.Format(time.RFC3339), 1283 + } 1284 + if f.RepoDid != "" { 1285 + rec.RepoDid = &f.RepoDid 1286 + } else { 1287 + s := string(f.RepoAt()) 1288 + rec.Repo = &s 1289 + } 1290 + return rec 1291 + }
+2 -3
appview/repo/settings.go
··· 188 Host: host, 189 } 190 191 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 192 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 193 var result types.RepoBranchesResponse 194 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 195 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) ··· 260 user := rp.oauth.GetMultiAccountUser(r) 261 262 collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) { 263 - repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot) 264 if err != nil { 265 return nil, err 266 }
··· 188 Host: host, 189 } 190 191 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 192 var result types.RepoBranchesResponse 193 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 194 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) ··· 259 user := rp.oauth.GetMultiAccountUser(r) 260 261 collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) { 262 + repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.RepoIdentifier(), repo.Knot) 263 if err != nil { 264 return nil, err 265 }
+3 -5
appview/repo/tags.go
··· 35 xrpcc := &indigoxrpc.Client{ 36 Host: host, 37 } 38 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 39 - xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 40 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 41 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 42 rp.pages.Error503(w) ··· 98 xrpcc := &indigoxrpc.Client{ 99 Host: host, 100 } 101 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 102 tag := chi.URLParam(r, "tag") 103 104 - xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, repo, tag) 105 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 106 // if we don't match an existing tag, and the tag we're trying 107 // to match is "latest", resolve to the most recent tag 108 if tag == "latest" { 109 - tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, repo) 110 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 111 l.Error("failed to call XRPC repo.tags for latest", "err", xrpcerr) 112 rp.pages.Error503(w)
··· 35 xrpcc := &indigoxrpc.Client{ 36 Host: host, 37 } 38 + xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 39 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 40 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 41 rp.pages.Error503(w) ··· 97 xrpcc := &indigoxrpc.Client{ 98 Host: host, 99 } 100 tag := chi.URLParam(r, "tag") 101 102 + xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, f.RepoIdentifier(), tag) 103 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 104 // if we don't match an existing tag, and the tag we're trying 105 // to match is "latest", resolve to the most recent tag 106 if tag == "latest" { 107 + tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, f.RepoIdentifier()) 108 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 109 l.Error("failed to call XRPC repo.tags for latest", "err", xrpcerr) 110 rp.pages.Error503(w)
+1 -2
appview/repo/tree.go
··· 41 xrpcc := &indigoxrpc.Client{ 42 Host: host, 43 } 44 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 45 - xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 46 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 47 l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 48 rp.pages.Error503(w)
··· 41 xrpcc := &indigoxrpc.Client{ 42 Host: host, 43 } 44 + xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, f.RepoIdentifier()) 45 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 46 l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 47 rp.pages.Error503(w)
+6 -5
appview/repo/webhooks.go
··· 89 } 90 91 webhook := &models.Webhook{ 92 - RepoAt: f.RepoAt(), 93 - Url: url, 94 - Secret: secret, 95 - Active: active, 96 - Events: events, 97 } 98 99 tx, err := rp.db.Begin()
··· 89 } 90 91 webhook := &models.Webhook{ 92 + RepoAt: f.RepoAt(), 93 + RepoDid: f.RepoDid, 94 + Url: url, 95 + Secret: secret, 96 + Active: active, 97 + Events: events, 98 } 99 100 tx, err := rp.db.Begin()
+20 -6
appview/reporesolver/resolver.go
··· 30 31 // NOTE: this... should not even be here. the entire package will be removed in future refactor 32 func GetBaseRepoPath(r *http.Request, repo *models.Repo) string { 33 var ( 34 user = chi.URLParam(r, "user") 35 name = chi.URLParam(r, "repo") 36 ) 37 if user == "" || name == "" { 38 - return repo.DidSlashRepo() 39 } 40 return path.Join(user, name) 41 } ··· 71 roles := repoinfo.RolesInRepo{} 72 if user != nil && user.Active != nil { 73 isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt) 74 - roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 75 } 76 77 stats := repo.RepoStats 78 if stats == nil { 79 - starCount, err := db.GetStarCount(rr.execer, repoAt) 80 - if err != nil { 81 log.Println("failed to get star count for ", repoAt) 82 } 83 issueCount, err := db.GetIssueCount(rr.execer, repoAt) ··· 98 var sourceRepo *models.Repo 99 var err error 100 if repo.Source != "" { 101 - sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source) 102 if err != nil { 103 - log.Println("failed to get repo by at uri", err) 104 } 105 } 106 ··· 108 // this is basically a models.Repo 109 OwnerDid: ownerId.DID.String(), 110 OwnerHandle: ownerId.Handle.String(), 111 Name: repo.Name, 112 Rkey: repo.Rkey, 113 Description: repo.Description,
··· 30 31 // NOTE: this... should not even be here. the entire package will be removed in future refactor 32 func GetBaseRepoPath(r *http.Request, repo *models.Repo) string { 33 + if repo.RepoDid != "" { 34 + return repo.RepoDid 35 + } 36 var ( 37 user = chi.URLParam(r, "user") 38 name = chi.URLParam(r, "repo") 39 ) 40 if user == "" || name == "" { 41 + return repo.RepoIdentifier() 42 } 43 return path.Join(user, name) 44 } ··· 74 roles := repoinfo.RolesInRepo{} 75 if user != nil && user.Active != nil { 76 isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt) 77 + roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.RepoIdentifier()) 78 } 79 80 stats := repo.RepoStats 81 if stats == nil { 82 + var starCount int 83 + var starErr error 84 + if repo.RepoDid != "" { 85 + starCount, starErr = db.GetStarCountByRepoDid(rr.execer, repo.RepoDid, repoAt) 86 + } else { 87 + starCount, starErr = db.GetStarCount(rr.execer, repoAt) 88 + } 89 + if starErr != nil { 90 log.Println("failed to get star count for ", repoAt) 91 } 92 issueCount, err := db.GetIssueCount(rr.execer, repoAt) ··· 107 var sourceRepo *models.Repo 108 var err error 109 if repo.Source != "" { 110 + if strings.HasPrefix(repo.Source, "did:") { 111 + sourceRepo, err = db.GetRepoByDid(rr.execer, repo.Source) 112 + } else { 113 + sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source) 114 + } 115 if err != nil { 116 + log.Println("failed to get source repo", err) 117 } 118 } 119 ··· 121 // this is basically a models.Repo 122 OwnerDid: ownerId.DID.String(), 123 OwnerHandle: ownerId.Handle.String(), 124 + RepoDid: repo.RepoDid, 125 Name: repo.Name, 126 Rkey: repo.Rkey, 127 Description: repo.Description,
+7 -20
appview/state/git_http.go
··· 12 ) 13 14 func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 - user := r.Context().Value("resolvedId").(identity.Identity) 16 repo := r.Context().Value("repo").(*models.Repo) 17 18 scheme := "https" ··· 20 scheme = "http" 21 } 22 23 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 24 s.proxyRequest(w, r, targetURL) 25 26 } 27 28 func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 29 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 - if !ok { 31 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 - return 33 - } 34 repo := r.Context().Value("repo").(*models.Repo) 35 36 scheme := "https" ··· 38 scheme = "http" 39 } 40 41 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 s.proxyRequest(w, r, targetURL) 43 } 44 45 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 46 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 47 - if !ok { 48 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 49 - return 50 - } 51 repo := r.Context().Value("repo").(*models.Repo) 52 53 scheme := "https" ··· 55 scheme = "http" 56 } 57 58 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 s.proxyRequest(w, r, targetURL) 60 } 61 62 func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 63 - user, ok := r.Context().Value("resolvedId").(identity.Identity) 64 - if !ok { 65 - http.Error(w, "failed to resolve user", http.StatusInternalServerError) 66 - return 67 - } 68 repo := r.Context().Value("repo").(*models.Repo) 69 70 scheme := "https" ··· 72 scheme = "http" 73 } 74 75 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 76 s.proxyRequest(w, r, targetURL) 77 } 78 ··· 90 proxyReq.Header = r.Header 91 92 repoOwnerHandle := chi.URLParam(r, "user") 93 proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 94 95 // Execute request
··· 12 ) 13 14 func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 repo := r.Context().Value("repo").(*models.Repo) 16 17 scheme := "https" ··· 19 scheme = "http" 20 } 21 22 + targetURL := fmt.Sprintf("%s://%s/%s/info/refs?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 23 s.proxyRequest(w, r, targetURL) 24 25 } 26 27 func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 28 repo := r.Context().Value("repo").(*models.Repo) 29 30 scheme := "https" ··· 32 scheme = "http" 33 } 34 35 + targetURL := fmt.Sprintf("%s://%s/%s/git-upload-archive?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 36 s.proxyRequest(w, r, targetURL) 37 } 38 39 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 40 repo := r.Context().Value("repo").(*models.Repo) 41 42 scheme := "https" ··· 44 scheme = "http" 45 } 46 47 + targetURL := fmt.Sprintf("%s://%s/%s/git-upload-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 48 s.proxyRequest(w, r, targetURL) 49 } 50 51 func (s *State) ReceivePack(w http.ResponseWriter, r *http.Request) { 52 repo := r.Context().Value("repo").(*models.Repo) 53 54 scheme := "https" ··· 56 scheme = "http" 57 } 58 59 + targetURL := fmt.Sprintf("%s://%s/%s/git-receive-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 60 s.proxyRequest(w, r, targetURL) 61 } 62 ··· 74 proxyReq.Header = r.Header 75 76 repoOwnerHandle := chi.URLParam(r, "user") 77 + if id, ok := r.Context().Value("resolvedId").(identity.Identity); ok && !id.Handle.IsInvalidHandle() { 78 + repoOwnerHandle = id.Handle.String() 79 + } 80 proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 81 82 // Execute request
+35 -34
appview/state/knotstream.go
··· 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" ··· 86 return err 87 } 88 89 knownKnots, err := enforcer.GetKnotsForUser(record.CommitterDid) 90 if err != nil { 91 return err ··· 96 97 logger.Info("processing gitRefUpdate event", 98 "repo_did", record.RepoDid, 99 - "repo_name", record.RepoName, 100 "ref", record.Ref, 101 "old_sha", record.OldSha, 102 "new_sha", record.NewSha) 103 104 - // trigger webhook notifications first (before other ops that might fail) 105 var errWebhook error 106 - repos, err := db.GetRepos( 107 - d, 108 - 0, 109 - orm.FilterEq("did", record.RepoDid), 110 - orm.FilterEq("name", record.RepoName), 111 - ) 112 - if err != nil { 113 - errWebhook = fmt.Errorf("failed to lookup repo for webhooks: %w", err) 114 - } else if len(repos) == 1 { 115 notifier.Push(ctx, &repos[0], record.Ref, record.OldSha, record.NewSha, record.CommitterDid) 116 } 117 ··· 167 168 func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { 169 if record.Meta == nil || record.Meta.LangBreakdown == nil || record.Meta.LangBreakdown.Inputs == nil { 170 - return fmt.Errorf("empty language data for repo: %s/%s", record.RepoDid, record.RepoName) 171 } 172 173 - repos, err := db.GetRepos( 174 - d, 175 - 0, 176 - orm.FilterEq("did", record.RepoDid), 177 - orm.FilterEq("name", record.RepoName), 178 - ) 179 - if err != nil { 180 - return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err) 181 } 182 - if len(repos) != 1 { 183 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 184 } 185 - repo := repos[0] 186 187 ref := plumbing.ReferenceName(record.Ref) 188 if !ref.IsBranch() { ··· 197 198 langs = append(langs, models.RepoLanguage{ 199 RepoAt: repo.RepoAt(), 200 Ref: ref.Short(), 201 IsDefaultRef: record.Meta.IsDefaultRef, 202 Language: l.Lang, ··· 235 return fmt.Errorf("empty repo: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 236 } 237 238 - // does this repo have a spindle configured? 239 - repos, err := db.GetRepos( 240 - d, 241 - 0, 242 - orm.FilterEq("did", record.TriggerMetadata.Repo.Did), 243 - orm.FilterEq("name", record.TriggerMetadata.Repo.Repo), 244 - ) 245 - if err != nil { 246 - return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err) 247 } 248 - if len(repos) != 1 { 249 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 250 } 251 if repos[0].Spindle == "" { 252 return fmt.Errorf("repo does not have a spindle configured yet: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 253 } ··· 285 Knot: source.Key(), 286 RepoOwner: syntax.DID(record.TriggerMetadata.Repo.Did), 287 RepoName: record.TriggerMetadata.Repo.Repo, 288 TriggerId: int(triggerId), 289 Sha: sha, 290 }
··· 2 3 import ( 4 "context" 5 + "database/sql" 6 "encoding/json" 7 "errors" 8 "fmt" ··· 87 return err 88 } 89 90 + if record.RepoDid == "" { 91 + logger.Error("gitRefUpdate missing repoDid, skipping", "owner_did", record.OwnerDid, "repo_name", record.RepoName) 92 + return fmt.Errorf("gitRefUpdate missing repoDid") 93 + } 94 + 95 knownKnots, err := enforcer.GetKnotsForUser(record.CommitterDid) 96 if err != nil { 97 return err ··· 102 103 logger.Info("processing gitRefUpdate event", 104 "repo_did", record.RepoDid, 105 "ref", record.Ref, 106 "old_sha", record.OldSha, 107 "new_sha", record.NewSha) 108 109 var errWebhook error 110 + 111 + repo, lookupErr := db.GetRepoByDid(d, record.RepoDid) 112 + if lookupErr != nil && !errors.Is(lookupErr, sql.ErrNoRows) { 113 + return fmt.Errorf("failed to look up repo by DID %s: %w", record.RepoDid, lookupErr) 114 + } 115 + 116 + var repos []models.Repo 117 + if lookupErr == nil { 118 + repos = []models.Repo{*repo} 119 + } 120 + 121 + if errWebhook == nil && len(repos) == 1 { 122 notifier.Push(ctx, &repos[0], record.Ref, record.OldSha, record.NewSha, record.CommitterDid) 123 } 124 ··· 174 175 func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { 176 if record.Meta == nil || record.Meta.LangBreakdown == nil || record.Meta.LangBreakdown.Inputs == nil { 177 + return fmt.Errorf("empty language data for repo: %s/%s", record.OwnerDid, record.RepoName) 178 } 179 180 + if record.RepoDid == "" { 181 + return fmt.Errorf("gitRefUpdate missing repoDid for language update") 182 } 183 + 184 + r, lookupErr := db.GetRepoByDid(d, record.RepoDid) 185 + if lookupErr != nil { 186 + return fmt.Errorf("failed to look up repo by DID %s: %w", record.RepoDid, lookupErr) 187 } 188 + repo := *r 189 190 ref := plumbing.ReferenceName(record.Ref) 191 if !ref.IsBranch() { ··· 200 201 langs = append(langs, models.RepoLanguage{ 202 RepoAt: repo.RepoAt(), 203 + RepoDid: repo.RepoDid, 204 Ref: ref.Short(), 205 IsDefaultRef: record.Meta.IsDefaultRef, 206 Language: l.Lang, ··· 239 return fmt.Errorf("empty repo: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 240 } 241 242 + if record.TriggerMetadata.Repo.RepoDid == "" { 243 + return fmt.Errorf("pipeline missing repoDid: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 244 } 245 + 246 + repo, lookupErr := db.GetRepoByDid(d, record.TriggerMetadata.Repo.RepoDid) 247 + if lookupErr != nil { 248 + return fmt.Errorf("failed to look up repo by DID %s: %w", record.TriggerMetadata.Repo.RepoDid, lookupErr) 249 } 250 + repos := []models.Repo{*repo} 251 if repos[0].Spindle == "" { 252 return fmt.Errorf("repo does not have a spindle configured yet: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 253 } ··· 285 Knot: source.Key(), 286 RepoOwner: syntax.DID(record.TriggerMetadata.Repo.Did), 287 RepoName: record.TriggerMetadata.Repo.Repo, 288 + RepoDid: repos[0].RepoDid, 289 TriggerId: int(triggerId), 290 Sha: sha, 291 }
+28 -2
appview/state/router.go
··· 1 package state 2 3 import ( 4 "net/http" 5 "strings" 6 7 "github.com/go-chi/chi/v5" 8 "tangled.org/core/appview/issues" 9 "tangled.org/core/appview/knots" 10 "tangled.org/core/appview/labels" ··· 45 if len(pathParts) > 0 { 46 firstPart := pathParts[0] 47 48 - // if using a DID or handle, just continue as per usual 49 - if userutil.IsDid(firstPart) || userutil.IsHandle(firstPart) { 50 userRouter.ServeHTTP(w, r) 51 return 52 }
··· 1 package state 2 3 import ( 4 + "context" 5 + "database/sql" 6 + "errors" 7 "net/http" 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 + "tangled.org/core/appview/db" 12 "tangled.org/core/appview/issues" 13 "tangled.org/core/appview/knots" 14 "tangled.org/core/appview/labels" ··· 49 if len(pathParts) > 0 { 50 firstPart := pathParts[0] 51 52 + if userutil.IsDid(firstPart) { 53 + repo, err := db.GetRepoByDid(s.db, firstPart) 54 + switch { 55 + case err == nil: 56 + remaining := "" 57 + if len(pathParts) > 1 { 58 + remaining = "/" + pathParts[1] 59 + } 60 + rewritten := "/" + repo.Did + "/" + repo.Name + remaining 61 + r2 := r.Clone(r.Context()) 62 + r2.URL.Path = rewritten 63 + r2.URL.RawPath = rewritten 64 + ctx := context.WithValue(r2.Context(), "repoDidCanonical", true) 65 + userRouter.ServeHTTP(w, r2.WithContext(ctx)) 66 + case errors.Is(err, sql.ErrNoRows): 67 + userRouter.ServeHTTP(w, r) 68 + default: 69 + s.logger.Error("db error looking up repo DID", "repoDid", firstPart, "err", err) 70 + http.Error(w, "internal server error", http.StatusInternalServerError) 71 + } 72 + return 73 + } 74 + 75 + if userutil.IsHandle(firstPart) { 76 userRouter.ServeHTTP(w, r) 77 return 78 }
+18 -5
appview/state/star.go
··· 12 "tangled.org/core/appview/db" 13 "tangled.org/core/appview/models" 14 "tangled.org/core/appview/pages" 15 "tangled.org/core/tid" 16 ) 17 ··· 40 case http.MethodPost: 41 createdAt := time.Now().Format(time.RFC3339) 42 rkey := tid.TID() 43 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 Collection: tangled.FeedStarNSID, 45 Repo: currentUser.Active.Did, 46 Rkey: rkey, 47 - Record: &lexutil.LexiconTypeDecoder{ 48 - Val: &tangled.FeedStar{ 49 - Subject: subjectUri.String(), 50 - CreatedAt: createdAt, 51 - }}, 52 }) 53 if err != nil { 54 log.Println("failed to create atproto record", err) ··· 60 Did: currentUser.Active.Did, 61 RepoAt: subjectUri, 62 Rkey: rkey, 63 } 64 65 err = db.AddStar(s.db, star)
··· 12 "tangled.org/core/appview/db" 13 "tangled.org/core/appview/models" 14 "tangled.org/core/appview/pages" 15 + "tangled.org/core/orm" 16 "tangled.org/core/tid" 17 ) 18 ··· 41 case http.MethodPost: 42 createdAt := time.Now().Format(time.RFC3339) 43 rkey := tid.TID() 44 + 45 + starRecord := &tangled.FeedStar{ 46 + CreatedAt: createdAt, 47 + } 48 + repo, err := db.GetRepo(s.db, orm.FilterEq("at_uri", subjectUri.String())) 49 + repoHasDid := err == nil && repo.RepoDid != "" 50 + if repoHasDid { 51 + starRecord.SubjectDid = &repo.RepoDid 52 + } else { 53 + s := subjectUri.String() 54 + starRecord.Subject = &s 55 + } 56 + 57 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 58 Collection: tangled.FeedStarNSID, 59 Repo: currentUser.Active.Did, 60 Rkey: rkey, 61 + Record: &lexutil.LexiconTypeDecoder{Val: starRecord}, 62 }) 63 if err != nil { 64 log.Println("failed to create atproto record", err) ··· 70 Did: currentUser.Active.Did, 71 RepoAt: subjectUri, 72 Rkey: rkey, 73 + } 74 + if repoHasDid { 75 + star.SubjectDid = repo.RepoDid 76 } 77 78 err = db.AddStar(s.db, star)
+92 -38
appview/state/state.go
··· 41 "github.com/bluesky-social/indigo/atproto/syntax" 42 lexutil "github.com/bluesky-social/indigo/lex/util" 43 "github.com/bluesky-social/indigo/xrpc" 44 - securejoin "github.com/cyphar/filepath-securejoin" 45 "github.com/go-chi/chi/v5" 46 "github.com/posthog/posthog-go" 47 ) ··· 432 return 433 } 434 435 - // create atproto record for this repo 436 rkey := tid.TID() 437 repo := &models.Repo{ 438 Did: user.Active.Did, 439 Name: repoName, ··· 442 Description: description, 443 Created: time.Now(), 444 Labels: s.config.Label.DefaultLabelDefs, 445 } 446 record := repo.AsRecord() 447 448 atpClient, err := s.oauth.AuthorizedClient(r) 449 if err != nil { 450 l.Info("PDS write failed", "err", err) 451 s.pages.Notice(w, "repo", "Failed to write record to PDS.") 452 return 453 } ··· 462 }) 463 if err != nil { 464 l.Info("PDS write failed", "err", err) 465 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 466 return 467 } ··· 477 return 478 } 479 480 - // The rollback function reverts a few things on failure: 481 - // - the pending txn 482 - // - the ACLs 483 - // - the atproto record created 484 rollback := func() { 485 err1 := tx.Rollback() 486 err2 := s.enforcer.E.LoadPolicy() 487 err3 := rollbackRecord(context.Background(), aturi, atpClient) 488 489 - // ignore txn complete errors, this is okay 490 if errors.Is(err1, sql.ErrTxDone) { 491 err1 = nil 492 } 493 494 if errs := errors.Join(err1, err2, err3); errs != nil { 495 l.Error("failed to rollback changes", "errs", errs) 496 - return 497 } 498 - } 499 - defer rollback() 500 501 - client, err := s.oauth.ServiceClient( 502 - r, 503 - oauth.WithService(domain), 504 - oauth.WithLxm(tangled.RepoCreateNSID), 505 - oauth.WithDev(s.config.Core.Dev), 506 - ) 507 - if err != nil { 508 - l.Error("service auth failed", "err", err) 509 - s.pages.Notice(w, "repo", "Failed to reach PDS.") 510 - return 511 - } 512 - 513 - xe := tangled.RepoCreate( 514 - r.Context(), 515 - client, 516 - &tangled.RepoCreate_Input{ 517 - Rkey: rkey, 518 - }, 519 - ) 520 - if err := xrpcclient.HandleXrpcErr(xe); err != nil { 521 - l.Error("xrpc error", "xe", xe) 522 - s.pages.Notice(w, "repo", err.Error()) 523 - return 524 } 525 526 err = db.AddRepo(tx, repo) 527 if err != nil { ··· 530 return 531 } 532 533 - // acls 534 - p, _ := securejoin.SecureJoin(user.Active.Did, repoName) 535 - err = s.enforcer.AddRepo(user.Active.Did, domain, p) 536 if err != nil { 537 l.Error("acl setup failed", "err", err) 538 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 553 return 554 } 555 556 - // reset the ATURI because the transaction completed successfully 557 aturi = "" 558 559 s.notifier.NewRepo(r.Context(), repo) 560 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 561 } 562 } 563
··· 41 "github.com/bluesky-social/indigo/atproto/syntax" 42 lexutil "github.com/bluesky-social/indigo/lex/util" 43 "github.com/bluesky-social/indigo/xrpc" 44 + 45 "github.com/go-chi/chi/v5" 46 "github.com/posthog/posthog-go" 47 ) ··· 432 return 433 } 434 435 rkey := tid.TID() 436 + 437 + client, err := s.oauth.ServiceClient( 438 + r, 439 + oauth.WithService(domain), 440 + oauth.WithLxm(tangled.RepoCreateNSID), 441 + oauth.WithDev(s.config.Core.Dev), 442 + ) 443 + if err != nil { 444 + l.Error("service auth failed", "err", err) 445 + s.pages.Notice(w, "repo", "Failed to reach knot server.") 446 + return 447 + } 448 + 449 + input := &tangled.RepoCreate_Input{ 450 + Rkey: rkey, 451 + Name: repoName, 452 + DefaultBranch: &defaultBranch, 453 + } 454 + if rd := strings.TrimSpace(r.FormValue("repo_did")); rd != "" { 455 + input.RepoDid = &rd 456 + } 457 + 458 + createResp, xe := tangled.RepoCreate( 459 + r.Context(), 460 + client, 461 + input, 462 + ) 463 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 464 + l.Error("xrpc error", "xe", xe) 465 + s.pages.Notice(w, "repo", err.Error()) 466 + return 467 + } 468 + 469 + var repoDid string 470 + if createResp != nil && createResp.RepoDid != nil { 471 + repoDid = *createResp.RepoDid 472 + } 473 + if repoDid == "" { 474 + l.Error("knot returned empty repo DID") 475 + s.pages.Notice(w, "repo", "Knot failed to mint a repo DID. The knot may need to be upgraded.") 476 + return 477 + } 478 + 479 repo := &models.Repo{ 480 Did: user.Active.Did, 481 Name: repoName, ··· 484 Description: description, 485 Created: time.Now(), 486 Labels: s.config.Label.DefaultLabelDefs, 487 + RepoDid: repoDid, 488 } 489 record := repo.AsRecord() 490 491 + cleanupKnot := func() { 492 + go func() { 493 + delays := []time.Duration{0, 2 * time.Second, 5 * time.Second} 494 + for attempt, delay := range delays { 495 + time.Sleep(delay) 496 + deleteClient, dErr := s.oauth.ServiceClient( 497 + r, 498 + oauth.WithService(domain), 499 + oauth.WithLxm(tangled.RepoDeleteNSID), 500 + oauth.WithDev(s.config.Core.Dev), 501 + ) 502 + if dErr != nil { 503 + l.Error("failed to create delete client for knot cleanup", "attempt", attempt+1, "err", dErr) 504 + continue 505 + } 506 + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 507 + if dErr := tangled.RepoDelete(ctx, deleteClient, &tangled.RepoDelete_Input{ 508 + Did: user.Active.Did, 509 + Name: repoName, 510 + Rkey: rkey, 511 + }); dErr != nil { 512 + cancel() 513 + l.Error("failed to clean up repo on knot after rollback", "attempt", attempt+1, "err", dErr) 514 + continue 515 + } 516 + cancel() 517 + l.Info("successfully cleaned up repo on knot after rollback", "attempt", attempt+1) 518 + return 519 + } 520 + l.Error("exhausted retries for knot cleanup, repo may be orphaned", 521 + "did", user.Active.Did, "repo", repoName, "knot", domain) 522 + }() 523 + } 524 + 525 atpClient, err := s.oauth.AuthorizedClient(r) 526 if err != nil { 527 l.Info("PDS write failed", "err", err) 528 + cleanupKnot() 529 s.pages.Notice(w, "repo", "Failed to write record to PDS.") 530 return 531 } ··· 540 }) 541 if err != nil { 542 l.Info("PDS write failed", "err", err) 543 + cleanupKnot() 544 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 545 return 546 } ··· 556 return 557 } 558 559 rollback := func() { 560 err1 := tx.Rollback() 561 err2 := s.enforcer.E.LoadPolicy() 562 err3 := rollbackRecord(context.Background(), aturi, atpClient) 563 564 if errors.Is(err1, sql.ErrTxDone) { 565 err1 = nil 566 } 567 568 if errs := errors.Join(err1, err2, err3); errs != nil { 569 l.Error("failed to rollback changes", "errs", errs) 570 } 571 572 + if aturi != "" { 573 + cleanupKnot() 574 + } 575 } 576 + defer rollback() 577 578 err = db.AddRepo(tx, repo) 579 if err != nil { ··· 582 return 583 } 584 585 + rbacPath := repo.RepoIdentifier() 586 + err = s.enforcer.AddRepo(user.Active.Did, domain, rbacPath) 587 if err != nil { 588 l.Error("acl setup failed", "err", err) 589 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 604 return 605 } 606 607 aturi = "" 608 609 s.notifier.NewRepo(r.Context(), repo) 610 + if repoDid != "" { 611 + s.pages.HxLocation(w, fmt.Sprintf("/%s", repoDid)) 612 + } else { 613 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 614 + } 615 } 616 } 617
+1 -1
appview/validator/label.go
··· 109 // validate permissions: only collaborators can apply labels currently 110 // 111 // TODO: introduce a repo:triage permission 112 - ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.DidSlashRepo()) 113 if err != nil { 114 return fmt.Errorf("failed to enforce permissions: %w", err) 115 }
··· 109 // validate permissions: only collaborators can apply labels currently 110 // 111 // TODO: introduce a repo:triage permission 112 + ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.RepoIdentifier()) 113 if err != nil { 114 return fmt.Errorf("failed to enforce permissions: %w", err) 115 }
+34 -32
go.mod
··· 18 github.com/cloudflare/cloudflare-go v0.115.0 19 github.com/cyphar/filepath-securejoin v0.4.1 20 github.com/dgraph-io/ristretto v0.2.0 21 github.com/docker/docker v28.2.2+incompatible 22 github.com/dustin/go-humanize v1.0.1 23 github.com/gliderlabs/ssh v0.3.8 ··· 31 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 32 github.com/hiddeco/sshsig v0.2.0 33 github.com/hpcloud/tail v1.0.0 34 - github.com/ipfs/go-cid v0.5.0 35 - github.com/mattn/go-sqlite3 v1.14.24 36 github.com/microcosm-cc/bluemonday v1.0.27 37 github.com/openbao/openbao/api/v2 v2.3.0 38 github.com/posthog/posthog-go v1.5.5 ··· 41 github.com/sethvargo/go-envconfig v1.1.0 42 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c 43 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef 44 - github.com/stretchr/testify v1.10.0 45 - github.com/urfave/cli/v3 v3.3.3 46 github.com/whyrusleeping/cbor-gen v0.3.1 47 github.com/yuin/goldmark v1.7.13 48 github.com/yuin/goldmark-emoji v1.0.6 49 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 50 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 51 go.abhg.dev/goldmark/mermaid v0.6.0 52 - golang.org/x/crypto v0.40.0 53 golang.org/x/image v0.31.0 54 - golang.org/x/net v0.42.0 55 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 56 gopkg.in/yaml.v3 v3.0.1 57 ) ··· 116 github.com/go-test/deep v1.1.1 // indirect 117 github.com/goccy/go-json v0.10.5 // indirect 118 github.com/gogo/protobuf v1.3.2 // indirect 119 - github.com/golang-jwt/jwt/v5 v5.2.3 // indirect 120 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 121 github.com/golang/mock v1.6.0 // indirect 122 github.com/golang/protobuf v1.5.4 // indirect ··· 136 github.com/hashicorp/hcl v1.0.1-vault-7 // indirect 137 github.com/hexops/gotextdiff v1.0.3 // indirect 138 github.com/ipfs/bbloom v0.0.4 // indirect 139 - github.com/ipfs/boxo v0.33.0 // indirect 140 - github.com/ipfs/go-block-format v0.2.2 // indirect 141 - github.com/ipfs/go-datastore v0.8.2 // indirect 142 github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 143 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 144 github.com/ipfs/go-ipld-cbor v0.2.1 // indirect 145 - github.com/ipfs/go-ipld-format v0.6.2 // indirect 146 github.com/ipfs/go-log v1.0.5 // indirect 147 - github.com/ipfs/go-log/v2 v2.6.0 // indirect 148 github.com/ipfs/go-metrics-interface v0.3.0 // indirect 149 github.com/json-iterator/go v1.1.12 // indirect 150 github.com/kevinburke/ssh_config v1.2.0 // indirect ··· 168 github.com/multiformats/go-base36 v0.2.0 // indirect 169 github.com/multiformats/go-multibase v0.2.0 // indirect 170 github.com/multiformats/go-multihash v0.2.3 // indirect 171 - github.com/multiformats/go-varint v0.0.7 // indirect 172 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 173 github.com/onsi/gomega v1.37.0 // indirect 174 github.com/opencontainers/go-digest v1.0.0 // indirect ··· 178 github.com/pkg/errors v0.9.1 // indirect 179 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 180 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 181 - github.com/prometheus/client_golang v1.22.0 // indirect 182 github.com/prometheus/client_model v0.6.2 // indirect 183 - github.com/prometheus/common v0.64.0 // indirect 184 - github.com/prometheus/procfs v0.16.1 // indirect 185 github.com/rivo/uniseg v0.4.7 // indirect 186 github.com/ryanuber/go-glob v1.0.0 // indirect 187 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 193 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 194 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 195 go.etcd.io/bbolt v1.4.0 // indirect 196 - go.opentelemetry.io/auto/sdk v1.1.0 // indirect 197 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect 198 - go.opentelemetry.io/otel v1.37.0 // indirect 199 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect 200 - go.opentelemetry.io/otel/metric v1.37.0 // indirect 201 - go.opentelemetry.io/otel/trace v1.37.0 // indirect 202 - go.opentelemetry.io/proto/otlp v1.6.0 // indirect 203 go.uber.org/atomic v1.11.0 // indirect 204 go.uber.org/multierr v1.11.0 // indirect 205 - go.uber.org/zap v1.27.0 // indirect 206 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect 207 - golang.org/x/sync v0.17.0 // indirect 208 - golang.org/x/sys v0.34.0 // indirect 209 - golang.org/x/text v0.29.0 // indirect 210 golang.org/x/time v0.12.0 // indirect 211 - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect 212 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect 213 - google.golang.org/grpc v1.73.0 // indirect 214 - google.golang.org/protobuf v1.36.6 // indirect 215 gopkg.in/fsnotify.v1 v1.4.7 // indirect 216 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 217 gopkg.in/warnings.v0 v0.1.2 // indirect
··· 18 github.com/cloudflare/cloudflare-go v0.115.0 19 github.com/cyphar/filepath-securejoin v0.4.1 20 github.com/dgraph-io/ristretto v0.2.0 21 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038 22 github.com/docker/docker v28.2.2+incompatible 23 github.com/dustin/go-humanize v1.0.1 24 github.com/gliderlabs/ssh v0.3.8 ··· 32 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 33 github.com/hiddeco/sshsig v0.2.0 34 github.com/hpcloud/tail v1.0.0 35 + github.com/ipfs/go-cid v0.6.0 36 + github.com/mattn/go-sqlite3 v1.14.34 37 github.com/microcosm-cc/bluemonday v1.0.27 38 github.com/openbao/openbao/api/v2 v2.3.0 39 github.com/posthog/posthog-go v1.5.5 ··· 42 github.com/sethvargo/go-envconfig v1.1.0 43 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c 44 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef 45 + github.com/stretchr/testify v1.11.1 46 + github.com/urfave/cli/v3 v3.6.2 47 github.com/whyrusleeping/cbor-gen v0.3.1 48 github.com/yuin/goldmark v1.7.13 49 github.com/yuin/goldmark-emoji v1.0.6 50 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 51 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 52 go.abhg.dev/goldmark/mermaid v0.6.0 53 + golang.org/x/crypto v0.48.0 54 golang.org/x/image v0.31.0 55 + golang.org/x/net v0.50.0 56 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 57 gopkg.in/yaml.v3 v3.0.1 58 ) ··· 117 github.com/go-test/deep v1.1.1 // indirect 118 github.com/goccy/go-json v0.10.5 // indirect 119 github.com/gogo/protobuf v1.3.2 // indirect 120 + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect 121 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 122 github.com/golang/mock v1.6.0 // indirect 123 github.com/golang/protobuf v1.5.4 // indirect ··· 137 github.com/hashicorp/hcl v1.0.1-vault-7 // indirect 138 github.com/hexops/gotextdiff v1.0.3 // indirect 139 github.com/ipfs/bbloom v0.0.4 // indirect 140 + github.com/ipfs/boxo v0.36.0 // indirect 141 + github.com/ipfs/go-block-format v0.2.3 // indirect 142 + github.com/ipfs/go-datastore v0.9.0 // indirect 143 github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 144 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 145 github.com/ipfs/go-ipld-cbor v0.2.1 // indirect 146 + github.com/ipfs/go-ipld-format v0.6.3 // indirect 147 github.com/ipfs/go-log v1.0.5 // indirect 148 + github.com/ipfs/go-log/v2 v2.9.1 // indirect 149 github.com/ipfs/go-metrics-interface v0.3.0 // indirect 150 github.com/json-iterator/go v1.1.12 // indirect 151 github.com/kevinburke/ssh_config v1.2.0 // indirect ··· 169 github.com/multiformats/go-base36 v0.2.0 // indirect 170 github.com/multiformats/go-multibase v0.2.0 // indirect 171 github.com/multiformats/go-multihash v0.2.3 // indirect 172 + github.com/multiformats/go-varint v0.1.0 // indirect 173 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 174 github.com/onsi/gomega v1.37.0 // indirect 175 github.com/opencontainers/go-digest v1.0.0 // indirect ··· 179 github.com/pkg/errors v0.9.1 // indirect 180 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 181 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 182 + github.com/prometheus/client_golang v1.23.2 // indirect 183 github.com/prometheus/client_model v0.6.2 // indirect 184 + github.com/prometheus/common v0.67.5 // indirect 185 + github.com/prometheus/procfs v0.19.2 // indirect 186 github.com/rivo/uniseg v0.4.7 // indirect 187 github.com/ryanuber/go-glob v1.0.0 // indirect 188 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 194 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 195 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 196 go.etcd.io/bbolt v1.4.0 // indirect 197 + go.opentelemetry.io/auto/sdk v1.2.1 // indirect 198 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect 199 + go.opentelemetry.io/otel v1.40.0 // indirect 200 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect 201 + go.opentelemetry.io/otel/metric v1.40.0 // indirect 202 + go.opentelemetry.io/otel/trace v1.40.0 // indirect 203 + go.opentelemetry.io/proto/otlp v1.9.0 // indirect 204 go.uber.org/atomic v1.11.0 // indirect 205 go.uber.org/multierr v1.11.0 // indirect 206 + go.uber.org/zap v1.27.1 // indirect 207 + go.yaml.in/yaml/v2 v2.4.3 // indirect 208 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect 209 + golang.org/x/sync v0.19.0 // indirect 210 + golang.org/x/sys v0.41.0 // indirect 211 + golang.org/x/text v0.34.0 // indirect 212 golang.org/x/time v0.12.0 // indirect 213 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect 214 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect 215 + google.golang.org/grpc v1.78.0 // indirect 216 + google.golang.org/protobuf v1.36.11 // indirect 217 gopkg.in/fsnotify.v1 v1.4.7 // indirect 218 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 219 gopkg.in/warnings.v0 v0.1.2 // indirect
+80 -74
go.sum
··· 89 github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= 90 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 91 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 92 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 93 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 94 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= ··· 138 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 139 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 140 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 141 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 142 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 143 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= ··· 209 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 210 github.com/goki/freetype v1.0.5 h1:yi2lQeUhXnBgSMqYd0vVmPw6RnnfIeTP3N4uvaJXd7A= 211 github.com/goki/freetype v1.0.5/go.mod h1:wKmKxddbzKmeci9K96Wknn5kjTWLyfC8tKOqAFbEX8E= 212 - github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= 213 - github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 214 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 215 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 216 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= ··· 260 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 261 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 262 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 263 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 264 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 265 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 266 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 267 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= ··· 294 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 295 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 296 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 297 - github.com/ipfs/boxo v0.33.0 h1:9ow3chwkDzMj0Deq4AWRUEI7WnIIV7SZhPTzzG2mmfw= 298 - github.com/ipfs/boxo v0.33.0/go.mod h1:3IPh7YFcCIcKp6o02mCHovrPntoT5Pctj/7j4syh/RM= 299 - github.com/ipfs/go-block-format v0.2.2 h1:uecCTgRwDIXyZPgYspaLXoMiMmxQpSx2aq34eNc4YvQ= 300 - github.com/ipfs/go-block-format v0.2.2/go.mod h1:vmuefuWU6b+9kIU0vZJgpiJt1yicQz9baHXE8qR+KB8= 301 - github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 302 - github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 303 - github.com/ipfs/go-datastore v0.8.2 h1:Jy3wjqQR6sg/LhyY0NIePZC3Vux19nLtg7dx0TVqr6U= 304 - github.com/ipfs/go-datastore v0.8.2/go.mod h1:W+pI1NsUsz3tcsAACMtfC+IZdnQTnC/7VfPoJBQuts0= 305 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 306 github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 307 github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= ··· 312 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 313 github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= 314 github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= 315 - github.com/ipfs/go-ipld-format v0.6.2 h1:bPZQ+A05ol0b3lsJSl0bLvwbuQ+HQbSsdGTy4xtYUkU= 316 - github.com/ipfs/go-ipld-format v0.6.2/go.mod h1:nni2xFdHKx5lxvXJ6brt/pndtGxKAE+FPR1rg4jTkyk= 317 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 318 github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 319 github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 320 - github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg= 321 - github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8= 322 github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 323 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 324 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= ··· 350 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 351 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 352 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 353 - github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 354 - github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 355 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 356 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 357 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= ··· 387 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 388 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 389 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 390 - github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 391 - github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 392 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 393 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 394 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= ··· 443 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 444 github.com/posthog/posthog-go v1.5.5 h1:2o3j7IrHbTIfxRtj4MPaXKeimuTYg49onNzNBZbwksM= 445 github.com/posthog/posthog-go v1.5.5/go.mod h1:3RqUmSnPuwmeVj/GYrS75wNGqcAKdpODiwc83xZWgdE= 446 - github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 447 - github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 448 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 449 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 450 - github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 451 - github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 452 - github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 453 - github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 454 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 455 github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 456 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= ··· 494 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 495 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 496 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 497 - github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 498 - github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 499 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 500 - github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= 501 - github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 502 github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 503 github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 504 github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= ··· 534 go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I= 535 go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 536 go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= 537 - go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 538 - go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 539 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= 540 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= 541 - go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 542 - go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 543 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= 544 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= 545 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= 546 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= 547 - go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 548 - go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 549 - go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 550 - go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 551 - go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 552 - go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 553 - go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 554 - go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 555 - go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= 556 - go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= 557 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 558 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 559 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 566 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 567 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 568 go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 569 - go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 570 - go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 571 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 572 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 573 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= ··· 575 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 576 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 577 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 578 - golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= 579 - golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 580 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 581 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 582 golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= 583 golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= 584 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 611 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 612 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 613 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 614 - golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 615 - golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 616 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 617 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 618 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 620 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 621 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 622 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 623 - golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 624 - golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 625 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 626 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 627 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 652 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 653 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 654 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 655 - golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 656 - golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 657 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 658 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 659 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= ··· 663 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 664 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 665 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 666 - golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 667 - golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 668 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 669 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 670 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ··· 675 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 676 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 677 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 678 - golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 679 - golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 680 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 681 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 682 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 702 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 703 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 704 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 705 - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= 706 - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= 707 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= 708 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 709 - google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= 710 - google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 711 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 712 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 713 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= ··· 717 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 718 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 719 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 720 - google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 721 - google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 722 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 723 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 724 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
··· 89 github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= 90 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 91 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 92 + github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= 93 + github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= 94 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 95 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 96 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= ··· 140 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 141 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 142 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 143 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038 h1:AGh+Vn9fXhf9eo8erG1CK4+LACduPo64P1OICQLDv88= 144 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038/go.mod h1:ddIXqTTSXWtj5kMsHAPj8SvbIx2GZdAkBFgFa6e6+CM= 145 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 146 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 147 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= ··· 213 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 214 github.com/goki/freetype v1.0.5 h1:yi2lQeUhXnBgSMqYd0vVmPw6RnnfIeTP3N4uvaJXd7A= 215 github.com/goki/freetype v1.0.5/go.mod h1:wKmKxddbzKmeci9K96Wknn5kjTWLyfC8tKOqAFbEX8E= 216 + github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 217 + github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 218 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 219 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 220 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= ··· 264 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 265 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 266 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 267 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= 268 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= 269 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 270 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 271 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= ··· 298 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 299 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 300 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 301 + github.com/ipfs/boxo v0.36.0 h1:DarrMBM46xCs6GU6Vz+AL8VUyXykqHAqZYx8mR0Oics= 302 + github.com/ipfs/boxo v0.36.0/go.mod h1:92hnRXfP5ScKEIqlq9Ns7LR1dFXEVADKWVGH0fjk83k= 303 + github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk= 304 + github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA= 305 + github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= 306 + github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= 307 + github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w= 308 + github.com/ipfs/go-datastore v0.9.0/go.mod h1:uT77w/XEGrvJWwHgdrMr8bqCN6ZTW9gzmi+3uK+ouHg= 309 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 310 github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 311 github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= ··· 316 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 317 github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= 318 github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= 319 + github.com/ipfs/go-ipld-format v0.6.3 h1:9/lurLDTotJpZSuL++gh3sTdmcFhVkCwsgx2+rAh4j8= 320 + github.com/ipfs/go-ipld-format v0.6.3/go.mod h1:74ilVN12NXVMIV+SrBAyC05UJRk0jVvGqdmrcYZvCBk= 321 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 322 github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 323 github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 324 + github.com/ipfs/go-log/v2 v2.9.1 h1:3JXwHWU31dsCpvQ+7asz6/QsFJHqFr4gLgQ0FWteujk= 325 + github.com/ipfs/go-log/v2 v2.9.1/go.mod h1:evFx7sBiohUN3AG12mXlZBw5hacBQld3ZPHrowlJYoo= 326 github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 327 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 328 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= ··· 354 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 355 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 356 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 357 + github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= 358 + github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 359 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 360 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 361 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= ··· 391 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 392 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 393 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 394 + github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= 395 + github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= 396 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 397 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 398 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= ··· 447 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 448 github.com/posthog/posthog-go v1.5.5 h1:2o3j7IrHbTIfxRtj4MPaXKeimuTYg49onNzNBZbwksM= 449 github.com/posthog/posthog-go v1.5.5/go.mod h1:3RqUmSnPuwmeVj/GYrS75wNGqcAKdpODiwc83xZWgdE= 450 + github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 451 + github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 452 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 453 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 454 + github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= 455 + github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= 456 + github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= 457 + github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 458 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 459 github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 460 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= ··· 498 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 499 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 500 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 501 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 502 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 503 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 504 + github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= 505 + github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 506 github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 507 github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 508 github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= ··· 538 go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I= 539 go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 540 go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= 541 + go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 542 + go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 543 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= 544 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= 545 + go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= 546 + go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= 547 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= 548 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= 549 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= 550 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= 551 + go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= 552 + go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= 553 + go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= 554 + go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= 555 + go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= 556 + go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= 557 + go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= 558 + go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= 559 + go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= 560 + go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 561 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 562 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 563 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 570 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 571 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 572 go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 573 + go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 574 + go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 575 + go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 576 + go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 577 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 578 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 579 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= ··· 581 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 582 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 583 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 584 + golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= 585 + golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= 586 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= 587 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= 588 golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= 589 golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= 590 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 617 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 618 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 619 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 620 + golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= 621 + golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= 622 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 623 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 624 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 626 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 627 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 628 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 629 + golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 630 + golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 631 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 632 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 633 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 658 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 659 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 660 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 661 + golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= 662 + golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 663 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 664 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 665 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= ··· 669 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 670 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 671 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 672 + golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= 673 + golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= 674 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 675 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 676 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ··· 681 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 682 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 683 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 684 + golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= 685 + golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 686 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 687 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 688 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 708 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 709 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 710 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 711 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= 712 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= 713 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= 714 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= 715 + google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= 716 + google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 717 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 718 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 719 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= ··· 723 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 724 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 725 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 726 + google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 727 + google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 728 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 729 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 730 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+172
knotserver/db/db.go
··· 3 import ( 4 "context" 5 "database/sql" 6 "log/slog" 7 "strings" 8 9 _ "github.com/mattn/go-sqlite3" 10 "tangled.org/core/log" 11 ) ··· 65 primary key (rkey, nsid) 66 ); 67 68 create table if not exists migrations ( 69 id integer primary key autoincrement, 70 name text unique ··· 74 return nil, err 75 } 76 77 return &DB{ 78 db: db, 79 logger: logger, 80 }, nil 81 }
··· 3 import ( 4 "context" 5 "database/sql" 6 + "fmt" 7 "log/slog" 8 + "os" 9 "strings" 10 11 + securejoin "github.com/cyphar/filepath-securejoin" 12 _ "github.com/mattn/go-sqlite3" 13 "tangled.org/core/log" 14 ) ··· 68 primary key (rkey, nsid) 69 ); 70 71 + create table if not exists repo_keys ( 72 + repo_did text primary key, 73 + signing_key blob not null, 74 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) 75 + ); 76 + 77 create table if not exists migrations ( 78 id integer primary key autoincrement, 79 name text unique ··· 83 return nil, err 84 } 85 86 + migrationCheck := func(name string) bool { 87 + var count int 88 + conn.QueryRowContext(ctx, `SELECT count(1) FROM migrations WHERE name = ?`, name).Scan(&count) 89 + return count > 0 90 + } 91 + 92 + runMigration := func(name string, fn func() error) error { 93 + if migrationCheck(name) { 94 + return nil 95 + } 96 + if err := fn(); err != nil { 97 + return fmt.Errorf("migration %q failed: %w", name, err) 98 + } 99 + _, err := conn.ExecContext(ctx, `INSERT INTO migrations (name) VALUES (?)`, name) 100 + if err != nil { 101 + return fmt.Errorf("recording migration %q: %w", name, err) 102 + } 103 + return nil 104 + } 105 + 106 + if err := runMigration("add-owner-did-to-repo-keys", func() error { 107 + _, mErr := conn.ExecContext(ctx, `ALTER TABLE repo_keys ADD COLUMN owner_did TEXT`) 108 + return mErr 109 + }); err != nil { 110 + return nil, err 111 + } 112 + 113 + if err := runMigration("add-repo-name-to-repo-keys", func() error { 114 + _, mErr := conn.ExecContext(ctx, `ALTER TABLE repo_keys ADD COLUMN repo_name TEXT`) 115 + return mErr 116 + }); err != nil { 117 + return nil, err 118 + } 119 + 120 + if err := runMigration("add-unique-owner-repo-on-repo-keys", func() error { 121 + _, mErr := conn.ExecContext(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS idx_repo_keys_owner_repo ON repo_keys(owner_did, repo_name)`) 122 + return mErr 123 + }); err != nil { 124 + return nil, err 125 + } 126 + 127 + if err := runMigration("add-key-type-and-nullable-signing-key", func() error { 128 + tx, txErr := conn.BeginTx(ctx, nil) 129 + if txErr != nil { 130 + return txErr 131 + } 132 + defer tx.Rollback() 133 + 134 + _, mErr := tx.ExecContext(ctx, ` 135 + create table repo_keys_new ( 136 + repo_did text primary key, 137 + signing_key blob, 138 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 139 + owner_did text, 140 + repo_name text, 141 + key_type text not null default 'k256' 142 + ); 143 + insert into repo_keys_new 144 + select repo_did, signing_key, created_at, owner_did, repo_name, 'k256' 145 + from repo_keys; 146 + drop table repo_keys; 147 + alter table repo_keys_new rename to repo_keys; 148 + create unique index if not exists idx_repo_keys_owner_repo 149 + on repo_keys(owner_did, repo_name); 150 + `) 151 + if mErr != nil { 152 + return mErr 153 + } 154 + return tx.Commit() 155 + }); err != nil { 156 + return nil, err 157 + } 158 + 159 return &DB{ 160 db: db, 161 logger: logger, 162 }, nil 163 } 164 + 165 + func (d *DB) StoreRepoKey(repoDid string, signingKey []byte, ownerDid, repoName string) error { 166 + _, err := d.db.Exec( 167 + `INSERT INTO repo_keys (repo_did, signing_key, owner_did, repo_name, key_type) VALUES (?, ?, ?, ?, 'k256')`, 168 + repoDid, signingKey, ownerDid, repoName, 169 + ) 170 + return err 171 + } 172 + 173 + func (d *DB) StoreRepoDidWeb(repoDid, ownerDid, repoName string) error { 174 + _, err := d.db.Exec( 175 + `INSERT INTO repo_keys (repo_did, signing_key, owner_did, repo_name, key_type) VALUES (?, NULL, ?, ?, 'web')`, 176 + repoDid, ownerDid, repoName, 177 + ) 178 + return err 179 + } 180 + 181 + func (d *DB) DeleteRepoKey(repoDid string) error { 182 + _, err := d.db.Exec(`DELETE FROM repo_keys WHERE repo_did = ?`, repoDid) 183 + return err 184 + } 185 + 186 + func (d *DB) RepoDidExists(repoDid string) (bool, error) { 187 + var count int 188 + err := d.db.QueryRow(`SELECT count(1) FROM repo_keys WHERE repo_did = ?`, repoDid).Scan(&count) 189 + return count > 0, err 190 + } 191 + 192 + func (d *DB) GetRepoDid(ownerDid, repoName string) (string, error) { 193 + var repoDid string 194 + err := d.db.QueryRow( 195 + `SELECT repo_did FROM repo_keys WHERE owner_did = ? AND repo_name = ?`, 196 + ownerDid, repoName, 197 + ).Scan(&repoDid) 198 + return repoDid, err 199 + } 200 + 201 + func (d *DB) GetRepoSigningKey(repoDid string) ([]byte, error) { 202 + var signingKey []byte 203 + err := d.db.QueryRow( 204 + `SELECT signing_key FROM repo_keys WHERE repo_did = ? AND key_type = 'k256'`, 205 + repoDid, 206 + ).Scan(&signingKey) 207 + if err != nil { 208 + return nil, fmt.Errorf("retrieving signing key for %s: %w", repoDid, err) 209 + } 210 + if signingKey == nil { 211 + return nil, fmt.Errorf("signing key for %s is null (did:web repo?)", repoDid) 212 + } 213 + return signingKey, nil 214 + } 215 + 216 + func (d *DB) GetRepoKeyOwner(repoDid string) (ownerDid string, repoName string, err error) { 217 + var nullOwner, nullName sql.NullString 218 + err = d.db.QueryRow( 219 + `SELECT owner_did, repo_name FROM repo_keys WHERE repo_did = ?`, 220 + repoDid, 221 + ).Scan(&nullOwner, &nullName) 222 + if err != nil { 223 + return 224 + } 225 + if !nullOwner.Valid || !nullName.Valid || nullOwner.String == "" || nullName.String == "" { 226 + err = fmt.Errorf("repo_keys row for %s has empty or null owner_did or repo_name", repoDid) 227 + return 228 + } 229 + ownerDid = nullOwner.String 230 + repoName = nullName.String 231 + return 232 + } 233 + 234 + func (d *DB) ResolveRepoDIDOnDisk(scanPath, repoDid string) (repoPath, ownerDid, repoName string, err error) { 235 + ownerDid, repoName, err = d.GetRepoKeyOwner(repoDid) 236 + if err != nil { 237 + return 238 + } 239 + 240 + didPath, joinErr := securejoin.SecureJoin(scanPath, repoDid) 241 + if joinErr != nil { 242 + err = fmt.Errorf("securejoin failed for repo DID path %s: %w", repoDid, joinErr) 243 + return 244 + } 245 + 246 + if _, statErr := os.Stat(didPath); statErr != nil { 247 + err = fmt.Errorf("repo DID directory not found on disk: %s", didPath) 248 + return 249 + } 250 + 251 + repoPath = didPath 252 + return 253 + }
+30 -24
knotserver/git.go
··· 5 "fmt" 6 "io" 7 "net/http" 8 - "path/filepath" 9 "strings" 10 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 "github.com/go-chi/chi/v5" 13 "tangled.org/core/knotserver/git/service" 14 ) 15 16 - func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 17 did := chi.URLParam(r, "did") 18 name := chi.URLParam(r, "name") 19 - repoName, err := securejoin.SecureJoin(did, name) 20 if err != nil { 21 - gitError(w, "repository not found", http.StatusNotFound) 22 - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 23 - return 24 } 25 26 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repoName) 27 if err != nil { 28 gitError(w, "repository not found", http.StatusNotFound) 29 - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 30 return 31 } 32 ··· 57 } 58 59 func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { 60 - did := chi.URLParam(r, "did") 61 - name := chi.URLParam(r, "name") 62 - repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 63 if err != nil { 64 - gitError(w, err.Error(), http.StatusInternalServerError) 65 - h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 66 return 67 } 68 ··· 104 } 105 106 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 107 - did := chi.URLParam(r, "did") 108 - name := chi.URLParam(r, "name") 109 - repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 110 if err != nil { 111 - gitError(w, err.Error(), http.StatusInternalServerError) 112 - h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 113 return 114 } 115 ··· 153 } 154 155 func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { 156 - did := chi.URLParam(r, "did") 157 - name := chi.URLParam(r, "name") 158 - _, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 159 if err != nil { 160 - gitError(w, err.Error(), http.StatusForbidden) 161 - h.l.Error("git: failed to secure join repo path", "handler", "ReceivePack", "error", err) 162 return 163 } 164
··· 5 "fmt" 6 "io" 7 "net/http" 8 "strings" 9 10 "github.com/go-chi/chi/v5" 11 "tangled.org/core/knotserver/git/service" 12 ) 13 14 + func (h *Knot) resolveRepoPath(r *http.Request) (string, string, error) { 15 did := chi.URLParam(r, "did") 16 name := chi.URLParam(r, "name") 17 + 18 + if name == "" && strings.HasPrefix(did, "did:") { 19 + repoPath, _, repoName, err := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, did) 20 + if err != nil { 21 + return "", "", fmt.Errorf("unknown repo DID: %w", err) 22 + } 23 + return repoPath, repoName, nil 24 + } 25 + 26 + repoDid, err := h.db.GetRepoDid(did, name) 27 + if err != nil { 28 + return "", "", fmt.Errorf("repo not found: %w", err) 29 + } 30 + repoPath, _, _, err := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 31 if err != nil { 32 + return "", "", fmt.Errorf("repo not found: %w", err) 33 } 34 + return repoPath, name, nil 35 + } 36 37 + func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 38 + repoPath, name, err := h.resolveRepoPath(r) 39 if err != nil { 40 gitError(w, "repository not found", http.StatusNotFound) 41 + h.l.Error("git: failed to resolve repo path", "handler", "InfoRefs", "error", err) 42 return 43 } 44 ··· 69 } 70 71 func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { 72 + repo, _, err := h.resolveRepoPath(r) 73 if err != nil { 74 + gitError(w, "repository not found", http.StatusNotFound) 75 + h.l.Error("git: failed to resolve repo path", "handler", "UploadArchive", "error", err) 76 return 77 } 78 ··· 114 } 115 116 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 117 + repo, _, err := h.resolveRepoPath(r) 118 if err != nil { 119 + gitError(w, "repository not found", http.StatusNotFound) 120 + h.l.Error("git: failed to resolve repo path", "handler", "UploadPack", "error", err) 121 return 122 } 123 ··· 161 } 162 163 func (h *Knot) ReceivePack(w http.ResponseWriter, r *http.Request) { 164 + _, name, err := h.resolveRepoPath(r) 165 if err != nil { 166 + gitError(w, "repository not found", http.StatusNotFound) 167 + h.l.Error("git: failed to resolve repo path", "handler", "ReceivePack", "error", err) 168 return 169 } 170
+95 -57
knotserver/ingester.go
··· 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "github.com/bluesky-social/indigo/xrpc" 16 "github.com/bluesky-social/jetstream/pkg/models" 17 - securejoin "github.com/cyphar/filepath-securejoin" 18 "tangled.org/core/api/tangled" 19 "tangled.org/core/knotserver/db" 20 "tangled.org/core/knotserver/git" ··· 102 return fmt.Errorf("ignoring pull record: target repo is nil") 103 } 104 105 - l = l.With("target_repo", record.Target.Repo) 106 l = l.With("target_branch", record.Target.Branch) 107 108 if record.Source == nil { 109 return fmt.Errorf("ignoring pull record: not a branch-based pull request") 110 } 111 112 - if record.Source.Repo != nil { 113 return fmt.Errorf("ignoring pull record: fork based pull") 114 } 115 116 - repoAt, err := syntax.ParseATURI(record.Target.Repo) 117 - if err != nil { 118 - return fmt.Errorf("failed to parse ATURI: %w", err) 119 - } 120 121 - // resolve this aturi to extract the repo record 122 - ident, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) 123 - if err != nil || ident.Handle.IsInvalidHandle() { 124 - return fmt.Errorf("failed to resolve handle: %w", err) 125 - } 126 127 - xrpcc := xrpc.Client{ 128 - Host: ident.PDSEndpoint(), 129 - } 130 131 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 132 - if err != nil { 133 - return fmt.Errorf("failed to resolver repo: %w", err) 134 - } 135 136 - repo := resp.Value.Val.(*tangled.Repo) 137 138 - if repo.Knot != h.c.Server.Hostname { 139 - return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname) 140 - } 141 142 - didSlashRepo, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 143 - if err != nil { 144 - return fmt.Errorf("failed to construct relative repo path: %w", err) 145 - } 146 147 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 148 - if err != nil { 149 - return fmt.Errorf("failed to construct absolute repo path: %w", err) 150 } 151 152 gr, err := git.Open(repoPath, record.Source.Sha) ··· 189 Kind: string(workflow.TriggerKindPullRequest), 190 PullRequest: &trigger, 191 Repo: &tangled.Pipeline_TriggerRepo{ 192 - Did: ident.DID.String(), 193 - Knot: repo.Knot, 194 - Repo: repo.Name, 195 }, 196 }, 197 } ··· 226 return fmt.Errorf("failed to unmarshal record: %w", err) 227 } 228 229 - repoAt, err := syntax.ParseATURI(record.Repo) 230 - if err != nil { 231 - return err 232 - } 233 - 234 subjectId, err := h.resolver.ResolveIdent(ctx, record.Subject) 235 if err != nil || subjectId.Handle.IsInvalidHandle() { 236 return err 237 } 238 239 - // TODO: fix this for good, we need to fetch the record here unfortunately 240 - // resolve this aturi to extract the repo record 241 - owner, err := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) 242 - if err != nil || owner.Handle.IsInvalidHandle() { 243 - return fmt.Errorf("failed to resolve handle: %w", err) 244 - } 245 246 - xrpcc := xrpc.Client{ 247 - Host: owner.PDSEndpoint(), 248 - } 249 250 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 251 - if err != nil { 252 - return err 253 - } 254 255 - repo := resp.Value.Val.(*tangled.Repo) 256 - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 257 258 - // check perms for this user 259 - ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo) 260 if err != nil { 261 return fmt.Errorf("failed to check permissions: %w", err) 262 } 263 if !ok { 264 - return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", didSlashRepo) 265 } 266 267 if err := h.db.AddDid(subjectId.DID.String()); err != nil { ··· 269 } 270 h.jc.AddDid(subjectId.DID.String()) 271 272 - if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, didSlashRepo); err != nil { 273 return err 274 } 275
··· 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 "github.com/bluesky-social/indigo/xrpc" 16 "github.com/bluesky-social/jetstream/pkg/models" 17 "tangled.org/core/api/tangled" 18 "tangled.org/core/knotserver/db" 19 "tangled.org/core/knotserver/git" ··· 101 return fmt.Errorf("ignoring pull record: target repo is nil") 102 } 103 104 + l = l.With("target_repo", record.Target.Repo, "target_repo_did", record.Target.RepoDid) 105 l = l.With("target_branch", record.Target.Branch) 106 107 if record.Source == nil { 108 return fmt.Errorf("ignoring pull record: not a branch-based pull request") 109 } 110 111 + if record.Source.Repo != nil || record.Source.RepoDid != nil { 112 return fmt.Errorf("ignoring pull record: fork based pull") 113 } 114 115 + var repoPath, ownerDid, repoName, repoDid string 116 + switch { 117 + case record.Target.RepoDid != nil && *record.Target.RepoDid != "": 118 + repoDid = *record.Target.RepoDid 119 + var lookupErr error 120 + repoPath, ownerDid, repoName, lookupErr = h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 121 + if lookupErr != nil { 122 + return fmt.Errorf("unknown target repo DID %s: %w", repoDid, lookupErr) 123 + } 124 125 + case record.Target.Repo != nil: 126 + // TODO: get rid of this PDS fetch once all repos have DIDs 127 + repoAt, parseErr := syntax.ParseATURI(*record.Target.Repo) 128 + if parseErr != nil { 129 + return fmt.Errorf("failed to parse ATURI: %w", parseErr) 130 + } 131 + 132 + ident, resolveErr := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) 133 + if resolveErr != nil || ident.Handle.IsInvalidHandle() { 134 + return fmt.Errorf("failed to resolve handle: %w", resolveErr) 135 + } 136 + 137 + xrpcc := xrpc.Client{ 138 + Host: ident.PDSEndpoint(), 139 + } 140 + 141 + resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 142 + if getErr != nil { 143 + return fmt.Errorf("failed to resolve repo: %w", getErr) 144 + } 145 146 + repo := resp.Value.Val.(*tangled.Repo) 147 148 + if repo.Knot != h.c.Server.Hostname { 149 + return fmt.Errorf("rejected pull record: not this knot, %s != %s", repo.Knot, h.c.Server.Hostname) 150 + } 151 152 + ownerDid = ident.DID.String() 153 + repoName = repo.Name 154 155 + repoDid, didErr := h.db.GetRepoDid(ownerDid, repoName) 156 + if didErr != nil { 157 + return fmt.Errorf("failed to resolve repo DID for %s/%s: %w", ownerDid, repoName, didErr) 158 + } 159 160 + var lookupErr error 161 + repoPath, _, _, lookupErr = h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 162 + if lookupErr != nil { 163 + return fmt.Errorf("failed to resolve repo on disk: %w", lookupErr) 164 + } 165 166 + default: 167 + return fmt.Errorf("ignoring pull record: target has neither repo nor repoDid") 168 } 169 170 gr, err := git.Open(repoPath, record.Source.Sha) ··· 207 Kind: string(workflow.TriggerKindPullRequest), 208 PullRequest: &trigger, 209 Repo: &tangled.Pipeline_TriggerRepo{ 210 + Did: ownerDid, 211 + Knot: h.c.Server.Hostname, 212 + Repo: repoName, 213 + RepoDid: repoDid, 214 }, 215 }, 216 } ··· 245 return fmt.Errorf("failed to unmarshal record: %w", err) 246 } 247 248 subjectId, err := h.resolver.ResolveIdent(ctx, record.Subject) 249 if err != nil || subjectId.Handle.IsInvalidHandle() { 250 return err 251 } 252 253 + var rbacResource string 254 + switch { 255 + case record.RepoDid != nil && *record.RepoDid != "": 256 + ownerDid, _, lookupErr := h.db.GetRepoKeyOwner(*record.RepoDid) 257 + if lookupErr != nil { 258 + return fmt.Errorf("unknown repo DID %s: %w", *record.RepoDid, lookupErr) 259 + } 260 + if ownerDid != did { 261 + return fmt.Errorf("collaborator record author %s does not own repo %s", did, *record.RepoDid) 262 + } 263 + rbacResource = *record.RepoDid 264 265 + case record.Repo != nil: 266 + // TODO: get rid of this PDS fetch once all repos have DIDs 267 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 268 + if parseErr != nil { 269 + return parseErr 270 + } 271 272 + owner, resolveErr := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) 273 + if resolveErr != nil || owner.Handle.IsInvalidHandle() { 274 + return fmt.Errorf("failed to resolve handle: %w", resolveErr) 275 + } 276 277 + xrpcc := xrpc.Client{ 278 + Host: owner.PDSEndpoint(), 279 + } 280 281 + resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 282 + if getErr != nil { 283 + return getErr 284 + } 285 + 286 + repo := resp.Value.Val.(*tangled.Repo) 287 + repoDid, didErr := h.db.GetRepoDid(owner.DID.String(), repo.Name) 288 + if didErr != nil { 289 + return fmt.Errorf("failed to resolve repo DID for %s/%s: %w", owner.DID.String(), repo.Name, didErr) 290 + } 291 + rbacResource = repoDid 292 + 293 + default: 294 + return fmt.Errorf("collaborator record has neither repo nor repoDid") 295 + } 296 + 297 + ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, rbacResource) 298 if err != nil { 299 return fmt.Errorf("failed to check permissions: %w", err) 300 } 301 if !ok { 302 + return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", rbacResource) 303 } 304 305 if err := h.db.AddDid(subjectId.DID.String()); err != nil { ··· 307 } 308 h.jc.AddDid(subjectId.DID.String()) 309 310 + if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, rbacResource); err != nil { 311 return err 312 } 313
+110 -74
knotserver/internal.go
··· 3 import ( 4 "context" 5 "encoding/json" 6 - "errors" 7 "fmt" 8 "log/slog" 9 "net/http" 10 "path/filepath" 11 "strings" 12 13 - securejoin "github.com/cyphar/filepath-securejoin" 14 "github.com/go-chi/chi/v5" 15 "github.com/go-chi/chi/v5/middleware" 16 "github.com/go-git/go-git/v5/plumbing" ··· 72 // the body will be qualified repository path on success/push-denied 73 // or an error message when process failed 74 func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) { 75 - l := h.l.With("handler", "PostReceiveHook") 76 77 var ( 78 incomingUser = r.URL.Query().Get("user") ··· 87 return 88 } 89 90 - // did:foo/repo-name or 91 - // handle/repo-name or 92 - // any of the above with a leading slash (/) 93 components := strings.Split(strings.TrimPrefix(strings.Trim(repo, "'"), "/"), "/") 94 l.Info("command components", "components", components) 95 96 - if len(components) != 2 { 97 - w.WriteHeader(http.StatusBadRequest) 98 - l.Error("invalid repo format", "components", components) 99 - fmt.Fprintln(w, "invalid repo format, needs <user>/<repo> or /<user>/<repo>") 100 - return 101 - } 102 - repoOwner := components[0] 103 - repoName := components[1] 104 105 - resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl) 106 107 - repoOwnerIdent, err := resolver.ResolveIdent(r.Context(), repoOwner) 108 - if err != nil || repoOwnerIdent.Handle.IsInvalidHandle() { 109 - l.Error("Error resolving handle", "handle", repoOwner, "err", err) 110 - w.WriteHeader(http.StatusInternalServerError) 111 - fmt.Fprintf(w, "error resolving handle: invalid handle\n") 112 return 113 } 114 - repoOwnerDid := repoOwnerIdent.DID.String() 115 - 116 - qualifiedRepo, _ := securejoin.SecureJoin(repoOwnerDid, repoName) 117 118 if gitCommand == "git-receive-pack" { 119 - ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, qualifiedRepo) 120 if err != nil || !ok { 121 w.WriteHeader(http.StatusForbidden) 122 fmt.Fprint(w, repo) ··· 125 } 126 127 w.WriteHeader(http.StatusOK) 128 - fmt.Fprint(w, qualifiedRepo) 129 } 130 131 type PushOptions struct { ··· 140 gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir) 141 if err != nil { 142 l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir) 143 return 144 } 145 146 - parts := strings.SplitN(gitRelativeDir, "/", 2) 147 - if len(parts) != 2 { 148 - l.Error("invalid git dir", "gitRelativeDir", gitRelativeDir) 149 return 150 } 151 - repoDid := parts[0] 152 - repoName := parts[1] 153 154 gitUserDid := r.Header.Get("X-Git-User-Did") 155 ··· 176 } 177 178 for _, line := range lines { 179 - err := h.insertRefUpdate(line, gitUserDid, repoDid, repoName) 180 if err != nil { 181 l.Error("failed to insert op", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 182 - // non-fatal 183 } 184 185 - err = h.emitCompareLink(&resp.Messages, line, repoDid, repoName) 186 if err != nil { 187 l.Error("failed to reply with compare link", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 188 - // non-fatal 189 } 190 191 - err = h.triggerPipeline(&resp.Messages, line, gitUserDid, repoDid, repoName, pushOptions) 192 if err != nil { 193 l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 194 - // non-fatal 195 } 196 } 197 198 writeJSON(w, resp) 199 } 200 201 - func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, repoDid, repoName string) error { 202 - didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) 203 - if err != nil { 204 - return err 205 - } 206 - 207 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 208 - if err != nil { 209 - return err 210 } 211 212 gr, err := git.Open(repoPath, line.Ref) ··· 214 return fmt.Errorf("failed to open git repo at ref %s: %w", line.Ref, err) 215 } 216 217 - var errs error 218 meta, err := gr.RefUpdateMeta(line) 219 - errors.Join(errs, err) 220 221 metaRecord := meta.AsRecord() 222 ··· 225 NewSha: line.NewSha.String(), 226 Ref: line.Ref, 227 CommitterDid: gitUserDid, 228 - RepoDid: repoDid, 229 RepoName: repoName, 230 Meta: &metaRecord, 231 } 232 eventJson, err := json.Marshal(refUpdate) 233 if err != nil { 234 return err ··· 240 EventJson: string(eventJson), 241 } 242 243 - return errors.Join(errs, h.db.InsertEvent(event, h.n)) 244 } 245 246 func (h *InternalHandle) triggerPipeline( 247 clientMsgs *[]string, 248 line git.PostReceiveLine, 249 gitUserDid string, 250 repoDid string, 251 - repoName string, 252 pushOptions PushOptions, 253 ) error { 254 if pushOptions.skipCi { 255 return nil 256 } 257 258 - didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) 259 - if err != nil { 260 - return err 261 - } 262 - 263 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 264 - if err != nil { 265 - return err 266 } 267 268 gr, err := git.Open(repoPath, line.Ref) ··· 299 NewSha: line.NewSha.String(), 300 } 301 302 compiler := workflow.Compiler{ 303 Trigger: tangled.Pipeline_TriggerMetadata{ 304 Kind: string(workflow.TriggerKindPush), 305 Push: &trigger, 306 - Repo: &tangled.Pipeline_TriggerRepo{ 307 - Did: repoDid, 308 - Knot: h.c.Server.Hostname, 309 - Repo: repoName, 310 - }, 311 }, 312 } 313 ··· 348 func (h *InternalHandle) emitCompareLink( 349 clientMsgs *[]string, 350 line git.PostReceiveLine, 351 repoDid string, 352 - repoName string, 353 ) error { 354 // this is a second push to a branch, don't reply with the link again 355 if !line.OldSha.IsZero() { ··· 365 366 pushedRef := plumbing.ReferenceName(line.Ref) 367 368 - userIdent, err := h.res.ResolveIdent(context.Background(), repoDid) 369 - user := repoDid 370 if err == nil { 371 user = userIdent.Handle.String() 372 } 373 374 - didSlashRepo, err := securejoin.SecureJoin(repoDid, repoName) 375 - if err != nil { 376 - return err 377 - } 378 - 379 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 380 - if err != nil { 381 - return err 382 } 383 384 gr, err := git.PlainOpen(repoPath)
··· 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "log/slog" 8 "net/http" 9 "path/filepath" 10 "strings" 11 12 "github.com/go-chi/chi/v5" 13 "github.com/go-chi/chi/v5/middleware" 14 "github.com/go-git/go-git/v5/plumbing" ··· 70 // the body will be qualified repository path on success/push-denied 71 // or an error message when process failed 72 func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) { 73 + l := h.l.With("handler", "Guard") 74 75 var ( 76 incomingUser = r.URL.Query().Get("user") ··· 85 return 86 } 87 88 components := strings.Split(strings.TrimPrefix(strings.Trim(repo, "'"), "/"), "/") 89 l.Info("command components", "components", components) 90 91 + var rbacResource string 92 + var diskRelative string 93 94 + switch { 95 + case len(components) == 1 && strings.HasPrefix(components[0], "did:"): 96 + repoDid := components[0] 97 + repoPath, _, _, lookupErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 98 + if lookupErr != nil { 99 + w.WriteHeader(http.StatusNotFound) 100 + l.Error("repo DID not found", "repoDid", repoDid, "err", lookupErr) 101 + fmt.Fprintln(w, "repo not found") 102 + return 103 + } 104 + rbacResource = repoDid 105 + rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath) 106 + if relErr != nil { 107 + w.WriteHeader(http.StatusInternalServerError) 108 + l.Error("failed to compute relative path", "repoPath", repoPath, "err", relErr) 109 + fmt.Fprintln(w, "internal error") 110 + return 111 + } 112 + diskRelative = rel 113 114 + case len(components) == 2: 115 + repoOwner := components[0] 116 + resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl) 117 + repoOwnerIdent, resolveErr := resolver.ResolveIdent(r.Context(), repoOwner) 118 + if resolveErr != nil || repoOwnerIdent.Handle.IsInvalidHandle() { 119 + l.Error("Error resolving handle", "handle", repoOwner, "err", resolveErr) 120 + w.WriteHeader(http.StatusInternalServerError) 121 + fmt.Fprintf(w, "error resolving handle: invalid handle\n") 122 + return 123 + } 124 + ownerDid := repoOwnerIdent.DID.String() 125 + repoName := components[1] 126 + repoDid, didErr := h.db.GetRepoDid(ownerDid, repoName) 127 + if didErr != nil { 128 + w.WriteHeader(http.StatusNotFound) 129 + l.Error("repo DID not found", "owner", ownerDid, "name", repoName, "err", didErr) 130 + fmt.Fprintln(w, "repo not found") 131 + return 132 + } 133 + repoPath, _, _, lookupErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 134 + if lookupErr != nil { 135 + w.WriteHeader(http.StatusNotFound) 136 + l.Error("repo not found on disk", "repoDid", repoDid, "err", lookupErr) 137 + fmt.Fprintln(w, "repo not found") 138 + return 139 + } 140 + rbacResource = repoDid 141 + rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath) 142 + if relErr != nil { 143 + w.WriteHeader(http.StatusInternalServerError) 144 + l.Error("failed to compute relative path", "repoPath", repoPath, "err", relErr) 145 + fmt.Fprintln(w, "internal error") 146 + return 147 + } 148 + diskRelative = rel 149 + 150 + default: 151 + w.WriteHeader(http.StatusBadRequest) 152 + l.Error("invalid repo format", "components", components) 153 + fmt.Fprintln(w, "invalid repo format, needs <user>/<repo>, /<user>/<repo>, or <repo-did>") 154 return 155 } 156 157 if gitCommand == "git-receive-pack" { 158 + ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, rbacResource) 159 if err != nil || !ok { 160 w.WriteHeader(http.StatusForbidden) 161 fmt.Fprint(w, repo) ··· 164 } 165 166 w.WriteHeader(http.StatusOK) 167 + fmt.Fprint(w, diskRelative) 168 } 169 170 type PushOptions struct { ··· 179 gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir) 180 if err != nil { 181 l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir) 182 + w.WriteHeader(http.StatusInternalServerError) 183 return 184 } 185 186 + repoDid := gitRelativeDir 187 + if !strings.HasPrefix(repoDid, "did:") { 188 + l.Error("invalid git dir, expected repo DID", "gitRelativeDir", gitRelativeDir) 189 + w.WriteHeader(http.StatusBadRequest) 190 return 191 } 192 + 193 + ownerDid, repoName, err := h.db.GetRepoKeyOwner(repoDid) 194 + if err != nil { 195 + l.Error("failed to resolve repo DID from git dir", "repoDid", repoDid, "err", err) 196 + w.WriteHeader(http.StatusBadRequest) 197 + return 198 + } 199 200 gitUserDid := r.Header.Get("X-Git-User-Did") 201 ··· 222 } 223 224 for _, line := range lines { 225 + err := h.insertRefUpdate(line, gitUserDid, ownerDid, repoName, repoDid) 226 if err != nil { 227 l.Error("failed to insert op", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 228 } 229 230 + err = h.emitCompareLink(&resp.Messages, line, ownerDid, repoName, repoDid) 231 if err != nil { 232 l.Error("failed to reply with compare link", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 233 } 234 235 + err = h.triggerPipeline(&resp.Messages, line, gitUserDid, ownerDid, repoName, repoDid, pushOptions) 236 if err != nil { 237 l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 238 } 239 } 240 241 writeJSON(w, resp) 242 } 243 244 + func (h *InternalHandle) insertRefUpdate(line git.PostReceiveLine, gitUserDid, ownerDid, repoName, repoDid string) error { 245 + repoPath, _, _, resolveErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 246 + if resolveErr != nil { 247 + return fmt.Errorf("failed to resolve repo on disk: %w", resolveErr) 248 } 249 250 gr, err := git.Open(repoPath, line.Ref) ··· 252 return fmt.Errorf("failed to open git repo at ref %s: %w", line.Ref, err) 253 } 254 255 meta, err := gr.RefUpdateMeta(line) 256 + if err != nil { 257 + return fmt.Errorf("failed to get ref update metadata: %w", err) 258 + } 259 260 metaRecord := meta.AsRecord() 261 ··· 264 NewSha: line.NewSha.String(), 265 Ref: line.Ref, 266 CommitterDid: gitUserDid, 267 + OwnerDid: ownerDid, 268 RepoName: repoName, 269 + RepoDid: repoDid, 270 Meta: &metaRecord, 271 } 272 + 273 eventJson, err := json.Marshal(refUpdate) 274 if err != nil { 275 return err ··· 281 EventJson: string(eventJson), 282 } 283 284 + return h.db.InsertEvent(event, h.n) 285 } 286 287 func (h *InternalHandle) triggerPipeline( 288 clientMsgs *[]string, 289 line git.PostReceiveLine, 290 gitUserDid string, 291 + ownerDid string, 292 + repoName string, 293 repoDid string, 294 pushOptions PushOptions, 295 ) error { 296 if pushOptions.skipCi { 297 return nil 298 } 299 300 + repoPath, _, _, resolveErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 301 + if resolveErr != nil { 302 + return fmt.Errorf("failed to resolve repo on disk: %w", resolveErr) 303 } 304 305 gr, err := git.Open(repoPath, line.Ref) ··· 336 NewSha: line.NewSha.String(), 337 } 338 339 + triggerRepo := &tangled.Pipeline_TriggerRepo{ 340 + Did: ownerDid, 341 + Knot: h.c.Server.Hostname, 342 + Repo: repoName, 343 + RepoDid: repoDid, 344 + } 345 + 346 compiler := workflow.Compiler{ 347 Trigger: tangled.Pipeline_TriggerMetadata{ 348 Kind: string(workflow.TriggerKindPush), 349 Push: &trigger, 350 + Repo: triggerRepo, 351 }, 352 } 353 ··· 388 func (h *InternalHandle) emitCompareLink( 389 clientMsgs *[]string, 390 line git.PostReceiveLine, 391 + ownerDid string, 392 + repoName string, 393 repoDid string, 394 ) error { 395 // this is a second push to a branch, don't reply with the link again 396 if !line.OldSha.IsZero() { ··· 406 407 pushedRef := plumbing.ReferenceName(line.Ref) 408 409 + userIdent, err := h.res.ResolveIdent(context.Background(), ownerDid) 410 + user := ownerDid 411 if err == nil { 412 user = userIdent.Handle.String() 413 } 414 415 + repoPath, _, _, resolveErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 416 + if resolveErr != nil { 417 + return fmt.Errorf("failed to resolve repo on disk: %w", resolveErr) 418 } 419 420 gr, err := git.PlainOpen(repoPath)
+122
knotserver/repodid/repodid.go
···
··· 1 + package repodid 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "net/url" 7 + "strings" 8 + 9 + atcrypto "github.com/bluesky-social/indigo/atproto/crypto" 10 + "github.com/did-method-plc/go-didplc" 11 + "tangled.org/core/idresolver" 12 + ) 13 + 14 + type PreparedDID struct { 15 + RepoDid string 16 + SigningKeyRaw []byte 17 + op *didplc.RegularOp 18 + plcUrl string 19 + } 20 + 21 + func PrepareRepoDID(plcUrl, knotServiceUrl string) (*PreparedDID, error) { 22 + if plcUrl == "" { 23 + return nil, fmt.Errorf("PLC directory URL is not configured") 24 + } 25 + parsed, parseErr := url.Parse(plcUrl) 26 + if parseErr != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { 27 + return nil, fmt.Errorf("PLC directory URL is invalid: %q", plcUrl) 28 + } 29 + 30 + privKey, err := atcrypto.GeneratePrivateKeyK256() 31 + if err != nil { 32 + return nil, fmt.Errorf("generating signing key: %w", err) 33 + } 34 + 35 + pubKey, err := privKey.PublicKey() 36 + if err != nil { 37 + return nil, fmt.Errorf("deriving public key: %w", err) 38 + } 39 + 40 + didKey := pubKey.DIDKey() 41 + 42 + op := didplc.RegularOp{ 43 + Type: "plc_operation", 44 + RotationKeys: []string{didKey}, 45 + VerificationMethods: map[string]string{ 46 + "atproto": didKey, 47 + }, 48 + AlsoKnownAs: []string{}, 49 + Services: map[string]didplc.OpService{ 50 + "atproto_pds": { 51 + Type: "AtprotoPersonalDataServer", 52 + Endpoint: knotServiceUrl, 53 + }, 54 + }, 55 + Prev: nil, 56 + } 57 + 58 + if err := op.Sign(privKey); err != nil { 59 + return nil, fmt.Errorf("signing genesis op: %w", err) 60 + } 61 + 62 + repoDid, err := op.DID() 63 + if err != nil { 64 + return nil, fmt.Errorf("deriving DID from genesis: %w", err) 65 + } 66 + 67 + return &PreparedDID{ 68 + RepoDid: repoDid, 69 + SigningKeyRaw: privKey.Bytes(), 70 + op: &op, 71 + plcUrl: plcUrl, 72 + }, nil 73 + } 74 + 75 + func (p *PreparedDID) Submit(ctx context.Context) error { 76 + plcClient := didplc.Client{ 77 + DirectoryURL: p.plcUrl, 78 + UserAgent: "tangled-knot", 79 + } 80 + if err := plcClient.Submit(ctx, p.RepoDid, p.op); err != nil { 81 + return fmt.Errorf("submitting to PLC directory: %w", err) 82 + } 83 + return nil 84 + } 85 + 86 + const maxDidWebLength = 256 87 + 88 + func VerifyRepoDIDWeb(ctx context.Context, resolver *idresolver.Resolver, repoDid, knotServiceUrl string) error { 89 + if !strings.HasPrefix(repoDid, "did:web:") { 90 + return fmt.Errorf("expected did:web, got: %s", repoDid) 91 + } 92 + 93 + if len(repoDid) > maxDidWebLength { 94 + return fmt.Errorf("did:web exceeds maximum length of %d characters", maxDidWebLength) 95 + } 96 + 97 + authority := strings.TrimPrefix(repoDid, "did:web:") 98 + if colonIdx := strings.IndexByte(authority, ':'); colonIdx >= 0 { 99 + authority = authority[:colonIdx] 100 + } 101 + if authority == "" || strings.ContainsAny(authority, "/#?@ ") { 102 + return fmt.Errorf("did:web has invalid authority: %s", repoDid) 103 + } 104 + 105 + ident, err := resolver.ResolveIdent(ctx, repoDid) 106 + if err != nil { 107 + return fmt.Errorf("resolving did:web document: %w", err) 108 + } 109 + 110 + if strings.TrimRight(ident.PDSEndpoint(), "/") != strings.TrimRight(knotServiceUrl, "/") { 111 + return fmt.Errorf( 112 + "did:web service endpoint %q does not match this knot %q", 113 + ident.PDSEndpoint(), knotServiceUrl, 114 + ) 115 + } 116 + 117 + if _, err := ident.PublicKey(); err != nil { 118 + return fmt.Errorf("did:web document missing valid atproto verification method: %w", err) 119 + } 120 + 121 + return nil 122 + }
+10 -2
knotserver/router.go
··· 81 82 r.Route("/{did}", func(r chi.Router) { 83 r.Use(h.resolveDidRedirect) 84 r.Route("/{name}", func(r chi.Router) { 85 - // routes for git operations 86 r.Get("/info/refs", h.InfoRefs) 87 r.Post("/git-upload-archive", h.UploadArchive) 88 r.Post("/git-upload-pack", h.UploadPack) ··· 136 } 137 138 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 139 - newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery) 140 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 141 }) 142 }
··· 81 82 r.Route("/{did}", func(r chi.Router) { 83 r.Use(h.resolveDidRedirect) 84 + 85 + r.Get("/info/refs", h.InfoRefs) 86 + r.Post("/git-upload-archive", h.UploadArchive) 87 + r.Post("/git-upload-pack", h.UploadPack) 88 + r.Post("/git-receive-pack", h.ReceivePack) 89 + 90 r.Route("/{name}", func(r chi.Router) { 91 r.Get("/info/refs", h.InfoRefs) 92 r.Post("/git-upload-archive", h.UploadArchive) 93 r.Post("/git-upload-pack", h.UploadPack) ··· 141 } 142 143 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 144 + newPath := "/" + id.DID.String() + suffix 145 + if r.URL.RawQuery != "" { 146 + newPath += "?" + r.URL.RawQuery 147 + } 148 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 149 }) 150 }
+124 -24
knotserver/xrpc/create_repo.go
··· 1 package xrpc 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 - "path/filepath" 9 "strings" 10 11 - comatproto "github.com/bluesky-social/indigo/api/atproto" 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 - "github.com/bluesky-social/indigo/xrpc" 14 securejoin "github.com/cyphar/filepath-securejoin" 15 gogit "github.com/go-git/go-git/v5" 16 "tangled.org/core/api/tangled" 17 "tangled.org/core/hook" 18 "tangled.org/core/knotserver/git" 19 "tangled.org/core/rbac" 20 xrpcerr "tangled.org/core/xrpc/errors" 21 ) ··· 49 return 50 } 51 52 - rkey := data.Rkey 53 54 - ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String()) 55 - if err != nil || ident.Handle.IsInvalidHandle() { 56 - fail(xrpcerr.GenericError(err)) 57 return 58 } 59 60 - xrpcc := xrpc.Client{ 61 - Host: ident.PDSEndpoint(), 62 } 63 64 - resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 65 - if err != nil { 66 fail(xrpcerr.GenericError(err)) 67 return 68 } 69 70 - repo := resp.Value.Val.(*tangled.Repo) 71 72 - defaultBranch := h.Config.Repo.MainBranch 73 - if data.DefaultBranch != nil && *data.DefaultBranch != "" { 74 - defaultBranch = *data.DefaultBranch 75 } 76 77 - if err := validateRepoName(repo.Name); err != nil { 78 - l.Error("creating repo", "error", err.Error()) 79 - fail(xrpcerr.GenericError(err)) 80 return 81 } 82 83 - relativeRepoPath := filepath.Join(actorDid.String(), repo.Name) 84 - repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath) 85 86 if data.Source != nil && *data.Source != "" { 87 err = git.Fork(repoPath, *data.Source, h.Config) 88 if err != nil { 89 l.Error("forking repo", "error", err.Error()) 90 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 91 return 92 } ··· 94 err = git.InitBare(repoPath, defaultBranch) 95 if err != nil { 96 l.Error("initializing bare repo", "error", err.Error()) 97 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 98 fail(xrpcerr.RepoExistsError("repository already exists")) 99 return 100 - } else { 101 - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 102 return 103 } 104 } 105 } 106 107 // add perms for this user to access the repo 108 - err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath) 109 if err != nil { 110 l.Error("adding repo permissions", "error", err.Error()) 111 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 112 return 113 } ··· 120 repoPath, 121 ) 122 123 - w.WriteHeader(http.StatusOK) 124 } 125 126 func validateRepoName(name string) error {
··· 1 package xrpc 2 3 import ( 4 + "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 + "os" 10 "strings" 11 + "time" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 14 securejoin "github.com/cyphar/filepath-securejoin" 15 gogit "github.com/go-git/go-git/v5" 16 "tangled.org/core/api/tangled" 17 "tangled.org/core/hook" 18 "tangled.org/core/knotserver/git" 19 + "tangled.org/core/knotserver/repodid" 20 "tangled.org/core/rbac" 21 xrpcerr "tangled.org/core/xrpc/errors" 22 ) ··· 50 return 51 } 52 53 + repoName := data.Name 54 55 + if repoName == "" { 56 + fail(xrpcerr.GenericError(fmt.Errorf("repository name is required"))) 57 return 58 } 59 60 + defaultBranch := h.Config.Repo.MainBranch 61 + if data.DefaultBranch != nil && *data.DefaultBranch != "" { 62 + defaultBranch = *data.DefaultBranch 63 } 64 65 + if err := validateRepoName(repoName); err != nil { 66 + l.Error("creating repo", "error", err.Error()) 67 fail(xrpcerr.GenericError(err)) 68 return 69 } 70 71 + var repoDid string 72 + var prepared *repodid.PreparedDID 73 74 + knotServiceUrl := "https://" + h.Config.Server.Hostname 75 + if h.Config.Server.Dev { 76 + knotServiceUrl = "http://" + h.Config.Server.Hostname 77 } 78 79 + switch { 80 + case data.RepoDid != nil && strings.HasPrefix(*data.RepoDid, "did:web:"): 81 + if err := repodid.VerifyRepoDIDWeb(r.Context(), h.Resolver, *data.RepoDid, knotServiceUrl); err != nil { 82 + l.Error("verifying did:web", "error", err.Error()) 83 + writeError(w, xrpcerr.GenericError(err), http.StatusBadRequest) 84 + return 85 + } 86 + 87 + exists, err := h.Db.RepoDidExists(*data.RepoDid) 88 + if err != nil { 89 + l.Error("checking did:web uniqueness", "error", err.Error()) 90 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 91 + return 92 + } 93 + if exists { 94 + writeError(w, xrpcerr.GenericError(fmt.Errorf("did:web %s is already in use on this knot", *data.RepoDid)), http.StatusConflict) 95 + return 96 + } 97 + 98 + repoDid = *data.RepoDid 99 + 100 + case data.RepoDid != nil && *data.RepoDid != "": 101 + writeError(w, xrpcerr.GenericError(fmt.Errorf("only did:web is accepted as a user-provided repo DID; did:plc is auto-generated")), http.StatusBadRequest) 102 return 103 + 104 + default: 105 + existingDid, dbErr := h.Db.GetRepoDid(actorDid.String(), repoName) 106 + if dbErr == nil && existingDid != "" { 107 + didRepoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, existingDid) 108 + if _, statErr := os.Stat(didRepoPath); statErr == nil { 109 + l.Info("repo already exists from previous attempt", "repoDid", existingDid) 110 + output := tangled.RepoCreate_Output{RepoDid: &existingDid} 111 + writeJson(w, &output) 112 + return 113 + } 114 + l.Warn("stale repo key found without directory, cleaning up", "repoDid", existingDid) 115 + if delErr := h.Db.DeleteRepoKey(existingDid); delErr != nil { 116 + l.Error("failed to clean up stale repo key", "repoDid", existingDid, "error", delErr.Error()) 117 + writeError(w, xrpcerr.GenericError(fmt.Errorf("failed to clean up stale state, retry later")), http.StatusInternalServerError) 118 + return 119 + } 120 + } 121 + 122 + var prepErr error 123 + prepared, prepErr = repodid.PrepareRepoDID(h.Config.Server.PlcUrl, knotServiceUrl) 124 + if prepErr != nil { 125 + l.Error("preparing repo DID", "error", prepErr.Error()) 126 + writeError(w, xrpcerr.GenericError(prepErr), http.StatusInternalServerError) 127 + return 128 + } 129 + repoDid = prepared.RepoDid 130 + 131 + if err := h.Db.StoreRepoKey(repoDid, prepared.SigningKeyRaw, actorDid.String(), repoName); err != nil { 132 + if strings.Contains(err.Error(), "UNIQUE constraint failed") { 133 + writeError(w, xrpcerr.GenericError(fmt.Errorf("repository %s already being created", repoName)), http.StatusConflict) 134 + return 135 + } 136 + l.Error("claiming repo key slot", "error", err.Error()) 137 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 138 + return 139 + } 140 } 141 142 + l = l.With("repoDid", repoDid) 143 + 144 + repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, repoDid) 145 + rbacPath := repoDid 146 + 147 + cleanup := func() { 148 + if rmErr := os.RemoveAll(repoPath); rmErr != nil { 149 + l.Error("failed to clean up repo directory", "path", repoPath, "error", rmErr.Error()) 150 + } 151 + } 152 + 153 + cleanupAll := func() { 154 + cleanup() 155 + if delErr := h.Db.DeleteRepoKey(repoDid); delErr != nil { 156 + l.Error("failed to clean up repo key", "error", delErr.Error()) 157 + } 158 + } 159 160 if data.Source != nil && *data.Source != "" { 161 err = git.Fork(repoPath, *data.Source, h.Config) 162 if err != nil { 163 l.Error("forking repo", "error", err.Error()) 164 + cleanupAll() 165 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 166 return 167 } ··· 169 err = git.InitBare(repoPath, defaultBranch) 170 if err != nil { 171 l.Error("initializing bare repo", "error", err.Error()) 172 + cleanupAll() 173 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 174 fail(xrpcerr.RepoExistsError("repository already exists")) 175 return 176 + } 177 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 178 + return 179 + } 180 + } 181 + 182 + if data.RepoDid != nil && strings.HasPrefix(*data.RepoDid, "did:web:") { 183 + if err := h.Db.StoreRepoDidWeb(repoDid, actorDid.String(), repoName); err != nil { 184 + cleanupAll() 185 + if strings.Contains(err.Error(), "UNIQUE constraint failed") { 186 + writeError(w, xrpcerr.GenericError(fmt.Errorf("did:web %s is already in use", repoDid)), http.StatusConflict) 187 return 188 } 189 + l.Error("storing did:web repo entry", "error", err.Error()) 190 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 191 + return 192 + } 193 + } 194 + 195 + if prepared != nil { 196 + plcCtx, plcCancel := context.WithTimeout(context.Background(), 30*time.Second) 197 + defer plcCancel() 198 + if err := prepared.Submit(plcCtx); err != nil { 199 + l.Error("submitting to PLC directory", "error", err.Error()) 200 + cleanupAll() 201 + writeError(w, xrpcerr.GenericError(fmt.Errorf("PLC directory submission failed: %w", err)), http.StatusInternalServerError) 202 + return 203 } 204 } 205 206 // add perms for this user to access the repo 207 + err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, rbacPath) 208 if err != nil { 209 l.Error("adding repo permissions", "error", err.Error()) 210 + cleanupAll() 211 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 212 return 213 } ··· 220 repoPath, 221 ) 222 223 + writeJson(w, &tangled.RepoCreate_Output{RepoDid: &repoDid}) 224 } 225 226 func validateRepoName(name string) error {
+10 -7
knotserver/xrpc/delete_branch.go
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/knotserver/git" 14 "tangled.org/core/rbac" ··· 57 } 58 59 repo := resp.Value.Val.(*tangled.Repo) 60 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 61 if err != nil { 62 - fail(xrpcerr.GenericError(err)) 63 return 64 } 65 66 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 67 - l.Error("insufficent permissions", "did", actorDid.String(), "repo", didPath) 68 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 69 return 70 } 71 72 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 73 - gr, err := git.PlainOpen(path) 74 if err != nil { 75 fail(xrpcerr.GenericError(err)) 76 return
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 56 } 57 58 repo := resp.Value.Val.(*tangled.Repo) 59 + repoDid, err := x.Db.GetRepoDid(ident.DID.String(), repo.Name) 60 if err != nil { 61 + fail(xrpcerr.RepoNotFoundError) 62 + return 63 + } 64 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 65 + if err != nil { 66 + fail(xrpcerr.RepoNotFoundError) 67 return 68 } 69 70 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 71 + l.Error("insufficent permissions", "did", actorDid.String(), "repo", repoDid) 72 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 73 return 74 } 75 76 + gr, err := git.PlainOpen(repoPath) 77 if err != nil { 78 fail(xrpcerr.GenericError(err)) 79 return
+15 -9
knotserver/xrpc/delete_repo.go
··· 5 "fmt" 6 "net/http" 7 "os" 8 - "path/filepath" 9 10 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 "github.com/bluesky-social/indigo/atproto/syntax" 12 "github.com/bluesky-social/indigo/xrpc" 13 - securejoin "github.com/cyphar/filepath-securejoin" 14 "tangled.org/core/api/tangled" 15 "tangled.org/core/rbac" 16 xrpcerr "tangled.org/core/xrpc/errors" ··· 61 return 62 } 63 64 - relativeRepoPath := filepath.Join(did, name) 65 - isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 66 if err != nil { 67 - fail(xrpcerr.GenericError(err)) 68 return 69 } 70 - if !isDeleteAllowed { 71 - fail(xrpcerr.AccessControlError(actorDid.String())) 72 return 73 } 74 75 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 76 if err != nil { 77 fail(xrpcerr.GenericError(err)) 78 return 79 } 80 ··· 85 return 86 } 87 88 - err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 89 if err != nil { 90 l.Error("failed to delete repo from enforcer", "error", err.Error()) 91 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 92 return 93 } 94 95 w.WriteHeader(http.StatusOK)
··· 5 "fmt" 6 "net/http" 7 "os" 8 9 comatproto "github.com/bluesky-social/indigo/api/atproto" 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 "github.com/bluesky-social/indigo/xrpc" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/rbac" 14 xrpcerr "tangled.org/core/xrpc/errors" ··· 59 return 60 } 61 62 + repoDid, err := x.Db.GetRepoDid(did, name) 63 if err != nil { 64 + fail(xrpcerr.RepoNotFoundError) 65 return 66 } 67 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 68 + if err != nil { 69 + fail(xrpcerr.RepoNotFoundError) 70 return 71 } 72 73 + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, repoDid) 74 if err != nil { 75 fail(xrpcerr.GenericError(err)) 76 + return 77 + } 78 + if !isDeleteAllowed { 79 + fail(xrpcerr.AccessControlError(actorDid.String())) 80 return 81 } 82 ··· 87 return 88 } 89 90 + err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, repoDid) 91 if err != nil { 92 l.Error("failed to delete repo from enforcer", "error", err.Error()) 93 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 94 return 95 + } 96 + 97 + if err := x.Db.DeleteRepoKey(repoDid); err != nil { 98 + l.Error("failed to delete repo key", "error", err.Error()) 99 } 100 101 w.WriteHeader(http.StatusOK)
+11 -9
knotserver/xrpc/fork_status.go
··· 7 "path/filepath" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 51 name = filepath.Base(source) 52 } 53 54 - relativeRepoPath := filepath.Join(did, name) 55 - 56 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 57 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 58 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 59 return 60 } 61 62 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 63 - if err != nil { 64 - fail(xrpcerr.GenericError(err)) 65 return 66 } 67
··· 7 "path/filepath" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/knotserver/git" 12 "tangled.org/core/rbac" ··· 50 name = filepath.Base(source) 51 } 52 53 + repoDid, err := x.Db.GetRepoDid(did, name) 54 + if err != nil { 55 + fail(xrpcerr.RepoNotFoundError) 56 + return 57 + } 58 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 59 + if err != nil { 60 + fail(xrpcerr.RepoNotFoundError) 61 return 62 } 63 64 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 65 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 66 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 67 return 68 } 69
+11 -10
knotserver/xrpc/fork_sync.go
··· 4 "encoding/json" 5 "fmt" 6 "net/http" 7 - "path/filepath" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 42 return 43 } 44 45 - relativeRepoPath := filepath.Join(did, name) 46 - 47 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 48 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 49 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 50 return 51 } 52 53 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 54 - if err != nil { 55 - fail(xrpcerr.GenericError(err)) 56 return 57 } 58
··· 4 "encoding/json" 5 "fmt" 6 "net/http" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/knotserver/git" 11 "tangled.org/core/rbac" ··· 40 return 41 } 42 43 + repoDid, err := x.Db.GetRepoDid(did, name) 44 + if err != nil { 45 + fail(xrpcerr.RepoNotFoundError) 46 + return 47 + } 48 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 49 + if err != nil { 50 + fail(xrpcerr.RepoNotFoundError) 51 return 52 } 53 54 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 55 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 56 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 57 return 58 } 59
+8 -10
knotserver/xrpc/hidden_ref.go
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/knotserver/git" 14 "tangled.org/core/rbac" ··· 63 } 64 65 repo := resp.Value.Val.(*tangled.Repo) 66 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 67 if err != nil { 68 - fail(xrpcerr.GenericError(err)) 69 return 70 } 71 - 72 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 73 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", didPath) 74 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 75 return 76 } 77 78 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 79 - if err != nil { 80 - fail(xrpcerr.GenericError(err)) 81 return 82 } 83
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 62 } 63 64 repo := resp.Value.Val.(*tangled.Repo) 65 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 66 if err != nil { 67 + fail(xrpcerr.RepoNotFoundError) 68 return 69 } 70 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 71 + if err != nil { 72 + fail(xrpcerr.RepoNotFoundError) 73 return 74 } 75 76 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 77 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 78 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 79 return 80 } 81
+8 -10
knotserver/xrpc/merge.go
··· 7 "net/http" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/patchutil" ··· 43 return 44 } 45 46 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 47 if err != nil { 48 - fail(xrpcerr.GenericError(err)) 49 return 50 } 51 - 52 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 53 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 54 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 55 return 56 } 57 58 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 59 - if err != nil { 60 - fail(xrpcerr.GenericError(err)) 61 return 62 } 63
··· 7 "net/http" 8 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/knotserver/git" 12 "tangled.org/core/patchutil" ··· 42 return 43 } 44 45 + repoDid, err := x.Db.GetRepoDid(did, name) 46 if err != nil { 47 + fail(xrpcerr.RepoNotFoundError) 48 return 49 } 50 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 51 + if err != nil { 52 + fail(xrpcerr.RepoNotFoundError) 53 return 54 } 55 56 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 57 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) 58 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 59 return 60 } 61
+4 -6
knotserver/xrpc/merge_check.go
··· 6 "fmt" 7 "net/http" 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/knotserver/git" 12 "tangled.org/core/patchutil" ··· 34 return 35 } 36 37 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 38 if err != nil { 39 - fail(xrpcerr.GenericError(err)) 40 return 41 } 42 - 43 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 44 if err != nil { 45 - fail(xrpcerr.GenericError(err)) 46 return 47 } 48
··· 6 "fmt" 7 "net/http" 8 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/knotserver/git" 11 "tangled.org/core/patchutil" ··· 33 return 34 } 35 36 + repoDid, err := x.Db.GetRepoDid(did, name) 37 if err != nil { 38 + fail(xrpcerr.RepoNotFoundError) 39 return 40 } 41 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 42 if err != nil { 43 + fail(xrpcerr.RepoNotFoundError) 44 return 45 } 46
+9 -6
knotserver/xrpc/set_default_branch.go
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 "tangled.org/core/api/tangled" 13 "tangled.org/core/knotserver/git" 14 "tangled.org/core/rbac" ··· 59 } 60 61 repo := resp.Value.Val.(*tangled.Repo) 62 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 63 if err != nil { 64 - fail(xrpcerr.GenericError(err)) 65 return 66 } 67 68 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 69 l.Error("insufficent permissions", "did", actorDid.String()) 70 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 71 return 72 } 73 74 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 75 - gr, err := git.PlainOpen(path) 76 if err != nil { 77 fail(xrpcerr.GenericError(err)) 78 return
··· 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 "github.com/bluesky-social/indigo/xrpc" 11 "tangled.org/core/api/tangled" 12 "tangled.org/core/knotserver/git" 13 "tangled.org/core/rbac" ··· 58 } 59 60 repo := resp.Value.Val.(*tangled.Repo) 61 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 62 if err != nil { 63 + fail(xrpcerr.RepoNotFoundError) 64 + return 65 + } 66 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 67 + if err != nil { 68 + fail(xrpcerr.RepoNotFoundError) 69 return 70 } 71 72 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 73 l.Error("insufficent permissions", "did", actorDid.String()) 74 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 75 return 76 } 77 78 + gr, err := git.PlainOpen(repoPath) 79 if err != nil { 80 fail(xrpcerr.GenericError(err)) 81 return
+3 -25
knotserver/xrpc/xrpc.go
··· 6 "net/http" 7 "strings" 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/idresolver" 12 "tangled.org/core/jetstream" ··· 78 return r 79 } 80 81 - // parseRepoParam parses a repo parameter in 'did/repoName' format and returns 82 - // the full repository path on disk 83 func (x *Xrpc) parseRepoParam(repo string) (string, error) { 84 - if repo == "" { 85 return "", xrpcerr.NewXrpcError( 86 xrpcerr.WithTag("InvalidRequest"), 87 - xrpcerr.WithMessage("missing repo parameter"), 88 ) 89 } 90 91 - // Parse repo string (did/repoName format) 92 - parts := strings.SplitN(repo, "/", 2) 93 - if len(parts) != 2 { 94 - return "", xrpcerr.NewXrpcError( 95 - xrpcerr.WithTag("InvalidRequest"), 96 - xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"), 97 - ) 98 - } 99 - 100 - did := parts[0] 101 - repoName := parts[1] 102 - 103 - // Construct repository path using the same logic as didPath 104 - didRepoPath, err := securejoin.SecureJoin(did, repoName) 105 if err != nil { 106 return "", xrpcerr.RepoNotFoundError 107 } 108 - 109 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) 110 - if err != nil { 111 - return "", xrpcerr.RepoNotFoundError 112 - } 113 - 114 return repoPath, nil 115 } 116
··· 6 "net/http" 7 "strings" 8 9 "tangled.org/core/api/tangled" 10 "tangled.org/core/idresolver" 11 "tangled.org/core/jetstream" ··· 77 return r 78 } 79 80 func (x *Xrpc) parseRepoParam(repo string) (string, error) { 81 + if repo == "" || !strings.HasPrefix(repo, "did:") { 82 return "", xrpcerr.NewXrpcError( 83 xrpcerr.WithTag("InvalidRequest"), 84 + xrpcerr.WithMessage("missing or invalid repo parameter, expected a repo DID"), 85 ) 86 } 87 88 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo) 89 if err != nil { 90 return "", xrpcerr.RepoNotFoundError 91 } 92 return repoPath, nil 93 } 94
+9
lexicons/actor/profile.json
··· 60 "maxGraphemes": 40, 61 "maxLength": 400 62 }, 63 "pinnedRepositories": { 64 "type": "array", 65 "description": "Any ATURI, it is up to appviews to validate these fields.",
··· 60 "maxGraphemes": 40, 61 "maxLength": 400 62 }, 63 + "pinnedRepositoryDids": { 64 + "type": "array", 65 + "minLength": 0, 66 + "maxLength": 6, 67 + "items": { 68 + "type": "string", 69 + "format": "did" 70 + } 71 + }, 72 "pinnedRepositories": { 73 "type": "array", 74 "description": "Any ATURI, it is up to appviews to validate these fields.",
+4
lexicons/feed/reaction.json
··· 19 "type": "string", 20 "format": "at-uri" 21 }, 22 "reaction": { 23 "type": "string", 24 "enum": [ "👍", "👎", "😆", "🎉", "🫤", "❤️", "🚀", "👀" ]
··· 19 "type": "string", 20 "format": "at-uri" 21 }, 22 + "subjectDid": { 23 + "type": "string", 24 + "format": "did" 25 + }, 26 "reaction": { 27 "type": "string", 28 "enum": [ "👍", "👎", "😆", "🎉", "🫤", "❤️", "🚀", "👀" ]
+4 -1
lexicons/feed/star.json
··· 10 "record": { 11 "type": "object", 12 "required": [ 13 - "subject", 14 "createdAt" 15 ], 16 "properties": { 17 "subject": { 18 "type": "string", 19 "format": "at-uri" 20 }, 21 "createdAt": { 22 "type": "string",
··· 10 "record": { 11 "type": "object", 12 "required": [ 13 "createdAt" 14 ], 15 "properties": { 16 "subject": { 17 "type": "string", 18 "format": "at-uri" 19 + }, 20 + "subjectDid": { 21 + "type": "string", 22 + "format": "did" 23 }, 24 "createdAt": { 25 "type": "string",
+7 -1
lexicons/git/refUpdate.json
··· 11 "required": [ 12 "ref", 13 "committerDid", 14 "repoDid", 15 "repoName", 16 "oldSha", ··· 29 "description": "did of the user that pushed this ref", 30 "format": "did" 31 }, 32 "repoDid": { 33 "type": "string", 34 - "description": "did of the owner of the repo", 35 "format": "did" 36 }, 37 "repoName": {
··· 11 "required": [ 12 "ref", 13 "committerDid", 14 + "ownerDid", 15 "repoDid", 16 "repoName", 17 "oldSha", ··· 30 "description": "did of the user that pushed this ref", 31 "format": "did" 32 }, 33 + "ownerDid": { 34 + "type": "string", 35 + "description": "did of the owner of the repo", 36 + "format": "did" 37 + }, 38 "repoDid": { 39 "type": "string", 40 + "description": "DID of the repo itself", 41 "format": "did" 42 }, 43 "repoName": {
+5 -1
lexicons/issue/issue.json
··· 9 "key": "tid", 10 "record": { 11 "type": "object", 12 - "required": ["repo", "title", "createdAt"], 13 "properties": { 14 "repo": { 15 "type": "string", 16 "format": "at-uri" 17 }, 18 "title": { 19 "type": "string"
··· 9 "key": "tid", 10 "record": { 11 "type": "object", 12 + "required": ["title", "createdAt"], 13 "properties": { 14 "repo": { 15 "type": "string", 16 "format": "at-uri" 17 + }, 18 + "repoDid": { 19 + "type": "string", 20 + "format": "did" 21 }, 22 "title": { 23 "type": "string"
+4
lexicons/label/op.json
··· 21 "format": "at-uri", 22 "description": "The subject (task, pull or discussion) of this label. Appviews may apply a `scope` check and refuse this op." 23 }, 24 "performedAt": { 25 "type": "string", 26 "format": "datetime"
··· 21 "format": "at-uri", 22 "description": "The subject (task, pull or discussion) of this label. Appviews may apply a `scope` check and refuse this op." 23 }, 24 + "subjectDid": { 25 + "type": "string", 26 + "format": "did" 27 + }, 28 "performedAt": { 29 "type": "string", 30 "format": "datetime"
+6
lexicons/pipeline/pipeline.json
··· 66 "required": [ 67 "knot", 68 "did", 69 "repo", 70 "defaultBranch" 71 ], ··· 75 }, 76 "did": { 77 "type": "string", 78 "format": "did" 79 }, 80 "repo": {
··· 66 "required": [ 67 "knot", 68 "did", 69 + "repoDid", 70 "repo", 71 "defaultBranch" 72 ], ··· 76 }, 77 "did": { 78 "type": "string", 79 + "format": "did" 80 + }, 81 + "repoDid": { 82 + "type": "string", 83 + "description": "DID of the repo itself", 84 "format": "did" 85 }, 86 "repo": {
+8 -1
lexicons/pulls/pull.json
··· 65 "target": { 66 "type": "object", 67 "required": [ 68 - "repo", 69 "branch" 70 ], 71 "properties": { 72 "repo": { 73 "type": "string", 74 "format": "at-uri" 75 }, 76 "branch": { 77 "type": "string" ··· 96 "repo": { 97 "type": "string", 98 "format": "at-uri" 99 } 100 } 101 }
··· 65 "target": { 66 "type": "object", 67 "required": [ 68 "branch" 69 ], 70 "properties": { 71 "repo": { 72 "type": "string", 73 "format": "at-uri" 74 + }, 75 + "repoDid": { 76 + "type": "string", 77 + "format": "did" 78 }, 79 "branch": { 80 "type": "string" ··· 99 "repo": { 100 "type": "string", 101 "format": "at-uri" 102 + }, 103 + "repoDid": { 104 + "type": "string", 105 + "format": "did" 106 } 107 } 108 }
+4 -1
lexicons/repo/artifact.json
··· 11 "type": "object", 12 "required": [ 13 "name", 14 - "repo", 15 "tag", 16 "createdAt", 17 "artifact" ··· 25 "type": "string", 26 "format": "at-uri", 27 "description": "repo that this artifact is being uploaded to" 28 }, 29 "tag": { 30 "type": "bytes",
··· 11 "type": "object", 12 "required": [ 13 "name", 14 "tag", 15 "createdAt", 16 "artifact" ··· 24 "type": "string", 25 "format": "at-uri", 26 "description": "repo that this artifact is being uploaded to" 27 + }, 28 + "repoDid": { 29 + "type": "string", 30 + "format": "did" 31 }, 32 "tag": { 33 "type": "bytes",
+4 -1
lexicons/repo/collaborator.json
··· 11 "type": "object", 12 "required": [ 13 "subject", 14 - "repo", 15 "createdAt" 16 ], 17 "properties": { ··· 23 "type": "string", 24 "description": "repo to add this user to", 25 "format": "at-uri" 26 }, 27 "createdAt": { 28 "type": "string",
··· 11 "type": "object", 12 "required": [ 13 "subject", 14 "createdAt" 15 ], 16 "properties": { ··· 22 "type": "string", 23 "description": "repo to add this user to", 24 "format": "at-uri" 25 + }, 26 + "repoDid": { 27 + "type": "string", 28 + "format": "did" 29 }, 30 "createdAt": { 31 "type": "string",
+23 -1
lexicons/repo/create.json
··· 10 "schema": { 11 "type": "object", 12 "required": [ 13 - "rkey" 14 ], 15 "properties": { 16 "rkey": { 17 "type": "string", 18 "description": "Rkey of the repository record" 19 }, 20 "defaultBranch": { 21 "type": "string", 22 "description": "Default branch to push to" ··· 24 "source": { 25 "type": "string", 26 "description": "A source URL to clone from, populate this when forking or importing a repository." 27 } 28 } 29 }
··· 10 "schema": { 11 "type": "object", 12 "required": [ 13 + "rkey", 14 + "name" 15 ], 16 "properties": { 17 "rkey": { 18 "type": "string", 19 "description": "Rkey of the repository record" 20 }, 21 + "name": { 22 + "type": "string", 23 + "description": "Name of the repository" 24 + }, 25 "defaultBranch": { 26 "type": "string", 27 "description": "Default branch to push to" ··· 29 "source": { 30 "type": "string", 31 "description": "A source URL to clone from, populate this when forking or importing a repository." 32 + }, 33 + "repoDid": { 34 + "type": "string", 35 + "format": "did", 36 + "description": "Optional user-provided did:web to use as the repo identity instead of minting a did:plc." 37 + } 38 + } 39 + } 40 + }, 41 + "output": { 42 + "encoding": "application/json", 43 + "schema": { 44 + "type": "object", 45 + "properties": { 46 + "repoDid": { 47 + "type": "string", 48 + "format": "did" 49 } 50 } 51 }
+5
lexicons/repo/repo.json
··· 60 "format": "at-uri" 61 } 62 }, 63 "createdAt": { 64 "type": "string", 65 "format": "datetime"
··· 60 "format": "at-uri" 61 } 62 }, 63 + "repoDid": { 64 + "type": "string", 65 + "format": "did", 66 + "description": "DID of the repo itself, if assigned" 67 + }, 68 "createdAt": { 69 "type": "string", 70 "format": "datetime"
+3
nix/gomod2nix.toml
··· 171 [mod."github.com/dgryski/go-rendezvous"] 172 version = "v0.0.0-20200823014737-9f7001d12a5f" 173 hash = "sha256-n/7xo5CQqo4yLaWMSzSN1Muk/oqK6O5dgDOFWapeDUI=" 174 [mod."github.com/distribution/reference"] 175 version = "v0.6.0" 176 hash = "sha256-gr4tL+qz4jKyAtl8LINcxMSanztdt+pybj1T+2ulQv4="
··· 171 [mod."github.com/dgryski/go-rendezvous"] 172 version = "v0.0.0-20200823014737-9f7001d12a5f" 173 hash = "sha256-n/7xo5CQqo4yLaWMSzSN1Muk/oqK6O5dgDOFWapeDUI=" 174 + [mod."github.com/did-method-plc/go-didplc"] 175 + version = "v0.0.0-20250716171643-635da8b4e038" 176 + hash = "sha256-o0uB/5tryjdB44ssALFr49PtfY3nRJnEENmE187md1w=" 177 [mod."github.com/distribution/reference"] 178 version = "v0.6.0" 179 hash = "sha256-gr4tL+qz4jKyAtl8LINcxMSanztdt+pybj1T+2ulQv4="
+1 -1
spindle/engine/engine.go
··· 25 // extract secrets 26 var allSecrets []secrets.UnlockedSecret 27 if didSlashRepo, err := securejoin.SecureJoin(pipeline.RepoOwner, pipeline.RepoName); err == nil { 28 - if res, err := vault.GetSecretsUnlocked(ctx, secrets.DidSlashRepo(didSlashRepo)); err == nil { 29 allSecrets = res 30 } 31 }
··· 25 // extract secrets 26 var allSecrets []secrets.UnlockedSecret 27 if didSlashRepo, err := securejoin.SecureJoin(pipeline.RepoOwner, pipeline.RepoName); err == nil { 28 + if res, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(didSlashRepo)); err == nil { 29 allSecrets = res 30 } 31 }
+32 -26
spindle/ingester.go
··· 228 return err 229 } 230 231 - repoAt, err := syntax.ParseATURI(record.Repo) 232 - if err != nil { 233 - l.Info("rejecting record, invalid repoAt", "repoAt", record.Repo) 234 - return nil 235 - } 236 237 - // TODO: get rid of this entirely 238 - // resolve this aturi to extract the repo record 239 - owner, err := s.res.ResolveIdent(ctx, repoAt.Authority().String()) 240 - if err != nil || owner.Handle.IsInvalidHandle() { 241 - return fmt.Errorf("failed to resolve handle: %w", err) 242 - } 243 244 - xrpcc := xrpc.Client{ 245 - Host: owner.PDSEndpoint(), 246 - } 247 248 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 249 - if err != nil { 250 - return err 251 } 252 253 - repo := resp.Value.Val.(*tangled.Repo) 254 - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 255 - 256 - // check perms for this user 257 - if ok, err := s.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil { 258 return fmt.Errorf("insufficient permissions: %w", err) 259 } 260 261 - // add collaborator to rbac 262 - if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil { 263 - l.Error("failed to add repo to enforcer", "error", err) 264 - return fmt.Errorf("failed to add repo: %w", err) 265 } 266 267 return nil
··· 228 return err 229 } 230 231 + var rbacResource string 232 + var ownerDid string 233 + switch { 234 + case record.Repo != nil: 235 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 236 + if parseErr != nil { 237 + l.Info("rejecting record, invalid repoAt", "repoAt", *record.Repo) 238 + return nil 239 + } 240 241 + owner, resolveErr := s.res.ResolveIdent(ctx, repoAt.Authority().String()) 242 + if resolveErr != nil || owner.Handle.IsInvalidHandle() { 243 + return fmt.Errorf("failed to resolve handle: %w", resolveErr) 244 + } 245 246 + xrpcc := xrpc.Client{ 247 + Host: owner.PDSEndpoint(), 248 + } 249 250 + resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 251 + if getErr != nil { 252 + return getErr 253 + } 254 + 255 + repo := resp.Value.Val.(*tangled.Repo) 256 + rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repo.Name) 257 + ownerDid = owner.DID.String() 258 + 259 + default: 260 + l.Info("rejecting collaborator record without repo at-uri (spindle RBAC keyed by owner/name)") 261 + return nil 262 } 263 264 + if ok, err := s.e.IsCollaboratorInviteAllowed(ownerDid, rbac.ThisServer, rbacResource); !ok || err != nil { 265 return fmt.Errorf("insufficient permissions: %w", err) 266 } 267 268 + if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, rbacResource); err != nil { 269 + l.Error("failed to add collaborator to enforcer", "error", err) 270 + return fmt.Errorf("failed to add collaborator: %w", err) 271 } 272 273 return nil
+4 -4
spindle/secrets/manager.go
··· 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 ) 11 12 - type DidSlashRepo string 13 14 type Secret[T any] struct { 15 Key string 16 Value T 17 - Repo DidSlashRepo 18 CreatedAt time.Time 19 CreatedBy syntax.DID 20 } ··· 29 type Manager interface { 30 AddSecret(ctx context.Context, secret UnlockedSecret) error 31 RemoveSecret(ctx context.Context, secret Secret[any]) error 32 - GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) 33 - GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) 34 } 35 36 // stopper interface for managers that need cleanup
··· 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 ) 11 12 + type RepoIdentifier string 13 14 type Secret[T any] struct { 15 Key string 16 Value T 17 + Repo RepoIdentifier 18 CreatedAt time.Time 19 CreatedBy syntax.DID 20 } ··· 29 type Manager interface { 30 AddSecret(ctx context.Context, secret UnlockedSecret) error 31 RemoveSecret(ctx context.Context, secret Secret[any]) error 32 + GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) 33 + GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) 34 } 35 36 // stopper interface for managers that need cleanup
+5 -5
spindle/secrets/openbao.go
··· 149 return nil 150 } 151 152 - func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) { 153 repoPath := v.buildRepoPath(repo) 154 155 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) ··· 224 return secrets, nil 225 } 226 227 - func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) { 228 repoPath := v.buildRepoPath(repo) 229 230 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) ··· 307 } 308 309 // buildRepoPath creates a safe path for a repository 310 - func (v *OpenBaoManager) buildRepoPath(repo DidSlashRepo) string { 311 - // convert DidSlashRepo to a safe path by replacing special characters 312 repoPath := strings.ReplaceAll(string(repo), "/", "_") 313 repoPath = strings.ReplaceAll(repoPath, ":", "_") 314 repoPath = strings.ReplaceAll(repoPath, ".", "_") ··· 316 } 317 318 // buildSecretPath creates a path for a specific secret 319 - func (v *OpenBaoManager) buildSecretPath(repo DidSlashRepo, key string) string { 320 return path.Join(v.buildRepoPath(repo), key) 321 }
··· 149 return nil 150 } 151 152 + func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) { 153 repoPath := v.buildRepoPath(repo) 154 155 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) ··· 224 return secrets, nil 225 } 226 227 + func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) { 228 repoPath := v.buildRepoPath(repo) 229 230 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) ··· 307 } 308 309 // buildRepoPath creates a safe path for a repository 310 + func (v *OpenBaoManager) buildRepoPath(repo RepoIdentifier) string { 311 + // convert RepoIdentifier to a safe path by replacing special characters 312 repoPath := strings.ReplaceAll(string(repo), "/", "_") 313 repoPath = strings.ReplaceAll(repoPath, ":", "_") 314 repoPath = strings.ReplaceAll(repoPath, ".", "_") ··· 316 } 317 318 // buildSecretPath creates a path for a specific secret 319 + func (v *OpenBaoManager) buildSecretPath(repo RepoIdentifier, key string) string { 320 return path.Join(v.buildRepoPath(repo), key) 321 }
+17 -17
spindle/secrets/openbao_test.go
··· 32 m.errorToReturn = nil 33 } 34 35 - func (m *MockOpenBaoManager) buildKey(repo DidSlashRepo, key string) string { 36 return string(repo) + "_" + key 37 } 38 ··· 64 return nil 65 } 66 67 - func (m *MockOpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) { 68 if m.shouldError { 69 return nil, m.errorToReturn 70 } ··· 84 return result, nil 85 } 86 87 - func (m *MockOpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) { 88 if m.shouldError { 89 return nil, m.errorToReturn 90 } ··· 103 return UnlockedSecret{ 104 Key: key, 105 Value: value, 106 - Repo: DidSlashRepo(repo), 107 CreatedAt: time.Now(), 108 CreatedBy: syntax.DID(createdBy), 109 } ··· 173 174 tests := []struct { 175 name string 176 - repo DidSlashRepo 177 key string 178 expected string 179 }{ 180 { 181 name: "simple repo path", 182 - repo: DidSlashRepo("did:plc:foo/repo"), 183 key: "api_key", 184 expected: "repos/did_plc_foo_repo/api_key", 185 }, 186 { 187 name: "complex repo path with dots", 188 - repo: DidSlashRepo("did:web:example.com/my-repo"), 189 key: "secret_key", 190 expected: "repos/did_web_example_com_my-repo/secret_key", 191 }, ··· 204 205 tests := []struct { 206 name string 207 - repo DidSlashRepo 208 expected string 209 }{ 210 { ··· 310 }, 311 removeSecret: Secret[any]{ 312 Key: "API_KEY", 313 - Repo: DidSlashRepo("did:plc:test/repo1"), 314 }, 315 expectError: false, 316 }, ··· 319 setupSecrets: []UnlockedSecret{}, 320 removeSecret: Secret[any]{ 321 Key: "API_KEY", 322 - Repo: DidSlashRepo("did:plc:test/repo1"), 323 }, 324 expectError: true, 325 }, ··· 352 tests := []struct { 353 name string 354 setupSecrets []UnlockedSecret 355 - queryRepo DidSlashRepo 356 expectedCount int 357 expectedKeys []string 358 expectError bool ··· 364 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 365 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 366 }, 367 - queryRepo: DidSlashRepo("did:plc:test/repo1"), 368 expectedCount: 2, 369 expectedKeys: []string{"API_KEY", "DB_PASSWORD"}, 370 expectError: false, ··· 372 { 373 name: "get secrets from empty repo", 374 setupSecrets: []UnlockedSecret{}, 375 - queryRepo: DidSlashRepo("did:plc:test/empty"), 376 expectedCount: 0, 377 expectedKeys: []string{}, 378 expectError: false, ··· 417 tests := []struct { 418 name string 419 setupSecrets []UnlockedSecret 420 - queryRepo DidSlashRepo 421 expectedCount int 422 expectedSecrets map[string]string // key -> value 423 expectError bool ··· 429 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 430 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 431 }, 432 - queryRepo: DidSlashRepo("did:plc:test/repo1"), 433 expectedCount: 2, 434 expectedSecrets: map[string]string{ 435 "API_KEY": "secret123", ··· 440 { 441 name: "get secrets from empty repo", 442 setupSecrets: []UnlockedSecret{}, 443 - queryRepo: DidSlashRepo("did:plc:test/empty"), 444 expectedCount: 0, 445 expectedSecrets: map[string]string{}, 446 expectError: false, ··· 521 name: "complete workflow", 522 scenario: func(t *testing.T, mock *MockOpenBaoManager) { 523 ctx := context.Background() 524 - repo := DidSlashRepo("did:plc:test/integration") 525 526 // Start with empty repo 527 secrets, err := mock.GetSecretsLocked(ctx, repo)
··· 32 m.errorToReturn = nil 33 } 34 35 + func (m *MockOpenBaoManager) buildKey(repo RepoIdentifier, key string) string { 36 return string(repo) + "_" + key 37 } 38 ··· 64 return nil 65 } 66 67 + func (m *MockOpenBaoManager) GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) { 68 if m.shouldError { 69 return nil, m.errorToReturn 70 } ··· 84 return result, nil 85 } 86 87 + func (m *MockOpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) { 88 if m.shouldError { 89 return nil, m.errorToReturn 90 } ··· 103 return UnlockedSecret{ 104 Key: key, 105 Value: value, 106 + Repo: RepoIdentifier(repo), 107 CreatedAt: time.Now(), 108 CreatedBy: syntax.DID(createdBy), 109 } ··· 173 174 tests := []struct { 175 name string 176 + repo RepoIdentifier 177 key string 178 expected string 179 }{ 180 { 181 name: "simple repo path", 182 + repo: RepoIdentifier("did:plc:foo/repo"), 183 key: "api_key", 184 expected: "repos/did_plc_foo_repo/api_key", 185 }, 186 { 187 name: "complex repo path with dots", 188 + repo: RepoIdentifier("did:web:example.com/my-repo"), 189 key: "secret_key", 190 expected: "repos/did_web_example_com_my-repo/secret_key", 191 }, ··· 204 205 tests := []struct { 206 name string 207 + repo RepoIdentifier 208 expected string 209 }{ 210 { ··· 310 }, 311 removeSecret: Secret[any]{ 312 Key: "API_KEY", 313 + Repo: RepoIdentifier("did:plc:test/repo1"), 314 }, 315 expectError: false, 316 }, ··· 319 setupSecrets: []UnlockedSecret{}, 320 removeSecret: Secret[any]{ 321 Key: "API_KEY", 322 + Repo: RepoIdentifier("did:plc:test/repo1"), 323 }, 324 expectError: true, 325 }, ··· 352 tests := []struct { 353 name string 354 setupSecrets []UnlockedSecret 355 + queryRepo RepoIdentifier 356 expectedCount int 357 expectedKeys []string 358 expectError bool ··· 364 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 365 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 366 }, 367 + queryRepo: RepoIdentifier("did:plc:test/repo1"), 368 expectedCount: 2, 369 expectedKeys: []string{"API_KEY", "DB_PASSWORD"}, 370 expectError: false, ··· 372 { 373 name: "get secrets from empty repo", 374 setupSecrets: []UnlockedSecret{}, 375 + queryRepo: RepoIdentifier("did:plc:test/empty"), 376 expectedCount: 0, 377 expectedKeys: []string{}, 378 expectError: false, ··· 417 tests := []struct { 418 name string 419 setupSecrets []UnlockedSecret 420 + queryRepo RepoIdentifier 421 expectedCount int 422 expectedSecrets map[string]string // key -> value 423 expectError bool ··· 429 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 430 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 431 }, 432 + queryRepo: RepoIdentifier("did:plc:test/repo1"), 433 expectedCount: 2, 434 expectedSecrets: map[string]string{ 435 "API_KEY": "secret123", ··· 440 { 441 name: "get secrets from empty repo", 442 setupSecrets: []UnlockedSecret{}, 443 + queryRepo: RepoIdentifier("did:plc:test/empty"), 444 expectedCount: 0, 445 expectedSecrets: map[string]string{}, 446 expectError: false, ··· 521 name: "complete workflow", 522 scenario: func(t *testing.T, mock *MockOpenBaoManager) { 523 ctx := context.Background() 524 + repo := RepoIdentifier("did:plc:test/integration") 525 526 // Start with empty repo 527 secrets, err := mock.GetSecretsLocked(ctx, repo)
+2 -2
spindle/secrets/sqlite.go
··· 107 return nil 108 } 109 110 - func (s *SqliteManager) GetSecretsLocked(ctx context.Context, didSlashRepo DidSlashRepo) ([]LockedSecret, error) { 111 query := fmt.Sprintf(` 112 select repo, key, created_at, created_by from %s where repo = ?; 113 `, s.tableName) ··· 139 return ls, nil 140 } 141 142 - func (s *SqliteManager) GetSecretsUnlocked(ctx context.Context, didSlashRepo DidSlashRepo) ([]UnlockedSecret, error) { 143 query := fmt.Sprintf(` 144 select repo, key, value, created_at, created_by from %s where repo = ?; 145 `, s.tableName)
··· 107 return nil 108 } 109 110 + func (s *SqliteManager) GetSecretsLocked(ctx context.Context, didSlashRepo RepoIdentifier) ([]LockedSecret, error) { 111 query := fmt.Sprintf(` 112 select repo, key, created_at, created_by from %s where repo = ?; 113 `, s.tableName) ··· 139 return ls, nil 140 } 141 142 + func (s *SqliteManager) GetSecretsUnlocked(ctx context.Context, didSlashRepo RepoIdentifier) ([]UnlockedSecret, error) { 143 query := fmt.Sprintf(` 144 select repo, key, value, created_at, created_by from %s where repo = ?; 145 `, s.tableName)
+21 -21
spindle/secrets/sqlite_test.go
··· 22 return UnlockedSecret{ 23 Key: key, 24 Value: value, 25 - Repo: DidSlashRepo(repo), 26 CreatedAt: time.Now(), 27 CreatedBy: syntax.DID(createdBy), 28 } ··· 147 }, 148 removeSecret: Secret[any]{ 149 Key: "api_key", 150 - Repo: DidSlashRepo("did:plc:foo/repo"), 151 }, 152 expectError: nil, 153 }, ··· 158 }, 159 removeSecret: Secret[any]{ 160 Key: "non_existent_key", 161 - Repo: DidSlashRepo("did:plc:foo/repo"), 162 }, 163 expectError: ErrKeyNotFound, 164 }, ··· 167 setupSecrets: []UnlockedSecret{}, 168 removeSecret: Secret[any]{ 169 Key: "any_key", 170 - Repo: DidSlashRepo("did:plc:foo/repo"), 171 }, 172 expectError: ErrKeyNotFound, 173 }, ··· 178 }, 179 removeSecret: Secret[any]{ 180 Key: "api_key", 181 - Repo: DidSlashRepo("other.com/repo"), 182 }, 183 expectError: ErrKeyNotFound, 184 }, ··· 209 tests := []struct { 210 name string 211 setupSecrets []UnlockedSecret 212 - queryRepo DidSlashRepo 213 expectedCount int 214 expectedKeys []string 215 expectError bool ··· 221 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 222 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 223 }, 224 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 225 expectedCount: 2, 226 expectedKeys: []string{"key1", "key2"}, 227 expectError: false, ··· 232 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 233 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 234 }, 235 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 236 expectedCount: 1, 237 expectedKeys: []string{"single_key"}, 238 expectError: false, ··· 242 setupSecrets: []UnlockedSecret{ 243 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 244 }, 245 - queryRepo: DidSlashRepo("nonexistent.com/repo"), 246 expectedCount: 0, 247 expectedKeys: []string{}, 248 expectError: false, ··· 250 { 251 name: "get secrets from empty database", 252 setupSecrets: []UnlockedSecret{}, 253 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 254 expectedCount: 0, 255 expectedKeys: []string{}, 256 expectError: false, ··· 311 tests := []struct { 312 name string 313 setupSecrets []UnlockedSecret 314 - queryRepo DidSlashRepo 315 expectedCount int 316 expectedSecrets map[string]string // key -> value 317 expectError bool ··· 323 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 324 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 325 }, 326 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 327 expectedCount: 2, 328 expectedSecrets: map[string]string{ 329 "key1": "value1", ··· 337 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 338 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 339 }, 340 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 341 expectedCount: 1, 342 expectedSecrets: map[string]string{ 343 "single_key": "single_value", ··· 349 setupSecrets: []UnlockedSecret{ 350 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 351 }, 352 - queryRepo: DidSlashRepo("nonexistent.com/repo"), 353 expectedCount: 0, 354 expectedSecrets: map[string]string{}, 355 expectError: false, ··· 357 { 358 name: "get unlocked secrets from empty database", 359 setupSecrets: []UnlockedSecret{}, 360 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 361 expectedCount: 0, 362 expectedSecrets: map[string]string{}, 363 expectError: false, ··· 429 return m.AddSecret(context.Background(), secret) 430 }, 431 func(m Manager) error { 432 - _, err := m.GetSecretsLocked(context.Background(), DidSlashRepo("interface.test/repo")) 433 return err 434 }, 435 func(m Manager) error { 436 - _, err := m.GetSecretsUnlocked(context.Background(), DidSlashRepo("interface.test/repo")) 437 return err 438 }, 439 func(m Manager) error { 440 secret := Secret[any]{ 441 Key: "test_key", 442 - Repo: DidSlashRepo("interface.test/repo"), 443 } 444 return m.RemoveSecret(context.Background(), secret) 445 }, ··· 498 { 499 name: "multi-repo secret management", 500 scenario: func(t *testing.T, manager *SqliteManager) { 501 - repo1 := DidSlashRepo("example1.com/repo") 502 - repo2 := DidSlashRepo("example2.com/repo") 503 504 secrets := []UnlockedSecret{ 505 createTestSecret(string(repo1), "db_password", "super_secret_123", "did:plc:admin"), ··· 543 { 544 name: "empty database operations", 545 scenario: func(t *testing.T, manager *SqliteManager) { 546 - repo := DidSlashRepo("empty.test/repo") 547 548 // Operations on empty database should not error 549 locked, err := manager.GetSecretsLocked(context.Background(), repo)
··· 22 return UnlockedSecret{ 23 Key: key, 24 Value: value, 25 + Repo: RepoIdentifier(repo), 26 CreatedAt: time.Now(), 27 CreatedBy: syntax.DID(createdBy), 28 } ··· 147 }, 148 removeSecret: Secret[any]{ 149 Key: "api_key", 150 + Repo: RepoIdentifier("did:plc:foo/repo"), 151 }, 152 expectError: nil, 153 }, ··· 158 }, 159 removeSecret: Secret[any]{ 160 Key: "non_existent_key", 161 + Repo: RepoIdentifier("did:plc:foo/repo"), 162 }, 163 expectError: ErrKeyNotFound, 164 }, ··· 167 setupSecrets: []UnlockedSecret{}, 168 removeSecret: Secret[any]{ 169 Key: "any_key", 170 + Repo: RepoIdentifier("did:plc:foo/repo"), 171 }, 172 expectError: ErrKeyNotFound, 173 }, ··· 178 }, 179 removeSecret: Secret[any]{ 180 Key: "api_key", 181 + Repo: RepoIdentifier("other.com/repo"), 182 }, 183 expectError: ErrKeyNotFound, 184 }, ··· 209 tests := []struct { 210 name string 211 setupSecrets []UnlockedSecret 212 + queryRepo RepoIdentifier 213 expectedCount int 214 expectedKeys []string 215 expectError bool ··· 221 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 222 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 223 }, 224 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 225 expectedCount: 2, 226 expectedKeys: []string{"key1", "key2"}, 227 expectError: false, ··· 232 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 233 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 234 }, 235 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 236 expectedCount: 1, 237 expectedKeys: []string{"single_key"}, 238 expectError: false, ··· 242 setupSecrets: []UnlockedSecret{ 243 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 244 }, 245 + queryRepo: RepoIdentifier("nonexistent.com/repo"), 246 expectedCount: 0, 247 expectedKeys: []string{}, 248 expectError: false, ··· 250 { 251 name: "get secrets from empty database", 252 setupSecrets: []UnlockedSecret{}, 253 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 254 expectedCount: 0, 255 expectedKeys: []string{}, 256 expectError: false, ··· 311 tests := []struct { 312 name string 313 setupSecrets []UnlockedSecret 314 + queryRepo RepoIdentifier 315 expectedCount int 316 expectedSecrets map[string]string // key -> value 317 expectError bool ··· 323 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 324 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 325 }, 326 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 327 expectedCount: 2, 328 expectedSecrets: map[string]string{ 329 "key1": "value1", ··· 337 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 338 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 339 }, 340 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 341 expectedCount: 1, 342 expectedSecrets: map[string]string{ 343 "single_key": "single_value", ··· 349 setupSecrets: []UnlockedSecret{ 350 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 351 }, 352 + queryRepo: RepoIdentifier("nonexistent.com/repo"), 353 expectedCount: 0, 354 expectedSecrets: map[string]string{}, 355 expectError: false, ··· 357 { 358 name: "get unlocked secrets from empty database", 359 setupSecrets: []UnlockedSecret{}, 360 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 361 expectedCount: 0, 362 expectedSecrets: map[string]string{}, 363 expectError: false, ··· 429 return m.AddSecret(context.Background(), secret) 430 }, 431 func(m Manager) error { 432 + _, err := m.GetSecretsLocked(context.Background(), RepoIdentifier("interface.test/repo")) 433 return err 434 }, 435 func(m Manager) error { 436 + _, err := m.GetSecretsUnlocked(context.Background(), RepoIdentifier("interface.test/repo")) 437 return err 438 }, 439 func(m Manager) error { 440 secret := Secret[any]{ 441 Key: "test_key", 442 + Repo: RepoIdentifier("interface.test/repo"), 443 } 444 return m.RemoveSecret(context.Background(), secret) 445 }, ··· 498 { 499 name: "multi-repo secret management", 500 scenario: func(t *testing.T, manager *SqliteManager) { 501 + repo1 := RepoIdentifier("example1.com/repo") 502 + repo2 := RepoIdentifier("example2.com/repo") 503 504 secrets := []UnlockedSecret{ 505 createTestSecret(string(repo1), "db_password", "super_secret_123", "did:plc:admin"), ··· 543 { 544 name: "empty database operations", 545 scenario: func(t *testing.T, manager *SqliteManager) { 546 + repo := RepoIdentifier("empty.test/repo") 547 548 // Operations on empty database should not error 549 locked, err := manager.GetSecretsLocked(context.Background(), repo)
+1 -1
spindle/xrpc/add_secret.go
··· 75 } 76 77 secret := secrets.UnlockedSecret{ 78 - Repo: secrets.DidSlashRepo(didPath), 79 Key: data.Key, 80 Value: data.Value, 81 CreatedAt: time.Now(),
··· 75 } 76 77 secret := secrets.UnlockedSecret{ 78 + Repo: secrets.RepoIdentifier(didPath), 79 Key: data.Key, 80 Value: data.Value, 81 CreatedAt: time.Now(),
+1 -1
spindle/xrpc/list_secrets.go
··· 69 return 70 } 71 72 - ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.DidSlashRepo(didPath)) 73 if err != nil { 74 l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err) 75 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
··· 69 return 70 } 71 72 + ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.RepoIdentifier(didPath)) 73 if err != nil { 74 l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err) 75 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+1 -1
spindle/xrpc/remove_secret.go
··· 69 } 70 71 secret := secrets.Secret[any]{ 72 - Repo: secrets.DidSlashRepo(didPath), 73 Key: data.Key, 74 } 75 err = x.Vault.RemoveSecret(r.Context(), secret)
··· 69 } 70 71 secret := secrets.Secret[any]{ 72 + Repo: secrets.RepoIdentifier(didPath), 73 Key: data.Key, 74 } 75 err = x.Vault.RemoveSecret(r.Context(), secret)