Monorepo for Tangled

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

+3287 -978
+2 -1
api/tangled/actorprofile.go
··· 28 28 // location: Free-form location text. 29 29 Location *string `json:"location,omitempty" cborgen:"location,omitempty"` 30 30 // pinnedRepositories: Any ATURI, it is up to appviews to validate these fields. 31 - PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"` 31 + PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"` 32 + PinnedRepositoryDids []string `json:"pinnedRepositoryDids,omitempty" cborgen:"pinnedRepositoryDids,omitempty"` 32 33 // pronouns: Preferred gender pronouns. 33 34 Pronouns *string `json:"pronouns,omitempty" cborgen:"pronouns,omitempty"` 34 35 Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"`
+891 -110
api/tangled/cbor_gen.go
··· 26 26 } 27 27 28 28 cw := cbg.NewCborWriter(w) 29 - fieldCount := 9 29 + fieldCount := 10 30 30 31 31 if t.Avatar == nil { 32 32 fieldCount-- ··· 45 45 } 46 46 47 47 if t.PinnedRepositories == nil { 48 + fieldCount-- 49 + } 50 + 51 + if t.PinnedRepositoryDids == nil { 48 52 fieldCount-- 49 53 } 50 54 ··· 317 321 318 322 } 319 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 + } 320 360 return nil 321 361 } 322 362 ··· 345 385 346 386 n := extra 347 387 348 - nameBuf := make([]byte, 18) 388 + nameBuf := make([]byte, 20) 349 389 for i := uint64(0); i < n; i++ { 350 390 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 351 391 if err != nil { ··· 592 632 } 593 633 594 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 + } 595 675 } 596 676 597 677 default: ··· 611 691 } 612 692 613 693 cw := cbg.NewCborWriter(w) 694 + fieldCount := 5 614 695 615 - if _, err := cw.Write([]byte{164}); err != nil { 696 + if t.SubjectDid == nil { 697 + fieldCount-- 698 + } 699 + 700 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 616 701 return err 617 702 } 618 703 ··· 703 788 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 704 789 return err 705 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 + } 706 823 return nil 707 824 } 708 825 ··· 731 848 732 849 n := extra 733 850 734 - nameBuf := make([]byte, 9) 851 + nameBuf := make([]byte, 10) 735 852 for i := uint64(0); i < n; i++ { 736 853 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 737 854 if err != nil { ··· 791 908 792 909 t.CreatedAt = string(sval) 793 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 + } 794 932 795 933 default: 796 934 // Field doesn't exist on this type, so ignore it ··· 809 947 } 810 948 811 949 cw := cbg.NewCborWriter(w) 950 + fieldCount := 4 812 951 813 - if _, err := cw.Write([]byte{163}); err != nil { 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 { 814 961 return err 815 962 } 816 963 ··· 834 981 } 835 982 836 983 // t.Subject (string) (string) 837 - if len("subject") > 1000000 { 838 - return xerrors.Errorf("Value in field \"subject\" was too long") 839 - } 984 + if t.Subject != nil { 840 985 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 - } 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 + } 847 996 848 - if len(t.Subject) > 1000000 { 849 - return xerrors.Errorf("Value in field t.Subject was too long") 850 - } 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 + } 851 1005 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 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 + } 857 1013 } 858 1014 859 1015 // t.CreatedAt (string) (string) ··· 878 1034 if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 879 1035 return err 880 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 + } 881 1069 return nil 882 1070 } 883 1071 ··· 906 1094 907 1095 n := extra 908 1096 909 - nameBuf := make([]byte, 9) 1097 + nameBuf := make([]byte, 10) 910 1098 for i := uint64(0); i < n; i++ { 911 1099 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 912 1100 if err != nil { ··· 937 1125 case "subject": 938 1126 939 1127 { 940 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1128 + b, err := cr.ReadByte() 941 1129 if err != nil { 942 1130 return err 943 1131 } 1132 + if b != cbg.CborNull[0] { 1133 + if err := cr.UnreadByte(); err != nil { 1134 + return err 1135 + } 944 1136 945 - t.Subject = string(sval) 1137 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1138 + if err != nil { 1139 + return err 1140 + } 1141 + 1142 + t.Subject = (*string)(&sval) 1143 + } 946 1144 } 947 1145 // t.CreatedAt (string) (string) 948 1146 case "createdAt": ··· 955 1153 956 1154 t.CreatedAt = string(sval) 957 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 + } 958 1177 959 1178 default: 960 1179 // Field doesn't exist on this type, so ignore it ··· 974 1193 975 1194 cw := cbg.NewCborWriter(w) 976 1195 977 - if _, err := cw.Write([]byte{168}); err != nil { 1196 + if _, err := cw.Write([]byte{169}); err != nil { 978 1197 return err 979 1198 } 980 1199 ··· 1105 1324 return err 1106 1325 } 1107 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 + 1108 1350 // t.RepoName (string) (string) 1109 1351 if len("repoName") > 1000000 { 1110 1352 return xerrors.Errorf("Value in field \"repoName\" was too long") ··· 1268 1510 } 1269 1511 1270 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) 1271 1524 } 1272 1525 // t.RepoName (string) (string) 1273 1526 case "repoName": ··· 3184 3437 } 3185 3438 3186 3439 cw := cbg.NewCborWriter(w) 3440 + fieldCount := 6 3187 3441 3188 - if _, err := cw.Write([]byte{165}); err != nil { 3442 + if t.SubjectDid == nil { 3443 + fieldCount-- 3444 + } 3445 + 3446 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 3189 3447 return err 3190 3448 } 3191 3449 ··· 3283 3541 return err 3284 3542 } 3285 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 + 3286 3576 // t.PerformedAt (string) (string) 3287 3577 if len("performedAt") > 1000000 { 3288 3578 return xerrors.Errorf("Value in field \"performedAt\" was too long") ··· 3468 3758 } 3469 3759 3470 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 + } 3471 3782 } 3472 3783 // t.PerformedAt (string) (string) 3473 3784 case "performedAt": ··· 5303 5614 5304 5615 cw := cbg.NewCborWriter(w) 5305 5616 5306 - if _, err := cw.Write([]byte{164}); err != nil { 5617 + if _, err := cw.Write([]byte{165}); err != nil { 5307 5618 return err 5308 5619 } 5309 5620 ··· 5376 5687 return err 5377 5688 } 5378 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 + 5379 5713 // t.DefaultBranch (string) (string) 5380 5714 if len("defaultBranch") > 1000000 { 5381 5715 return xerrors.Errorf("Value in field \"defaultBranch\" was too long") ··· 5474 5808 } 5475 5809 5476 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) 5477 5822 } 5478 5823 // t.DefaultBranch (string) (string) 5479 5824 case "defaultBranch": ··· 5906 6251 } 5907 6252 5908 6253 cw := cbg.NewCborWriter(w) 5909 - fieldCount := 10 6254 + fieldCount := 11 5910 6255 5911 6256 if t.Description == nil { 5912 6257 fieldCount-- 5913 6258 } 5914 6259 5915 6260 if t.Labels == nil { 6261 + fieldCount-- 6262 + } 6263 + 6264 + if t.RepoDid == nil { 5916 6265 fieldCount-- 5917 6266 } 5918 6267 ··· 6102 6451 return err 6103 6452 } 6104 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 + } 6105 6486 } 6106 6487 } 6107 6488 ··· 6401 6782 6402 6783 } 6403 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 + } 6404 6806 // t.Spindle (string) (string) 6405 6807 case "spindle": 6406 6808 ··· 6493 6895 } 6494 6896 6495 6897 cw := cbg.NewCborWriter(w) 6496 - fieldCount := 6 6898 + fieldCount := 7 6899 + 6900 + if t.Repo == nil { 6901 + fieldCount-- 6902 + } 6903 + 6904 + if t.RepoDid == nil { 6905 + fieldCount-- 6906 + } 6497 6907 6498 6908 if t.Tag == nil { 6499 6909 fieldCount-- ··· 6555 6965 } 6556 6966 6557 6967 // t.Repo (string) (string) 6558 - if len("repo") > 1000000 { 6559 - return xerrors.Errorf("Value in field \"repo\" was too long") 6560 - } 6968 + if t.Repo != nil { 6561 6969 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 - } 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 + } 6568 6980 6569 - if len(t.Repo) > 1000000 { 6570 - return xerrors.Errorf("Value in field t.Repo was too long") 6571 - } 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 + } 6572 6989 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 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 + } 6578 6997 } 6579 6998 6580 6999 // t.LexiconTypeID (string) (string) ··· 6596 7015 return err 6597 7016 } 6598 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 + 6599 7050 // t.Artifact (util.LexBlob) (struct) 6600 7051 if len("artifact") > 1000000 { 6601 7052 return xerrors.Errorf("Value in field \"artifact\" was too long") ··· 6716 7167 case "repo": 6717 7168 6718 7169 { 6719 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7170 + b, err := cr.ReadByte() 6720 7171 if err != nil { 6721 7172 return err 6722 7173 } 7174 + if b != cbg.CborNull[0] { 7175 + if err := cr.UnreadByte(); err != nil { 7176 + return err 7177 + } 6723 7178 6724 - t.Repo = string(sval) 7179 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7180 + if err != nil { 7181 + return err 7182 + } 7183 + 7184 + t.Repo = (*string)(&sval) 7185 + } 6725 7186 } 6726 7187 // t.LexiconTypeID (string) (string) 6727 7188 case "$type": ··· 6733 7194 } 6734 7195 6735 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 + } 6736 7218 } 6737 7219 // t.Artifact (util.LexBlob) (struct) 6738 7220 case "artifact": ··· 6783 7265 } 6784 7266 6785 7267 cw := cbg.NewCborWriter(w) 7268 + fieldCount := 5 6786 7269 6787 - if _, err := cw.Write([]byte{164}); err != nil { 6788 - return err 7270 + if t.Repo == nil { 7271 + fieldCount-- 6789 7272 } 6790 7273 6791 - // t.Repo (string) (string) 6792 - if len("repo") > 1000000 { 6793 - return xerrors.Errorf("Value in field \"repo\" was too long") 7274 + if t.RepoDid == nil { 7275 + fieldCount-- 6794 7276 } 6795 7277 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 { 7278 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 6800 7279 return err 6801 7280 } 6802 7281 6803 - if len(t.Repo) > 1000000 { 6804 - return xerrors.Errorf("Value in field t.Repo was too long") 6805 - } 7282 + // t.Repo (string) (string) 7283 + if t.Repo != nil { 6806 7284 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 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 + } 6812 7312 } 6813 7313 6814 7314 // t.LexiconTypeID (string) (string) ··· 6830 7330 return err 6831 7331 } 6832 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 + 6833 7365 // t.Subject (string) (string) 6834 7366 if len("subject") > 1000000 { 6835 7367 return xerrors.Errorf("Value in field \"subject\" was too long") ··· 6923 7455 case "repo": 6924 7456 6925 7457 { 6926 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7458 + b, err := cr.ReadByte() 6927 7459 if err != nil { 6928 7460 return err 6929 7461 } 7462 + if b != cbg.CborNull[0] { 7463 + if err := cr.UnreadByte(); err != nil { 7464 + return err 7465 + } 6930 7466 6931 - t.Repo = string(sval) 7467 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7468 + if err != nil { 7469 + return err 7470 + } 7471 + 7472 + t.Repo = (*string)(&sval) 7473 + } 6932 7474 } 6933 7475 // t.LexiconTypeID (string) (string) 6934 7476 case "$type": ··· 6941 7483 6942 7484 t.LexiconTypeID = string(sval) 6943 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 + } 6944 7507 // t.Subject (string) (string) 6945 7508 case "subject": 6946 7509 ··· 6981 7544 } 6982 7545 6983 7546 cw := cbg.NewCborWriter(w) 6984 - fieldCount := 7 7547 + fieldCount := 8 6985 7548 6986 7549 if t.Body == nil { 6987 7550 fieldCount-- ··· 6992 7555 } 6993 7556 6994 7557 if t.References == nil { 7558 + fieldCount-- 7559 + } 7560 + 7561 + if t.Repo == nil { 7562 + fieldCount-- 7563 + } 7564 + 7565 + if t.RepoDid == nil { 6995 7566 fieldCount-- 6996 7567 } 6997 7568 ··· 7032 7603 } 7033 7604 7034 7605 // t.Repo (string) (string) 7035 - if len("repo") > 1000000 { 7036 - return xerrors.Errorf("Value in field \"repo\" was too long") 7037 - } 7606 + if t.Repo != nil { 7038 7607 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 - } 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 + } 7045 7618 7046 - if len(t.Repo) > 1000000 { 7047 - return xerrors.Errorf("Value in field t.Repo was too long") 7048 - } 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 + } 7049 7627 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 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 + } 7055 7635 } 7056 7636 7057 7637 // t.LexiconTypeID (string) (string) ··· 7094 7674 } 7095 7675 if _, err := cw.WriteString(string(t.Title)); err != nil { 7096 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 + } 7097 7709 } 7098 7710 7099 7711 // t.Mentions ([]string) (slice) ··· 7259 7871 case "repo": 7260 7872 7261 7873 { 7262 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7874 + b, err := cr.ReadByte() 7263 7875 if err != nil { 7264 7876 return err 7265 7877 } 7878 + if b != cbg.CborNull[0] { 7879 + if err := cr.UnreadByte(); err != nil { 7880 + return err 7881 + } 7266 7882 7267 - t.Repo = string(sval) 7883 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7884 + if err != nil { 7885 + return err 7886 + } 7887 + 7888 + t.Repo = (*string)(&sval) 7889 + } 7268 7890 } 7269 7891 // t.LexiconTypeID (string) (string) 7270 7892 case "$type": ··· 7287 7909 } 7288 7910 7289 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 + } 7290 7933 } 7291 7934 // t.Mentions ([]string) (slice) 7292 7935 case "mentions": ··· 8890 9533 } 8891 9534 8892 9535 cw := cbg.NewCborWriter(w) 8893 - fieldCount := 3 9536 + fieldCount := 4 8894 9537 8895 9538 if t.Repo == nil { 9539 + fieldCount-- 9540 + } 9541 + 9542 + if t.RepoDid == nil { 8896 9543 fieldCount-- 8897 9544 } 8898 9545 ··· 8977 9624 if _, err := cw.WriteString(string(t.Branch)); err != nil { 8978 9625 return err 8979 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 + } 8980 9659 return nil 8981 9660 } 8982 9661 ··· 9005 9684 9006 9685 n := extra 9007 9686 9008 - nameBuf := make([]byte, 6) 9687 + nameBuf := make([]byte, 7) 9009 9688 for i := uint64(0); i < n; i++ { 9010 9689 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 9011 9690 if err != nil { ··· 9064 9743 9065 9744 t.Branch = string(sval) 9066 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 + } 9067 9767 9068 9768 default: 9069 9769 // Field doesn't exist on this type, so ignore it ··· 9246 9946 } 9247 9947 9248 9948 cw := cbg.NewCborWriter(w) 9949 + fieldCount := 3 9249 9950 9250 - if _, err := cw.Write([]byte{162}); err != nil { 9251 - return err 9951 + if t.Repo == nil { 9952 + fieldCount-- 9252 9953 } 9253 9954 9254 - // t.Repo (string) (string) 9255 - if len("repo") > 1000000 { 9256 - return xerrors.Errorf("Value in field \"repo\" was too long") 9955 + if t.RepoDid == nil { 9956 + fieldCount-- 9257 9957 } 9258 9958 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 { 9959 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 9263 9960 return err 9264 9961 } 9265 9962 9266 - if len(t.Repo) > 1000000 { 9267 - return xerrors.Errorf("Value in field t.Repo was too long") 9268 - } 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 + } 9269 9969 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 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 + } 9275 9993 } 9276 9994 9277 9995 // t.Branch (string) (string) ··· 9296 10014 if _, err := cw.WriteString(string(t.Branch)); err != nil { 9297 10015 return err 9298 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 + } 9299 10049 return nil 9300 10050 } 9301 10051 ··· 9324 10074 9325 10075 n := extra 9326 10076 9327 - nameBuf := make([]byte, 6) 10077 + nameBuf := make([]byte, 7) 9328 10078 for i := uint64(0); i < n; i++ { 9329 10079 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 9330 10080 if err != nil { ··· 9344 10094 case "repo": 9345 10095 9346 10096 { 9347 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 10097 + b, err := cr.ReadByte() 9348 10098 if err != nil { 9349 10099 return err 9350 10100 } 10101 + if b != cbg.CborNull[0] { 10102 + if err := cr.UnreadByte(); err != nil { 10103 + return err 10104 + } 9351 10105 9352 - t.Repo = string(sval) 10106 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 10107 + if err != nil { 10108 + return err 10109 + } 10110 + 10111 + t.Repo = (*string)(&sval) 10112 + } 9353 10113 } 9354 10114 // t.Branch (string) (string) 9355 10115 case "branch": ··· 9361 10121 } 9362 10122 9363 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 + } 9364 10145 } 9365 10146 9366 10147 default:
+5 -4
api/tangled/feedreaction.go
··· 17 17 } // 18 18 // RECORDTYPE: FeedReaction 19 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"` 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"` 24 25 }
+4 -3
api/tangled/feedstar.go
··· 17 17 } // 18 18 // RECORDTYPE: FeedStar 19 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"` 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"` 23 24 }
+3 -1
api/tangled/gitrefUpdate.go
··· 25 25 NewSha string `json:"newSha" cborgen:"newSha"` 26 26 // oldSha: old SHA of this ref 27 27 OldSha string `json:"oldSha" cborgen:"oldSha"` 28 + // ownerDid: did of the owner of the repo 29 + OwnerDid string `json:"ownerDid" cborgen:"ownerDid"` 28 30 // ref: Ref being updated 29 31 Ref string `json:"ref" cborgen:"ref"` 30 - // repoDid: did of the owner of the repo 32 + // repoDid: DID of the repo itself 31 33 RepoDid string `json:"repoDid" cborgen:"repoDid"` 32 34 // repoName: name of the repo 33 35 RepoName string `json:"repoName" cborgen:"repoName"`
+2 -1
api/tangled/labelop.go
··· 22 22 Delete []*LabelOp_Operand `json:"delete" cborgen:"delete"` 23 23 PerformedAt string `json:"performedAt" cborgen:"performedAt"` 24 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"` 25 + Subject string `json:"subject" cborgen:"subject"` 26 + SubjectDid *string `json:"subjectDid,omitempty" cborgen:"subjectDid,omitempty"` 26 27 } 27 28 28 29 // LabelOp_Operand is a "operand" in the sh.tangled.label.op schema.
+2 -1
api/tangled/repoartifact.go
··· 25 25 // name: name of the artifact 26 26 Name string `json:"name" cborgen:"name"` 27 27 // repo: repo that this artifact is being uploaded to 28 - Repo string `json:"repo" cborgen:"repo"` 28 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 29 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 29 30 // tag: hash of the tag object that this artifact is attached to (only annotated tags are supported) 30 31 Tag util.LexBytes `json:"tag,omitempty" cborgen:"tag,omitempty"` 31 32 }
+3 -2
api/tangled/repocollaborator.go
··· 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.collaborator" cborgen:"$type,const=sh.tangled.repo.collaborator"` 21 21 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 22 // repo: repo to add this user to 23 - Repo string `json:"repo" cborgen:"repo"` 24 - Subject string `json:"subject" cborgen:"subject"` 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"` 25 26 }
+14 -4
api/tangled/repocreate.go
··· 18 18 type RepoCreate_Input struct { 19 19 // defaultBranch: Default branch to push to 20 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"` 21 25 // rkey: Rkey of the repository record 22 26 Rkey string `json:"rkey" cborgen:"rkey"` 23 27 // source: A source URL to clone from, populate this when forking or importing a repository. 24 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"` 25 34 } 26 35 27 36 // 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 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 31 41 } 32 42 33 - return nil 43 + return &out, nil 34 44 }
+2 -1
api/tangled/repoissue.go
··· 22 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 23 Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 24 References []string `json:"references,omitempty" cborgen:"references,omitempty"` 25 - Repo string `json:"repo" cborgen:"repo"` 25 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 26 + RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 26 27 Title string `json:"title" cborgen:"title"` 27 28 }
+7 -5
api/tangled/repopull.go
··· 33 33 34 34 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema. 35 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"` 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"` 39 40 } 40 41 41 42 // RepoPull_Target is a "target" in the sh.tangled.repo.pull schema. 42 43 type RepoPull_Target struct { 43 - Branch string `json:"branch" cborgen:"branch"` 44 - Repo string `json:"repo" cborgen:"repo"` 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"` 45 47 }
+2
api/tangled/tangledpipeline.go
··· 70 70 Did string `json:"did" cborgen:"did"` 71 71 Knot string `json:"knot" cborgen:"knot"` 72 72 Repo string `json:"repo" cborgen:"repo"` 73 + // repoDid: DID of the repo itself 74 + RepoDid string `json:"repoDid" cborgen:"repoDid"` 73 75 } 74 76 75 77 // Pipeline_Workflow is a "workflow" in the sh.tangled.pipeline schema.
+2
api/tangled/tangledrepo.go
··· 26 26 Labels []string `json:"labels,omitempty" cborgen:"labels,omitempty"` 27 27 // name: name of the repo 28 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"` 29 31 // source: source of the repo 30 32 Source *string `json:"source,omitempty" cborgen:"source,omitempty"` 31 33 // spindle: CI runner to send jobs to and receive results from
+14 -1
appview/db/artifact.go
··· 1 1 package db 2 2 3 3 import ( 4 + "database/sql" 4 5 "fmt" 5 6 "strings" 6 7 "time" ··· 12 13 ) 13 14 14 15 func AddArtifact(e Execer, artifact models.Artifact) error { 16 + var repoDid *string 17 + if artifact.RepoDid != "" { 18 + repoDid = &artifact.RepoDid 19 + } 15 20 _, err := e.Exec( 16 21 `insert or ignore into artifacts ( 17 22 did, 18 23 rkey, 19 24 repo_at, 25 + repo_did, 20 26 tag, 21 27 created, 22 28 blob_cid, ··· 24 30 size, 25 31 mimetype 26 32 ) 27 - values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 33 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 28 34 artifact.Did, 29 35 artifact.Rkey, 30 36 artifact.RepoAt, 37 + repoDid, 31 38 artifact.Tag[:], 32 39 artifact.CreatedAt.Format(time.RFC3339), 33 40 artifact.BlobCid.String(), ··· 57 64 did, 58 65 rkey, 59 66 repo_at, 67 + repo_did, 60 68 tag, 61 69 created, 62 70 blob_cid, ··· 78 86 var createdAt string 79 87 var tag []byte 80 88 var blobCid string 89 + var repoDid sql.NullString 81 90 82 91 if err := rows.Scan( 83 92 &artifact.Did, 84 93 &artifact.Rkey, 85 94 &artifact.RepoAt, 95 + &repoDid, 86 96 &tag, 87 97 &createdAt, 88 98 &blobCid, ··· 91 101 &artifact.MimeType, 92 102 ); err != nil { 93 103 return nil, err 104 + } 105 + if repoDid.Valid { 106 + artifact.RepoDid = repoDid.String 94 107 } 95 108 96 109 artifact.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
+15 -3
appview/db/collaborators.go
··· 1 1 package db 2 2 3 3 import ( 4 + "database/sql" 4 5 "fmt" 5 6 "strings" 6 7 "time" ··· 10 11 ) 11 12 12 13 func AddCollaborator(e Execer, c models.Collaborator) error { 14 + var repoDid *string 15 + if c.RepoDid != "" { 16 + repoDid = &c.RepoDid 17 + } 18 + 13 19 _, err := e.Exec( 14 - `insert into collaborators (did, rkey, subject_did, repo_at) values (?, ?, ?, ?);`, 15 - c.Did, c.Rkey, c.SubjectDid, c.RepoAt, 20 + `insert into collaborators (did, rkey, subject_did, repo_at, repo_did) values (?, ?, ?, ?, ?);`, 21 + c.Did, c.Rkey, c.SubjectDid, c.RepoAt, repoDid, 16 22 ) 17 23 return err 18 24 } ··· 80 86 rkey, 81 87 subject_did, 82 88 repo_at, 83 - created 89 + created, 90 + repo_did 84 91 from collaborators %s`, 85 92 whereClause, 86 93 ) ··· 92 99 for rows.Next() { 93 100 var collaborator models.Collaborator 94 101 var createdAt string 102 + var collabRepoDid sql.NullString 95 103 if err := rows.Scan( 96 104 &collaborator.Id, 97 105 &collaborator.Did, ··· 99 107 &collaborator.SubjectDid, 100 108 &collaborator.RepoAt, 101 109 &createdAt, 110 + &collabRepoDid, 102 111 ); err != nil { 103 112 return nil, err 104 113 } 105 114 collaborator.Created, err = time.Parse(time.RFC3339, createdAt) 106 115 if err != nil { 107 116 collaborator.Created = time.Now() 117 + } 118 + if collabRepoDid.Valid { 119 + collaborator.RepoDid = collabRepoDid.String 108 120 } 109 121 collaborators = append(collaborators, collaborator) 110 122 }
+318
appview/db/db.go
··· 1255 1255 return err 1256 1256 }) 1257 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 + 1258 1576 return &DB{ 1259 1577 db, 1260 1578 logger,
+28 -10
appview/db/issues.go
··· 17 17 ) 18 18 19 19 func PutIssue(tx *sql.Tx, issue *models.Issue) error { 20 - // ensure sequence exists 20 + var seqRepoDid *string 21 + if issue.RepoDid != "" { 22 + seqRepoDid = &issue.RepoDid 23 + } 21 24 _, err := tx.Exec(` 22 - insert or ignore into repo_issue_seqs (repo_at, next_issue_id) 23 - values (?, 1) 24 - `, issue.RepoAt) 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) 25 29 if err != nil { 26 30 return err 27 31 } ··· 64 68 return err 65 69 } 66 70 71 + var repoDid *string 72 + if issue.RepoDid != "" { 73 + repoDid = &issue.RepoDid 74 + } 75 + 67 76 // insert new issue 68 77 row := tx.QueryRow(` 69 - insert into issues (repo_at, did, rkey, issue_id, title, body) 70 - values (?, ?, ?, ?, ?, ?) 78 + insert into issues (repo_at, repo_did, did, rkey, issue_id, title, body) 79 + values (?, ?, ?, ?, ?, ?, ?) 71 80 returning rowid, issue_id 72 - `, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body) 81 + `, issue.RepoAt, repoDid, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body) 73 82 74 83 err = row.Scan(&issue.Id, &issue.IssueId) 75 84 if err != nil { ··· 83 92 } 84 93 85 94 func updateIssue(tx *sql.Tx, issue *models.Issue) error { 86 - // update existing issue 95 + var repoDid *string 96 + if issue.RepoDid != "" { 97 + repoDid = &issue.RepoDid 98 + } 87 99 _, err := tx.Exec(` 88 100 update issues 89 - set title = ?, body = ?, edited = ? 101 + set title = ?, body = ?, edited = ?, repo_did = coalesce(?, repo_did) 90 102 where did = ? and rkey = ? 91 - `, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey) 103 + `, issue.Title, issue.Body, time.Now().Format(time.RFC3339), repoDid, issue.Did, issue.Rkey) 92 104 if err != nil { 93 105 return err 94 106 } ··· 133 145 did, 134 146 rkey, 135 147 repo_at, 148 + repo_did, 136 149 issue_id, 137 150 title, 138 151 body, ··· 161 174 var issue models.Issue 162 175 var createdAt string 163 176 var editedAt, deletedAt sql.Null[string] 177 + var nullableRepoDid sql.NullString 164 178 var rowNum int64 165 179 err := rows.Scan( 166 180 &issue.Id, 167 181 &issue.Did, 168 182 &issue.Rkey, 169 183 &issue.RepoAt, 184 + &nullableRepoDid, 170 185 &issue.IssueId, 171 186 &issue.Title, 172 187 &issue.Body, ··· 178 193 ) 179 194 if err != nil { 180 195 return nil, fmt.Errorf("failed to scan issue: %w", err) 196 + } 197 + if nullableRepoDid.Valid { 198 + issue.RepoDid = nullableRepoDid.String 181 199 } 182 200 183 201 if t, err := time.Parse(time.RFC3339, createdAt); err == nil {
+13 -3
appview/db/language.go
··· 24 24 } 25 25 26 26 query := fmt.Sprintf( 27 - `select id, repo_at, ref, is_default_ref, language, bytes from repo_languages %s`, 27 + `select id, repo_at, ref, is_default_ref, language, bytes, repo_did from repo_languages %s`, 28 28 whereClause, 29 29 ) 30 30 rows, err := e.Query(query, args...) ··· 37 37 for rows.Next() { 38 38 var rl models.RepoLanguage 39 39 var isDefaultRef int 40 + var langRepoDid sql.NullString 40 41 41 42 err := rows.Scan( 42 43 &rl.Id, ··· 45 46 &isDefaultRef, 46 47 &rl.Language, 47 48 &rl.Bytes, 49 + &langRepoDid, 48 50 ) 49 51 if err != nil { 50 52 return nil, fmt.Errorf("failed to scan: %w ", err) ··· 53 55 if isDefaultRef != 0 { 54 56 rl.IsDefaultRef = true 55 57 } 58 + if langRepoDid.Valid { 59 + rl.RepoDid = langRepoDid.String 60 + } 56 61 57 62 langs = append(langs, rl) 58 63 } ··· 65 70 66 71 func InsertRepoLanguages(e Execer, langs []models.RepoLanguage) error { 67 72 stmt, err := e.Prepare( 68 - "insert or replace into repo_languages (repo_at, ref, is_default_ref, language, bytes) values (?, ?, ?, ?, ?)", 73 + "insert or replace into repo_languages (repo_at, ref, is_default_ref, language, bytes, repo_did) values (?, ?, ?, ?, ?, ?)", 69 74 ) 70 75 if err != nil { 71 76 return err ··· 77 82 isDefaultRef = 1 78 83 } 79 84 80 - _, err := stmt.Exec(l.RepoAt, l.Ref, isDefaultRef, l.Language, l.Bytes) 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) 81 91 if err != nil { 82 92 return err 83 93 }
+21 -2
appview/db/pipeline.go
··· 1 1 package db 2 2 3 3 import ( 4 + "database/sql" 4 5 "fmt" 5 6 "slices" 6 7 "strings" ··· 26 27 whereClause = " where " + strings.Join(conditions, " and ") 27 28 } 28 29 29 - query := fmt.Sprintf(`select id, rkey, knot, repo_owner, repo_name, sha, created from pipelines %s`, whereClause) 30 + query := fmt.Sprintf(`select id, rkey, knot, repo_owner, repo_name, sha, created, repo_did from pipelines %s`, whereClause) 30 31 31 32 rows, err := e.Query(query, args...) 32 33 ··· 38 39 for rows.Next() { 39 40 var pipeline models.Pipeline 40 41 var createdAt string 42 + var repoDid sql.NullString 41 43 err = rows.Scan( 42 44 &pipeline.Id, 43 45 &pipeline.Rkey, ··· 46 48 &pipeline.RepoName, 47 49 &pipeline.Sha, 48 50 &createdAt, 51 + &repoDid, 49 52 ) 50 53 if err != nil { 51 54 return nil, err ··· 54 57 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 55 58 pipeline.Created = t 56 59 } 60 + if repoDid.Valid { 61 + pipeline.RepoDid = repoDid.String 62 + } 57 63 58 64 pipelines = append(pipelines, pipeline) 59 65 } ··· 66 72 } 67 73 68 74 func AddPipeline(e Execer, pipeline models.Pipeline) error { 75 + var repoDid *string 76 + if pipeline.RepoDid != "" { 77 + repoDid = &pipeline.RepoDid 78 + } 79 + 69 80 args := []any{ 70 81 pipeline.Rkey, 71 82 pipeline.Knot, ··· 73 84 pipeline.RepoName, 74 85 pipeline.TriggerId, 75 86 pipeline.Sha, 87 + repoDid, 76 88 } 77 89 78 90 placeholders := make([]string, len(args)) ··· 87 99 repo_owner, 88 100 repo_name, 89 101 trigger_id, 90 - sha 102 + sha, 103 + repo_did 91 104 ) values (%s) 92 105 `, strings.Join(placeholders, ",")) 93 106 ··· 195 208 p.repo_name, 196 209 p.sha, 197 210 p.created, 211 + p.repo_did, 198 212 t.id, 199 213 t.kind, 200 214 t.push_ref, ··· 224 238 var p models.Pipeline 225 239 var t models.Trigger 226 240 var created string 241 + var repoDid sql.NullString 227 242 228 243 err := rows.Scan( 229 244 &p.Id, ··· 233 248 &p.RepoName, 234 249 &p.Sha, 235 250 &created, 251 + &repoDid, 236 252 &p.TriggerId, 237 253 &t.Kind, 238 254 &t.PushRef, ··· 250 266 p.Created, err = time.Parse(time.RFC3339, created) 251 267 if err != nil { 252 268 return nil, fmt.Errorf("invalid pipeline created timestamp %q: %w", created, err) 269 + } 270 + if repoDid.Valid { 271 + p.RepoDid = repoDid.String 253 272 } 254 273 255 274 t.Id = p.TriggerId
+6 -3
appview/db/profile.go
··· 75 75 // TODO: get this in the original query; requires COALESCE because nullable 76 76 var sourceRepo *models.Repo 77 77 if repo.Source != "" { 78 - sourceRepo, err = GetRepoByAtUri(e, 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 + } 79 83 if err != nil { 80 - // the source repo was not found, skip this bit 81 84 log.Println("profile", "err", err) 82 85 } 83 86 } ··· 449 452 query = `select count(id) from repos where did = ?` 450 453 args = append(args, did) 451 454 case models.VanityStatStarCount: 452 - query = `select count(id) from stars where subject_at like 'at://' || ? || '%'` 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 = ?` 453 456 args = append(args, did) 454 457 case models.VanityStatNone: 455 458 return 0, nil
+57 -12
appview/db/pulls.go
··· 18 18 ) 19 19 20 20 func NewPull(tx *sql.Tx, pull *models.Pull) error { 21 + var repoDid *string 22 + if pull.RepoDid != "" { 23 + repoDid = &pull.RepoDid 24 + } 25 + 21 26 _, err := tx.Exec(` 22 - insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 23 - values (?, 1) 24 - `, pull.RepoAt) 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) 25 31 if err != nil { 26 32 return err 27 33 } ··· 40 46 pull.PullId = nextId 41 47 pull.State = models.PullOpen 42 48 43 - var sourceBranch, sourceRepoAt *string 49 + var sourceBranch, sourceRepoAt, sourceRepoDid *string 44 50 if pull.PullSource != nil { 45 51 sourceBranch = &pull.PullSource.Branch 46 52 if pull.PullSource.RepoAt != nil { 47 53 x := pull.PullSource.RepoAt.String() 48 54 sourceRepoAt = &x 49 55 } 56 + if pull.PullSource.RepoDid != "" { 57 + sourceRepoDid = &pull.PullSource.RepoDid 58 + } 50 59 } 51 60 52 61 var stackId, changeId, parentChangeId *string ··· 63 72 result, err := tx.Exec( 64 73 ` 65 74 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 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 67 76 ) 68 - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 77 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 69 78 pull.RepoAt, 70 79 pull.OwnerDid, 71 80 pull.PullId, ··· 79 88 stackId, 80 89 changeId, 81 90 parentChangeId, 91 + repoDid, 92 + sourceRepoDid, 82 93 ) 83 94 if err != nil { 84 95 return err ··· 159 170 source_repo_at, 160 171 stack_id, 161 172 change_id, 162 - parent_change_id 173 + parent_change_id, 174 + repo_did, 175 + source_repo_did 163 176 from 164 177 pulls 165 178 %s ··· 177 190 for rows.Next() { 178 191 var pull models.Pull 179 192 var createdAt string 180 - var sourceBranch, sourceRepoAt, stackId, changeId, parentChangeId sql.NullString 193 + var sourceBranch, sourceRepoAt, stackId, changeId, parentChangeId, repoDid, sourceRepoDid sql.NullString 181 194 err := rows.Scan( 182 195 &pull.ID, 183 196 &pull.OwnerDid, ··· 194 207 &stackId, 195 208 &changeId, 196 209 &parentChangeId, 210 + &repoDid, 211 + &sourceRepoDid, 197 212 ) 198 213 if err != nil { 199 214 return nil, err ··· 216 231 } 217 232 pull.PullSource.RepoAt = &sourceRepoAtParsed 218 233 } 234 + if sourceRepoDid.Valid { 235 + pull.PullSource.RepoDid = sourceRepoDid.String 236 + } 219 237 } 220 238 221 239 if stackId.Valid { ··· 227 245 if parentChangeId.Valid { 228 246 pull.ParentChangeId = parentChangeId.String 229 247 } 248 + if repoDid.Valid { 249 + pull.RepoDid = repoDid.String 250 + } 230 251 231 252 pulls[pull.AtUri()] = &pull 232 253 } ··· 441 462 owner_did, 442 463 comment_at, 443 464 body, 444 - created 465 + created, 466 + repo_did 445 467 from 446 468 pull_comments 447 469 %s ··· 459 481 for rows.Next() { 460 482 var comment models.PullComment 461 483 var createdAt string 484 + var commentRepoDid sql.NullString 462 485 err := rows.Scan( 463 486 &comment.ID, 464 487 &comment.PullId, ··· 468 491 &comment.CommentAt, 469 492 &comment.Body, 470 493 &createdAt, 494 + &commentRepoDid, 471 495 ) 472 496 if err != nil { 473 497 return nil, err ··· 475 499 476 500 if t, err := time.Parse(time.RFC3339, createdAt); err == nil { 477 501 comment.Created = t 502 + } 503 + if commentRepoDid.Valid { 504 + comment.RepoDid = commentRepoDid.String 478 505 } 479 506 480 507 atUri := comment.AtUri().String() ··· 522 549 p.created, 523 550 p.title, 524 551 p.state, 552 + p.repo_did, 525 553 r.did, 526 554 r.name, 527 555 r.knot, 528 556 r.rkey, 529 - r.created 557 + r.created, 558 + r.repo_did 530 559 from 531 560 pulls p 532 561 join ··· 544 573 var pull models.Pull 545 574 var repo models.Repo 546 575 var pullCreatedAt, repoCreatedAt string 576 + var pullRepoDid, repoRepoDid sql.NullString 547 577 err := rows.Scan( 548 578 &pull.OwnerDid, 549 579 &pull.RepoAt, ··· 551 581 &pullCreatedAt, 552 582 &pull.Title, 553 583 &pull.State, 584 + &pullRepoDid, 554 585 &repo.Did, 555 586 &repo.Name, 556 587 &repo.Knot, 557 588 &repo.Rkey, 558 589 &repoCreatedAt, 590 + &repoRepoDid, 559 591 ) 560 592 if err != nil { 561 593 return nil, err 562 594 } 563 595 596 + if pullRepoDid.Valid { 597 + pull.RepoDid = pullRepoDid.String 598 + } 599 + 564 600 pullCreatedTime, err := time.Parse(time.RFC3339, pullCreatedAt) 565 601 if err != nil { 566 602 return nil, err ··· 572 608 return nil, err 573 609 } 574 610 repo.Created = repoCreatedTime 611 + if repoRepoDid.Valid { 612 + repo.RepoDid = repoRepoDid.String 613 + } 575 614 576 615 pull.Repo = &repo 577 616 ··· 586 625 } 587 626 588 627 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 (?, ?, ?, ?, ?, ?)` 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 (?, ?, ?, ?, ?, ?, ?)` 590 634 res, err := tx.Exec( 591 635 query, 592 636 comment.OwnerDid, ··· 595 639 comment.CommentAt, 596 640 comment.PullId, 597 641 comment.Body, 642 + repoDid, 598 643 ) 599 644 if err != nil { 600 645 return 0, err ··· 614 659 615 660 func SetPullState(e Execer, repoAt syntax.ATURI, pullId int, pullState models.PullState) error { 616 661 _, err := e.Exec( 617 - `update pulls set state = ? where repo_at = ? and pull_id = ? and (state <> ? or state <> ?)`, 662 + `update pulls set state = ? where repo_at = ? and pull_id = ? and (state <> ? and state <> ?)`, 618 663 pullState, 619 664 repoAt, 620 665 pullId,
+36 -12
appview/db/reference.go
··· 60 60 on r.did = inp.owner_did 61 61 and r.name = inp.name 62 62 join issues i 63 - on i.repo_at = r.at_uri 63 + on coalesce( 64 + nullif(i.repo_did, '') = nullif(r.repo_did, ''), 65 + i.repo_at = r.at_uri 66 + ) 64 67 and i.issue_id = inp.issue_id 65 68 left join issue_comments c 66 69 on inp.comment_id is not null ··· 131 134 on r.did = inp.owner_did 132 135 and r.name = inp.name 133 136 join pulls p 134 - on p.repo_at = r.at_uri 137 + on coalesce( 138 + nullif(p.repo_did, '') = nullif(r.repo_did, ''), 139 + p.repo_at = r.at_uri 140 + ) 135 141 and p.pull_id = inp.pull_id 136 142 left join pull_comments c 137 143 on inp.comment_id is not null 138 - and c.repo_at = r.at_uri and c.pull_id = p.pull_id 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 139 148 and c.id = inp.comment_id 140 149 `, 141 150 strings.Join(vals, ","), ··· 316 325 } 317 326 rows, err := e.Query( 318 327 fmt.Sprintf( 319 - `select r.did, r.name, i.issue_id, i.title, i.open 328 + `select distinct r.did, r.name, i.issue_id, i.title, i.open 320 329 from issues i 321 330 join repos r 322 - on r.at_uri = i.repo_at 331 + on coalesce( 332 + nullif(i.repo_did, '') = nullif(r.repo_did, ''), 333 + i.repo_at = r.at_uri 334 + ) 323 335 where (i.did, i.rkey) in (%s)`, 324 336 strings.Join(vals, ","), 325 337 ), ··· 351 363 filter := orm.FilterIn("c.at_uri", aturis) 352 364 rows, err := e.Query( 353 365 fmt.Sprintf( 354 - `select r.did, r.name, i.issue_id, c.id, i.title, i.open 366 + `select distinct r.did, r.name, i.issue_id, c.id, i.title, i.open 355 367 from issue_comments c 356 368 join issues i 357 369 on i.at_uri = c.issue_at 358 370 join repos r 359 - on r.at_uri = i.repo_at 371 + on coalesce( 372 + nullif(i.repo_did, '') = nullif(r.repo_did, ''), 373 + i.repo_at = r.at_uri 374 + ) 360 375 where %s`, 361 376 filter.Condition(), 362 377 ), ··· 396 411 } 397 412 rows, err := e.Query( 398 413 fmt.Sprintf( 399 - `select r.did, r.name, p.pull_id, p.title, p.state 414 + `select distinct r.did, r.name, p.pull_id, p.title, p.state 400 415 from pulls p 401 416 join repos r 402 - on r.at_uri = p.repo_at 417 + on coalesce( 418 + nullif(p.repo_did, '') = nullif(r.repo_did, ''), 419 + p.repo_at = r.at_uri 420 + ) 403 421 where (p.owner_did, p.rkey) in (%s)`, 404 422 strings.Join(vals, ","), 405 423 ), ··· 431 449 filter := orm.FilterIn("c.comment_at", aturis) 432 450 rows, err := e.Query( 433 451 fmt.Sprintf( 434 - `select r.did, r.name, p.pull_id, c.id, p.title, p.state 452 + `select distinct r.did, r.name, p.pull_id, c.id, p.title, p.state 435 453 from repos r 436 454 join pulls p 437 - on r.at_uri = p.repo_at 455 + on coalesce( 456 + nullif(p.repo_did, '') = nullif(r.repo_did, ''), 457 + p.repo_at = r.at_uri 458 + ) 438 459 join pull_comments c 439 - on r.at_uri = c.repo_at and p.pull_id = c.pull_id 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 440 464 where %s`, 441 465 filter.Condition(), 442 466 ),
+107 -26
appview/db/repos.go
··· 46 46 website, 47 47 topics, 48 48 source, 49 - spindle 49 + spindle, 50 + repo_did 50 51 from 51 52 repos r 52 53 %s ··· 64 65 for rows.Next() { 65 66 var repo models.Repo 66 67 var createdAt string 67 - var description, website, topicStr, source, spindle sql.NullString 68 + var description, website, topicStr, source, spindle, repoDid sql.NullString 68 69 69 70 err := rows.Scan( 70 71 &repo.Id, ··· 78 79 &topicStr, 79 80 &source, 80 81 &spindle, 82 + &repoDid, 81 83 ) 82 84 if err != nil { 83 85 return nil, fmt.Errorf("failed to execute repo query: %w ", err) ··· 100 102 } 101 103 if spindle.Valid { 102 104 repo.Spindle = spindle.String 105 + } 106 + if repoDid.Valid { 107 + repo.RepoDid = repoDid.String 103 108 } 104 109 105 110 repo.RepoStats = &models.RepoStats{} ··· 184 189 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 185 190 } 186 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 + } 187 205 starCountQuery := fmt.Sprintf( 188 - `select 189 - subject_at, count(1) 206 + `select coalesce(subject_did, subject_at) as key, count(1) 190 207 from stars 191 - where subject_at in (%s) 192 - group by subject_at`, 193 - inClause, 208 + where subject_at in (%s) or subject_did in (%s) 209 + group by key`, 210 + inClause, didInClause, 194 211 ) 195 - rows, err = e.Query(starCountQuery, args...) 212 + starArgs := append(append([]any{}, args...), repoDids...) 213 + rows, err = e.Query(starCountQuery, starArgs...) 196 214 if err != nil { 197 215 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 198 216 } 199 217 defer rows.Close() 200 218 201 219 for rows.Next() { 202 - var repoat string 220 + var key string 203 221 var count int 204 - if err := rows.Scan(&repoat, &count); err != nil { 222 + if err := rows.Scan(&key, &count); err != nil { 205 223 log.Println("err", "err", err) 206 224 continue 207 225 } 208 - if r, ok := repoMap[syntax.ATURI(repoat)]; ok { 226 + if r, ok := repoMap[syntax.ATURI(key)]; ok { 209 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 + } 210 232 } 211 233 } 212 234 if err = rows.Err(); err != nil { ··· 352 374 var nullableDescription sql.NullString 353 375 var nullableWebsite sql.NullString 354 376 var nullableTopicStr sql.NullString 377 + var nullableRepoDid sql.NullString 378 + var nullableSource sql.NullString 379 + var nullableSpindle sql.NullString 355 380 356 - row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics from repos where at_uri = ?`, atUri) 381 + row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, source, spindle, repo_did from repos where at_uri = ?`, atUri) 357 382 358 383 var createdAt string 359 - if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr); err != nil { 384 + if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableSource, &nullableSpindle, &nullableRepoDid); err != nil { 360 385 return nil, err 361 386 } 362 387 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 371 396 if nullableTopicStr.Valid { 372 397 repo.Topics = strings.Fields(nullableTopicStr.String) 373 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 + } 374 408 375 409 return &repo, nil 376 410 } 377 411 378 412 func PutRepo(tx *sql.Tx, repo models.Repo) error { 413 + var repoDid *string 414 + if repo.RepoDid != "" { 415 + repoDid = &repo.RepoDid 416 + } 379 417 _, err := tx.Exec( 380 418 `update repos 381 - set knot = ?, description = ?, website = ?, topics = ? 419 + set knot = ?, description = ?, website = ?, topics = ?, repo_did = coalesce(?, repo_did) 382 420 where did = ? and rkey = ? 383 421 `, 384 - repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repo.Did, repo.Rkey, 422 + repo.Knot, repo.Description, repo.Website, repo.TopicStr(), repoDid, repo.Did, repo.Rkey, 385 423 ) 386 424 return err 387 425 } 388 426 389 427 func AddRepo(tx *sql.Tx, repo *models.Repo) error { 428 + var repoDid *string 429 + if repo.RepoDid != "" { 430 + repoDid = &repo.RepoDid 431 + } 390 432 _, err := tx.Exec( 391 433 `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, 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, 395 437 ) 396 438 if err != nil { 397 439 return fmt.Errorf("failed to insert repo: %w", err) ··· 401 443 if err := SubscribeLabel(tx, &models.RepoLabel{ 402 444 RepoAt: repo.RepoAt(), 403 445 LabelAt: syntax.ATURI(dl), 446 + RepoDid: repo.RepoDid, 404 447 }); err != nil { 405 448 return fmt.Errorf("failed to subscribe to label: %w", err) 406 449 } ··· 431 474 if err != nil { 432 475 return nil, err 433 476 } 477 + if strings.HasPrefix(source, "did:") { 478 + return GetRepoByDid(e, source) 479 + } 434 480 return GetRepoByAtUri(e, source) 435 481 } 436 482 ··· 438 484 var repos []models.Repo 439 485 440 486 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 487 + `select distinct r.id, r.did, r.name, r.knot, r.rkey, r.description, r.website, r.created, r.source, r.repo_did 442 488 from repos r 443 489 left join collaborators c on r.at_uri = c.repo_at 444 490 where (r.did = ? or c.subject_did = ?) ··· 458 504 var nullableDescription sql.NullString 459 505 var nullableWebsite sql.NullString 460 506 var nullableSource sql.NullString 507 + var nullableRepoDid sql.NullString 461 508 462 - err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource) 509 + err := rows.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &createdAt, &nullableSource, &nullableRepoDid) 463 510 if err != nil { 464 511 return nil, err 465 512 } ··· 467 514 if nullableDescription.Valid { 468 515 repo.Description = nullableDescription.String 469 516 } 517 + if nullableWebsite.Valid { 518 + repo.Website = nullableWebsite.String 519 + } 470 520 471 521 if nullableSource.Valid { 472 522 repo.Source = nullableSource.String 473 523 } 524 + if nullableRepoDid.Valid { 525 + repo.RepoDid = nullableRepoDid.String 526 + } 474 527 475 528 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 476 529 if err != nil { ··· 496 549 var nullableWebsite sql.NullString 497 550 var nullableTopicStr sql.NullString 498 551 var nullableSource sql.NullString 552 + var nullableRepoDid sql.NullString 499 553 500 554 row := e.QueryRow( 501 - `select id, did, name, knot, rkey, description, website, topics, created, source 555 + `select id, did, name, knot, rkey, description, website, topics, created, source, repo_did 502 556 from repos 503 557 where did = ? and name = ? and source is not null and source != ''`, 504 558 did, name, 505 559 ) 506 560 507 - err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &createdAt, &nullableSource) 561 + err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &createdAt, &nullableSource, &nullableRepoDid) 508 562 if err != nil { 509 563 return nil, err 510 564 } ··· 524 578 if nullableSource.Valid { 525 579 repo.Source = nullableSource.String 526 580 } 581 + if nullableRepoDid.Valid { 582 + repo.RepoDid = nullableRepoDid.String 583 + } 527 584 528 585 createdAtTime, err := time.Parse(time.RFC3339, createdAt) 529 586 if err != nil { ··· 535 592 return &repo, nil 536 593 } 537 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 + 538 609 func UpdateDescription(e Execer, repoAt, newDescription string) error { 539 610 _, err := e.Exec( 540 611 `update repos set description = ? where at_uri = ?`, newDescription, repoAt) ··· 548 619 } 549 620 550 621 func SubscribeLabel(e Execer, rl *models.RepoLabel) error { 551 - query := `insert or ignore into repo_labels (repo_at, label_at) values (?, ?)` 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)` 552 629 553 - _, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String()) 630 + _, err := e.Exec(query, rl.RepoAt.String(), rl.LabelAt.String(), repoDid) 554 631 return err 555 632 } 556 633 ··· 585 662 whereClause = " where " + strings.Join(conditions, " and ") 586 663 } 587 664 588 - query := fmt.Sprintf(`select id, repo_at, label_at from repo_labels %s`, whereClause) 665 + query := fmt.Sprintf(`select id, repo_at, label_at, repo_did from repo_labels %s`, whereClause) 589 666 590 667 rows, err := e.Query(query, args...) 591 668 if err != nil { ··· 596 673 var labels []models.RepoLabel 597 674 for rows.Next() { 598 675 var label models.RepoLabel 676 + var labelRepoDid sql.NullString 599 677 600 - err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt) 678 + err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt, &labelRepoDid) 601 679 if err != nil { 602 680 return nil, err 681 + } 682 + if labelRepoDid.Valid { 683 + label.RepoDid = labelRepoDid.String 603 684 } 604 685 605 686 labels = append(labels, label)
+29 -5
appview/db/star.go
··· 15 15 ) 16 16 17 17 func AddStar(e Execer, star *models.Star) error { 18 - query := `insert or ignore into stars (did, subject_at, rkey) values (?, ?, ?)` 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 + } 19 23 _, err := e.Exec( 20 24 query, 21 25 star.Did, 22 26 star.RepoAt.String(), 23 27 star.Rkey, 28 + subjectDid, 24 29 ) 25 30 return err 26 31 } ··· 28 33 // Get a star record 29 34 func GetStar(e Execer, did string, subjectAt syntax.ATURI) (*models.Star, error) { 30 35 query := ` 31 - select did, subject_at, created, rkey 36 + select did, subject_at, created, rkey, subject_did 32 37 from stars 33 38 where did = ? and subject_at = ?` 34 39 row := e.QueryRow(query, did, subjectAt) 35 40 36 41 var star models.Star 37 42 var created string 38 - err := row.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey) 43 + var nullableSubjectDid sql.NullString 44 + err := row.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey, &nullableSubjectDid) 39 45 if err != nil { 40 46 return nil, err 47 + } 48 + if nullableSubjectDid.Valid { 49 + star.SubjectDid = nullableSubjectDid.String 41 50 } 42 51 43 52 createdAtTime, err := time.Parse(time.RFC3339, created) ··· 73 82 return stars, nil 74 83 } 75 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 + 76 96 // getStarStatuses returns a map of repo URIs to star status for a given user 77 97 // This is an internal helper function to avoid N+1 queries 78 98 func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) { ··· 153 173 } 154 174 155 175 repoQuery := fmt.Sprintf( 156 - `select did, subject_at, created, rkey 176 + `select did, subject_at, created, rkey, subject_did 157 177 from stars 158 178 %s 159 179 order by created desc ··· 171 191 for rows.Next() { 172 192 var star models.Star 173 193 var created string 174 - err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey) 194 + var nullableSubjectDid sql.NullString 195 + err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey, &nullableSubjectDid) 175 196 if err != nil { 176 197 return nil, err 198 + } 199 + if nullableSubjectDid.Valid { 200 + star.SubjectDid = nullableSubjectDid.String 177 201 } 178 202 179 203 star.Created = time.Now()
+15 -5
appview/db/webhooks.go
··· 34 34 active, 35 35 events, 36 36 created_at, 37 - updated_at 37 + updated_at, 38 + repo_did 38 39 from webhooks 39 40 %s 40 41 order by created_at desc ··· 50 51 for rows.Next() { 51 52 var wh models.Webhook 52 53 var createdAt, updatedAt, eventsStr string 53 - var secret sql.NullString 54 + var secret, whRepoDid sql.NullString 54 55 var active int 55 56 56 57 err := rows.Scan( ··· 62 63 &eventsStr, 63 64 &createdAt, 64 65 &updatedAt, 66 + &whRepoDid, 65 67 ) 66 68 if err != nil { 67 69 return nil, fmt.Errorf("failed to scan webhook: %w", err) ··· 80 82 } 81 83 if t, err := time.Parse(time.RFC3339, updatedAt); err == nil { 82 84 wh.UpdatedAt = t 85 + } 86 + if whRepoDid.Valid { 87 + wh.RepoDid = whRepoDid.String 83 88 } 84 89 85 90 webhooks = append(webhooks, wh) ··· 118 123 active = 1 119 124 } 120 125 126 + var repoDid *string 127 + if webhook.RepoDid != "" { 128 + repoDid = &webhook.RepoDid 129 + } 130 + 121 131 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) 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) 125 135 126 136 if err != nil { 127 137 return fmt.Errorf("failed to insert webhook: %w", err)
+78 -18
appview/ingester.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "database/sql" 5 6 "encoding/json" 7 + "errors" 6 8 "fmt" 7 9 "log/slog" 8 10 "maps" ··· 116 118 return err 117 119 } 118 120 119 - subjectUri, err = syntax.ParseATURI(record.Subject) 120 - if err != nil { 121 - l.Error("invalid record", "err", err) 122 - return err 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") 123 151 } 124 - err = db.AddStar(i.Db, &models.Star{ 125 - Did: did, 126 - RepoAt: subjectUri, 127 - Rkey: e.Commit.RKey, 128 - }) 152 + err = db.AddStar(i.Db, star) 129 153 case jmodels.CommitOperationDelete: 130 154 err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey) 131 155 } ··· 220 244 return err 221 245 } 222 246 223 - repoAt, err := syntax.ParseATURI(record.Repo) 224 - if err != nil { 225 - return err 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 + } 226 253 } 227 - 228 - repo, err := db.GetRepoByAtUri(i.Db, repoAt.String()) 229 - if err != nil { 230 - return err 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") 231 266 } 232 267 233 - ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 268 + ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.RepoIdentifier(), "repo:push") 234 269 if err != nil || !ok { 235 270 return err 236 271 } 237 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 + 238 283 createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 239 284 if err != nil { 240 285 createdAt = time.Now() ··· 243 288 artifact := models.Artifact{ 244 289 Did: did, 245 290 Rkey: e.Commit.RKey, 246 - RepoAt: repoAt, 291 + RepoAt: repo.RepoAt(), 292 + RepoDid: repoDid, 247 293 Tag: plumbing.Hash(record.Tag), 248 294 CreatedAt: createdAt, 249 295 BlobCid: cid.Cid(record.Artifact.Ref), ··· 822 868 823 869 issue := models.IssueFromRecord(did, rkey, record) 824 870 871 + if issue.RepoDid == "" && issue.RepoAt == "" { 872 + return fmt.Errorf("issue record has neither repo nor repoDid") 873 + } 874 + 825 875 if err := i.Validator.ValidateIssue(&issue); err != nil { 826 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 + } 827 887 } 828 888 829 889 tx, err := ddb.BeginTx(ctx, nil)
+3 -2
appview/issues/issues.go
··· 306 306 return 307 307 } 308 308 309 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 309 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 310 310 isRepoOwner := roles.IsOwner() 311 311 isCollaborator := roles.IsCollaborator() 312 312 isIssueOwner := user.Active.Did == issue.Did ··· 354 354 return 355 355 } 356 356 357 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 357 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 358 358 isRepoOwner := roles.IsOwner() 359 359 isCollaborator := roles.IsCollaborator() 360 360 isIssueOwner := user.Active.Did == issue.Did ··· 1011 1011 1012 1012 issue := &models.Issue{ 1013 1013 RepoAt: f.RepoAt(), 1014 + RepoDid: f.RepoDid, 1014 1015 Rkey: tid.TID(), 1015 1016 Title: r.FormValue("title"), 1016 1017 Body: body,
+29 -5
appview/middleware/middleware.go
··· 17 17 "tangled.org/core/appview/pages" 18 18 "tangled.org/core/appview/pagination" 19 19 "tangled.org/core/appview/reporesolver" 20 + "tangled.org/core/appview/state/userutil" 20 21 "tangled.org/core/idresolver" 21 22 "tangled.org/core/orm" 22 23 "tangled.org/core/rbac" ··· 161 162 return 162 163 } 163 164 164 - ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 165 + ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.RepoIdentifier(), requiredPerm) 165 166 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 + log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.RepoIdentifier()) 167 168 http.Error(w, "Forbiden", http.StatusUnauthorized) 168 169 return 169 170 } ··· 188 189 189 190 id, err := mw.idResolver.ResolveIdent(req.Context(), didOrHandle) 190 191 if err != nil { 191 - // invalid did or handle 192 192 log.Printf("failed to resolve did/handle '%s': %s\n", didOrHandle, err) 193 193 mw.pages.Error404(w) 194 194 return ··· 226 226 return 227 227 } 228 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 + 229 249 ctx := context.WithValue(req.Context(), "repo", repo) 230 250 next.ServeHTTP(w, req.WithContext(ctx)) 231 251 }) ··· 334 354 335 355 if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 336 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 + } 337 361 html := fmt.Sprintf( 338 362 `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/> 339 363 <meta name="go-import" content="tangled.org/%s git https://tangled.org/%s"/>`, 340 - fullName, fullName, 341 - fullName, fullName, 364 + modulePath, fullName, 365 + modulePath, fullName, 342 366 ) 343 367 w.Header().Set("Content-Type", "text/html") 344 368 w.Write([]byte(html))
+1
appview/models/artifact.go
··· 16 16 Rkey string 17 17 18 18 RepoAt syntax.ATURI 19 + RepoDid string 19 20 Tag plumbing.Hash 20 21 CreatedAt time.Time 21 22
+1
appview/models/collaborator.go
··· 15 15 // content 16 16 SubjectDid syntax.DID 17 17 RepoAt syntax.ATURI 18 + RepoDid string 18 19 19 20 // meta 20 21 Created time.Time
+23 -2
appview/models/issue.go
··· 14 14 Did string 15 15 Rkey string 16 16 RepoAt syntax.ATURI 17 + RepoDid string 17 18 IssueId int 18 19 Created time.Time 19 20 Edited *time.Time ··· 44 45 for i, uri := range i.References { 45 46 references[i] = string(uri) 46 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 + } 47 56 return tangled.RepoIssue{ 48 - Repo: i.RepoAt.String(), 57 + Repo: repo, 58 + RepoDid: repoDid, 49 59 Title: i.Title, 50 60 Body: &i.Body, 51 61 Mentions: mentions, ··· 161 171 body = *record.Body 162 172 } 163 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 + 164 184 return Issue{ 165 - RepoAt: syntax.ATURI(record.Repo), 185 + RepoAt: repoAt, 186 + RepoDid: repoDid, 166 187 Did: did, 167 188 Rkey: rkey, 168 189 Created: created,
+1
appview/models/language.go
··· 7 7 type RepoLanguage struct { 8 8 Id int64 9 9 RepoAt syntax.ATURI 10 + RepoDid string 10 11 Ref string 11 12 IsDefaultRef bool 12 13 Language string
+1
appview/models/pipeline.go
··· 19 19 Knot string 20 20 RepoOwner syntax.DID 21 21 RepoName string 22 + RepoDid string 22 23 TriggerId int 23 24 Sha string 24 25 Created time.Time
+19 -5
appview/models/pull.go
··· 59 59 RepoAt syntax.ATURI 60 60 OwnerDid string 61 61 Rkey string 62 + RepoDid string 62 63 63 64 // content 64 65 Title string ··· 90 91 source = &tangled.RepoPull_Source{} 91 92 source.Branch = p.PullSource.Branch 92 93 source.Sha = p.LatestSha() 93 - if p.PullSource.RepoAt != nil { 94 + if p.PullSource.RepoDid != "" { 95 + source.RepoDid = &p.PullSource.RepoDid 96 + } else if p.PullSource.RepoAt != nil { 94 97 s := p.PullSource.RepoAt.String() 95 98 source.Repo = &s 96 99 } ··· 104 107 references[i] = string(uri) 105 108 } 106 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 + } 107 118 record := tangled.RepoPull{ 108 119 Title: p.Title, 109 120 Body: &p.Body, ··· 111 122 References: references, 112 123 CreatedAt: p.Created.Format(time.RFC3339), 113 124 Target: &tangled.RepoPull_Target{ 114 - Repo: p.RepoAt.String(), 115 - Branch: p.TargetBranch, 125 + Repo: targetRepo, 126 + RepoDid: targetRepoDid, 127 + Branch: p.TargetBranch, 116 128 }, 117 129 Source: source, 118 130 } ··· 120 132 } 121 133 122 134 type PullSource struct { 123 - Branch string 124 - RepoAt *syntax.ATURI 135 + Branch string 136 + RepoAt *syntax.ATURI 137 + RepoDid string 125 138 126 139 // optionally populate this for reverse mappings 127 140 Repo *Repo ··· 155 168 RepoAt string 156 169 OwnerDid string 157 170 CommentAt string 171 + RepoDid string 158 172 159 173 // content 160 174 Body string
+12 -1
appview/models/repo.go
··· 22 22 Topics []string 23 23 Spindle string 24 24 Labels []string 25 + RepoDid string 25 26 26 27 // optionally, populate this when querying for reverse mappings 27 28 RepoStats *RepoStats ··· 49 50 website = &r.Website 50 51 } 51 52 53 + var repoDid *string 54 + if r.RepoDid != "" { 55 + repoDid = &r.RepoDid 56 + } 57 + 52 58 return tangled.Repo{ 53 59 Knot: r.Knot, 54 60 Name: r.Name, ··· 59 65 Source: source, 60 66 Spindle: spindle, 61 67 Labels: r.Labels, 68 + RepoDid: repoDid, 62 69 } 63 70 } 64 71 ··· 66 73 return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 67 74 } 68 75 69 - func (r Repo) DidSlashRepo() string { 76 + func (r Repo) RepoIdentifier() string { 77 + if r.RepoDid != "" { 78 + return r.RepoDid 79 + } 70 80 p, _ := securejoin.SecureJoin(r.Did, r.Name) 71 81 return p 72 82 } ··· 98 108 Id int64 99 109 RepoAt syntax.ATURI 100 110 LabelAt syntax.ATURI 111 + RepoDid string 101 112 } 102 113 103 114 type RepoGroup struct {
+5 -4
appview/models/star.go
··· 7 7 ) 8 8 9 9 type Star struct { 10 - Did string 11 - RepoAt syntax.ATURI 12 - Created time.Time 13 - Rkey string 10 + Did string 11 + RepoAt syntax.ATURI 12 + SubjectDid string 13 + Created time.Time 14 + Rkey string 14 15 } 15 16 16 17 // RepoStar is used for reverse mapping to repos
+1
appview/models/webhook.go
··· 16 16 type Webhook struct { 17 17 Id int64 18 18 RepoAt syntax.ATURI 19 + RepoDid string 19 20 Url string 20 21 Secret string 21 22 Active bool
+5 -2
appview/pages/funcmap.go
··· 78 78 return identity.Handle.String() 79 79 }, 80 80 "ownerSlashRepo": func(repo *models.Repo) string { 81 + if repo.RepoDid != "" { 82 + return repo.RepoDid 83 + } 81 84 ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did) 82 85 if err != nil { 83 - return repo.DidSlashRepo() 86 + return repo.RepoIdentifier() 84 87 } 85 88 handle := ownerId.Handle 86 89 if handle != "" && !handle.IsInvalidHandle() { 87 90 return string(handle) + "/" + repo.Name 88 91 } 89 - return repo.DidSlashRepo() 92 + return repo.RepoIdentifier() 90 93 }, 91 94 "truncateAt30": func(s string) string { 92 95 if len(s) <= 30 {
+7
appview/pages/repoinfo/repoinfo.go
··· 20 20 } 21 21 22 22 func (r RepoInfo) FullName() string { 23 + if r.RepoDid != "" { 24 + return r.RepoDid 25 + } 23 26 return path.Join(r.owner(), r.Name) 24 27 } 25 28 ··· 32 35 } 33 36 34 37 func (r RepoInfo) FullNameWithoutAt() string { 38 + if r.RepoDid != "" { 39 + return userutil.FlattenDid(r.RepoDid) 40 + } 35 41 return path.Join(r.ownerWithoutAt(), r.Name) 36 42 } 37 43 ··· 59 65 Rkey string 60 66 OwnerDid string 61 67 OwnerHandle string 68 + RepoDid string 62 69 Description string 63 70 Website string 64 71 Topics []string
+21
appview/pages/templates/repo/fork.html
··· 37 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 38 </fieldset> 39 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 + 40 61 <div class="space-y-2"> 41 62 <button type="submit" class="btn-create flex items-center gap-2"> 42 63 {{ i "git-fork" "w-4 h-4" }}
+26
appview/pages/templates/repo/new.html
··· 70 70 <div class="space-y-2"> 71 71 {{ template "defaultBranch" . }} 72 72 {{ template "knot" . }} 73 + {{ template "repoDid" . }} 73 74 </div> 74 75 </div> 75 76 </div> ··· 168 169 A knot hosts repository data and handles Git operations. 169 170 You can also <a href="/settings/knots" class="underline">register your own knot</a>. 170 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> 171 197 </div> 172 198 {{ end }} 173 199
+44 -45
appview/pulls/pulls.go
··· 406 406 } 407 407 408 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()) 409 + perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.RepoIdentifier()) 410 410 if !slices.Contains(perms, "repo:push") { 411 411 return nil 412 412 } ··· 420 420 Host: host, 421 421 } 422 422 423 - resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, fmt.Sprintf("%s/%s", repo.Did, repo.Name)) 423 + resp, err := tangled.RepoBranch(r.Context(), xrpcc, branch, repo.RepoIdentifier()) 424 424 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 425 425 return nil 426 426 } ··· 436 436 return pages.Unknown 437 437 } 438 438 439 - var knot, ownerDid, repoName string 440 - 439 + var sourceRepo *models.Repo 441 440 if pull.PullSource.RepoAt != nil { 442 - // fork-based pulls 443 - sourceRepo, err := db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 441 + var err error 442 + sourceRepo, err = db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 444 443 if err != nil { 445 444 log.Println("failed to get source repo", err) 446 445 return pages.Unknown 447 446 } 448 - 449 - knot = sourceRepo.Knot 450 - ownerDid = sourceRepo.Did 451 - repoName = sourceRepo.Name 452 447 } else { 453 - // pulls within the same repo 454 - knot = repo.Knot 455 - ownerDid = repo.Did 456 - repoName = repo.Name 448 + sourceRepo = repo 457 449 } 458 450 459 451 scheme := "http" 460 452 if !s.config.Core.Dev { 461 453 scheme = "https" 462 454 } 463 - host := fmt.Sprintf("%s://%s", scheme, knot) 455 + host := fmt.Sprintf("%s://%s", scheme, sourceRepo.Knot) 464 456 xrpcc := &indigoxrpc.Client{ 465 457 Host: host, 466 458 } 467 459 468 - didSlashName := fmt.Sprintf("%s/%s", ownerDid, repoName) 469 - branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, didSlashName) 460 + branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, sourceRepo.RepoIdentifier()) 470 461 if err != nil { 471 462 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 472 463 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 863 854 comment := &models.PullComment{ 864 855 OwnerDid: user.Active.Did, 865 856 RepoAt: f.RepoAt().String(), 857 + RepoDid: f.RepoDid, 866 858 PullId: pull.PullId, 867 859 Body: body, 868 860 CommentAt: atResp.Uri, ··· 913 905 Host: host, 914 906 } 915 907 916 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 917 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 908 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 918 909 if err != nil { 919 910 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 920 911 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 963 954 } 964 955 965 956 // Determine PR type based on input parameters 966 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 957 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 967 958 isPushAllowed := roles.IsPushAllowed() 968 959 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 969 960 isForkBased := fromFork != "" && sourceBranch != "" ··· 1079 1070 Host: host, 1080 1071 } 1081 1072 1082 - didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name) 1083 - xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, didSlashRepo, targetBranch, sourceBranch) 1073 + xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo.RepoIdentifier(), targetBranch, sourceBranch) 1084 1074 if err != nil { 1085 1075 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1086 1076 log.Println("failed to call XRPC repo.compare", xrpcerr) ··· 1189 1179 Host: forkHost, 1190 1180 } 1191 1181 1192 - forkRepoId := fmt.Sprintf("%s/%s", fork.Did, fork.Name) 1193 - forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, forkRepoId, hiddenRef, sourceBranch) 1182 + forkXrpcBytes, err := tangled.RepoCompare(r.Context(), forkXrpcc, fork.RepoIdentifier(), hiddenRef, sourceBranch) 1194 1183 if err != nil { 1195 1184 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1196 1185 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) ··· 1223 1212 forkAtUriStr := forkAtUri.String() 1224 1213 1225 1214 pullSource := &models.PullSource{ 1226 - Branch: sourceBranch, 1227 - RepoAt: &forkAtUri, 1215 + Branch: sourceBranch, 1216 + RepoAt: &forkAtUri, 1217 + RepoDid: fork.RepoDid, 1228 1218 } 1229 1219 recordPullSource := &tangled.RepoPull_Source{ 1230 1220 Branch: sourceBranch, 1231 - Repo: &forkAtUriStr, 1232 1221 Sha: sourceRev, 1233 1222 } 1223 + if fork.RepoDid != "" { 1224 + recordPullSource.RepoDid = &fork.RepoDid 1225 + } else { 1226 + recordPullSource.Repo = &forkAtUriStr 1227 + } 1234 1228 1235 1229 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1236 1230 } ··· 1313 1307 TargetBranch: targetBranch, 1314 1308 OwnerDid: user.Active.Did, 1315 1309 RepoAt: repo.RepoAt(), 1310 + RepoDid: repo.RepoDid, 1316 1311 Rkey: rkey, 1317 1312 Mentions: mentions, 1318 1313 References: references, ··· 1347 1342 Rkey: rkey, 1348 1343 Record: &lexutil.LexiconTypeDecoder{ 1349 1344 Val: &tangled.RepoPull{ 1350 - Title: title, 1351 - Target: &tangled.RepoPull_Target{ 1352 - Repo: string(repo.RepoAt()), 1353 - Branch: targetBranch, 1354 - }, 1345 + Title: title, 1346 + Target: repoPullTarget(repo, targetBranch), 1355 1347 PatchBlob: blob.Blob, 1356 1348 Source: recordPullSource, 1357 1349 CreatedAt: time.Now().Format(time.RFC3339), ··· 1544 1536 Host: host, 1545 1537 } 1546 1538 1547 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1548 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1539 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 1549 1540 if err != nil { 1550 1541 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1551 1542 log.Println("failed to call XRPC repo.branches", xrpcerr) ··· 1631 1622 Host: sourceHost, 1632 1623 } 1633 1624 1634 - sourceRepo := fmt.Sprintf("%s/%s", forkOwnerDid, repo.Name) 1635 - sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, sourceRepo) 1625 + sourceXrpcBytes, err := tangled.RepoBranches(r.Context(), sourceXrpcc, "", 0, repo.RepoIdentifier()) 1636 1626 if err != nil { 1637 1627 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1638 1628 log.Println("failed to call XRPC repo.branches for source", xrpcerr) ··· 1660 1650 Host: targetHost, 1661 1651 } 1662 1652 1663 - targetRepo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1664 - targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo) 1653 + targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, f.RepoIdentifier()) 1665 1654 if err != nil { 1666 1655 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1667 1656 log.Println("failed to call XRPC repo.branches for target", xrpcerr) ··· 1771 1760 return 1772 1761 } 1773 1762 1774 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 1763 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 1775 1764 if !roles.IsPushAllowed() { 1776 1765 log.Println("unauthorized user") 1777 1766 w.WriteHeader(http.StatusUnauthorized) ··· 1787 1776 Host: host, 1788 1777 } 1789 1778 1790 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 1791 - xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch) 1779 + xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, f.RepoIdentifier(), pull.TargetBranch, pull.PullSource.Branch) 1792 1780 if err != nil { 1793 1781 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1794 1782 log.Println("failed to call XRPC repo.compare", xrpcerr) ··· 1881 1869 forkScheme = "https" 1882 1870 } 1883 1871 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) 1872 + forkXrpcBytes, err := tangled.RepoCompare(r.Context(), &indigoxrpc.Client{Host: forkHost}, forkRepo.RepoIdentifier(), hiddenRef, pull.PullSource.Branch) 1886 1873 if err != nil { 1887 1874 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1888 1875 log.Println("failed to call XRPC repo.compare for fork", xrpcerr) ··· 2360 2347 } 2361 2348 2362 2349 // auth filter: only owner or collaborators can close 2363 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2350 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 2364 2351 isOwner := roles.IsOwner() 2365 2352 isCollaborator := roles.IsCollaborator() 2366 2353 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2434 2421 } 2435 2422 2436 2423 // auth filter: only owner or collaborators can close 2437 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2424 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.RepoIdentifier())} 2438 2425 isOwner := roles.IsOwner() 2439 2426 isCollaborator := roles.IsCollaborator() 2440 2427 isPullAuthor := user.Active.Did == pull.OwnerDid ··· 2528 2515 TargetBranch: targetBranch, 2529 2516 OwnerDid: user.Active.Did, 2530 2517 RepoAt: repo.RepoAt(), 2518 + RepoDid: repo.RepoDid, 2531 2519 Rkey: rkey, 2532 2520 Mentions: mentions, 2533 2521 References: references, ··· 2559 2547 } 2560 2548 2561 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 25 scheme = "https" 26 26 } 27 27 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 28 - didSlashRepo := f.DidSlashRepo() 28 + didSlashRepo := f.RepoIdentifier() 29 29 30 30 // build the xrpc url 31 31 u, err := url.Parse(host) ··· 66 66 if link := resp.Header.Get("Link"); link != "" { 67 67 if resolvedRef, err := extractImmutableLink(link); err == nil { 68 68 newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 - rp.config.Core.BaseUrl(), f.DidSlashRepo(), resolvedRef) 69 + rp.config.Core.BaseUrl(), f.RepoIdentifier(), resolvedRef) 70 70 w.Header().Set("Link", newLink) 71 71 } 72 72 }
+19 -9
appview/repo/artifact.go
··· 80 80 Repo: user.Active.Did, 81 81 Rkey: rkey, 82 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 - }, 83 + Val: repoArtifactRecord(f, uploadBlobResp.Blob, createdAt, header.Filename, tag.Tag.Hash[:]), 90 84 }, 91 85 }) 92 86 if err != nil { ··· 109 103 Did: user.Active.Did, 110 104 Rkey: rkey, 111 105 RepoAt: f.RepoAt(), 106 + RepoDid: f.RepoDid, 112 107 Tag: tag.Tag.Hash, 113 108 CreatedAt: createdAt, 114 109 BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), ··· 322 317 Host: host, 323 318 } 324 319 325 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 326 - xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 320 + xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, f.RepoIdentifier()) 327 321 if err != nil { 328 322 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 329 323 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) ··· 358 352 359 353 return tag, nil 360 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 58 xrpcc := &indigoxrpc.Client{ 59 59 Host: host, 60 60 } 61 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 62 - resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 61 + resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, f.RepoIdentifier()) 63 62 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 64 63 l.Error("failed to call XRPC repo.blob", "err", xrpcerr) 65 64 rp.pages.Error503(w) ··· 139 138 if !rp.config.Core.Dev { 140 139 scheme = "https" 141 140 } 142 - repo := f.DidSlashRepo() 141 + repo := f.RepoIdentifier() 143 142 baseURL := &url.URL{ 144 143 Scheme: scheme, 145 144 Host: f.Knot, ··· 290 289 scheme = "https" 291 290 } 292 291 293 - repoName := fmt.Sprintf("%s/%s", repo.Did, repo.Name) 292 + repoName := repo.RepoIdentifier() 294 293 baseURL := &url.URL{ 295 294 Scheme: scheme, 296 295 Host: repo.Knot,
+1 -2
appview/repo/branches.go
··· 29 29 xrpcc := &indigoxrpc.Client{ 30 30 Host: host, 31 31 } 32 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 33 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 32 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 34 33 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 35 34 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 36 35 rp.pages.Error503(w)
+7 -7
appview/repo/compare.go
··· 36 36 Host: host, 37 37 } 38 38 39 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 40 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 39 + repoId := f.RepoIdentifier() 40 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId) 41 41 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 42 42 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 43 43 rp.pages.Error503(w) ··· 74 74 head = queryHead 75 75 } 76 76 77 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 77 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId) 78 78 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 79 79 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 80 80 rp.pages.Error503(w) ··· 149 149 Host: host, 150 150 } 151 151 152 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 152 + repoId := f.RepoIdentifier() 153 153 154 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 154 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId) 155 155 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 156 156 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 157 157 rp.pages.Error503(w) ··· 165 165 return 166 166 } 167 167 168 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 168 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId) 169 169 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 170 170 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 171 171 rp.pages.Error503(w) ··· 179 179 return 180 180 } 181 181 182 - compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 182 + compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repoId, base, head) 183 183 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 184 184 l.Error("failed to call XRPC repo.compare", "err", xrpcerr) 185 185 rp.pages.Error503(w)
+7 -8
appview/repo/index.go
··· 182 182 183 183 if err != nil || langs == nil { 184 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) 185 + ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo.RepoIdentifier()) 187 186 if err != nil { 188 187 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 189 188 l.Error("failed to call XRPC repo.languages", "err", xrpcerr) ··· 199 198 for _, lang := range ls.Languages { 200 199 langs = append(langs, models.RepoLanguage{ 201 200 RepoAt: repo.RepoAt(), 201 + RepoDid: repo.RepoDid, 202 202 Ref: currentRef, 203 203 IsDefaultRef: isDefaultRef, 204 204 Language: lang.Name, ··· 259 259 260 260 // buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 261 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) 262 + repoId := repo.RepoIdentifier() 263 263 264 - // first get branches to determine the ref if not specified 265 - branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, didSlashRepo) 264 + branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repoId) 266 265 if err != nil { 267 266 return nil, fmt.Errorf("failed to call repoBranches: %w", err) 268 267 } ··· 304 303 305 304 // tags 306 305 wg.Go(func() { 307 - tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, didSlashRepo) 306 + tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repoId) 308 307 if err != nil { 309 308 errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err)) 310 309 return ··· 317 316 318 317 // tree/files 319 318 wg.Go(func() { 320 - resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, didSlashRepo) 319 + resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repoId) 321 320 if err != nil { 322 321 errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err)) 323 322 return ··· 327 326 328 327 // commits 329 328 wg.Go(func() { 330 - logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, didSlashRepo) 329 + logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repoId) 331 330 if err != nil { 332 331 errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err)) 333 332 return
+5 -6
appview/repo/log.go
··· 57 57 cursor = strconv.Itoa(offset) 58 58 } 59 59 60 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 61 - xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 60 + xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, f.RepoIdentifier()) 62 61 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 63 62 l.Error("failed to call XRPC repo.log", "err", xrpcerr) 64 63 rp.pages.Error503(w) ··· 72 71 return 73 72 } 74 73 75 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 74 + repoId := f.RepoIdentifier() 75 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repoId) 76 76 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 77 77 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 78 78 rp.pages.Error503(w) ··· 93 93 } 94 94 } 95 95 96 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 96 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repoId) 97 97 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 98 98 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 99 99 rp.pages.Error503(w) ··· 172 172 Host: host, 173 173 } 174 174 175 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 176 - xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 175 + xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, f.RepoIdentifier()) 177 176 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 178 177 l.Error("failed to call XRPC repo.diff", "err", xrpcerr) 179 178 rp.pages.Error503(w)
+128 -55
appview/repo/repo.go
··· 33 33 atpclient "github.com/bluesky-social/indigo/atproto/client" 34 34 "github.com/bluesky-social/indigo/atproto/syntax" 35 35 lexutil "github.com/bluesky-social/indigo/lex/util" 36 - securejoin "github.com/cyphar/filepath-securejoin" 36 + 37 37 "github.com/go-chi/chi/v5" 38 38 ) 39 39 ··· 309 309 return 310 310 } 311 311 312 - err = db.SubscribeLabel(tx, &models.RepoLabel{ 312 + if err = db.SubscribeLabel(tx, &models.RepoLabel{ 313 313 RepoAt: f.RepoAt(), 314 314 LabelAt: label.AtUri(), 315 - }) 315 + RepoDid: f.RepoDid, 316 + }); err != nil { 317 + fail("Failed to subscribe to label.", err) 318 + return 319 + } 316 320 317 321 err = tx.Commit() 318 322 if err != nil { ··· 504 508 err = db.SubscribeLabel(tx, &models.RepoLabel{ 505 509 RepoAt: f.RepoAt(), 506 510 LabelAt: syntax.ATURI(l), 511 + RepoDid: f.RepoDid, 507 512 }) 508 513 if err != nil { 509 514 fail("Failed to subscribe to label.", err) ··· 746 751 Repo: currentUser.Active.Did, 747 752 Rkey: rkey, 748 753 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 + Val: repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt), 755 + }, 754 756 }) 755 757 // invalid record 756 758 if err != nil { ··· 785 787 } 786 788 defer rollback() 787 789 788 - err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 790 + err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.RepoIdentifier()) 789 791 if err != nil { 790 792 fail("Failed to add collaborator permissions.", err) 791 793 return ··· 796 798 Rkey: rkey, 797 799 SubjectDid: collaboratorIdent.DID, 798 800 RepoAt: f.RepoAt(), 801 + RepoDid: f.RepoDid, 799 802 Created: createdAt, 800 803 }) 801 804 if err != nil { ··· 891 894 }() 892 895 893 896 // remove collaborator RBAC 894 - repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 897 + repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.RepoIdentifier(), f.Knot) 895 898 if err != nil { 896 899 rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 897 900 return 898 901 } 899 902 for _, c := range repoCollaborators { 900 903 did := c[0] 901 - rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 904 + rp.enforcer.RemoveCollaborator(did, f.Knot, f.RepoIdentifier()) 902 905 } 903 906 l.Info("removed collaborators") 904 907 905 908 // remove repo RBAC 906 - err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.DidSlashRepo()) 909 + err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.RepoIdentifier()) 907 910 if err != nil { 908 911 rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 909 912 return ··· 1058 1061 uri = "http" 1059 1062 } 1060 1063 1061 - forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Name) 1064 + forkSourceUrl := fmt.Sprintf("%s://%s/%s", uri, f.Knot, f.RepoIdentifier()) 1062 1065 l = l.With("cloneUrl", forkSourceUrl) 1063 1066 1064 - sourceAt := f.RepoAt().String() 1067 + rkey := tid.TID() 1065 1068 1066 - // create an atproto record for this fork 1067 - rkey := tid.TID() 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 + 1068 1117 repo := &models.Repo{ 1069 1118 Did: user.Active.Did, 1070 1119 Name: forkName, 1071 1120 Knot: targetKnot, 1072 1121 Rkey: rkey, 1073 - Source: sourceAt, 1122 + Source: forkSource, 1074 1123 Description: f.Description, 1075 1124 Created: time.Now(), 1076 1125 Labels: rp.config.Label.DefaultLabelDefs, 1126 + RepoDid: repoDid, 1077 1127 } 1078 1128 record := repo.AsRecord() 1079 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 + 1080 1164 atpClient, err := rp.oauth.AuthorizedClient(r) 1081 1165 if err != nil { 1082 1166 l.Error("failed to create xrpcclient", "err", err) 1167 + cleanupKnot() 1083 1168 rp.pages.Notice(w, "repo", "Failed to fork repository.") 1084 1169 return 1085 1170 } ··· 1094 1179 }) 1095 1180 if err != nil { 1096 1181 l.Error("failed to write to PDS", "err", err) 1182 + cleanupKnot() 1097 1183 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1098 1184 return 1099 1185 } ··· 1109 1195 return 1110 1196 } 1111 1197 1112 - // The rollback function reverts a few things on failure: 1113 - // - the pending txn 1114 - // - the ACLs 1115 - // - the atproto record created 1116 1198 rollback := func() { 1117 1199 err1 := tx.Rollback() 1118 1200 err2 := rp.enforcer.E.LoadPolicy() 1119 1201 err3 := rollbackRecord(context.Background(), aturi, atpClient) 1120 1202 1121 - // ignore txn complete errors, this is okay 1122 1203 if errors.Is(err1, sql.ErrTxDone) { 1123 1204 err1 = nil 1124 1205 } 1125 1206 1126 1207 if errs := errors.Join(err1, err2, err3); errs != nil { 1127 1208 l.Error("failed to rollback changes", "errs", errs) 1128 - return 1209 + } 1210 + 1211 + if aturi != "" { 1212 + cleanupKnot() 1129 1213 } 1130 1214 } 1131 1215 defer rollback() 1132 1216 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 1217 err = db.AddRepo(tx, repo) 1161 1218 if err != nil { 1162 1219 l.Error("failed to AddRepo", "err", err) ··· 1164 1221 return 1165 1222 } 1166 1223 1167 - // acls 1168 - p, _ := securejoin.SecureJoin(user.Active.Did, forkName) 1169 - err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p) 1224 + rbacPath := repo.RepoIdentifier() 1225 + err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, rbacPath) 1170 1226 if err != nil { 1171 1227 l.Error("failed to add ACLs", "err", err) 1172 1228 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1187 1243 return 1188 1244 } 1189 1245 1190 - // reset the ATURI because the transaction completed successfully 1191 1246 aturi = "" 1192 1247 1193 1248 rp.notifier.NewRepo(r.Context(), repo) 1194 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName)) 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 + } 1195 1254 } 1196 1255 } 1197 1256 ··· 1216 1275 }) 1217 1276 return err 1218 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 188 Host: host, 189 189 } 190 190 191 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 192 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 191 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 193 192 var result types.RepoBranchesResponse 194 193 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 195 194 l.Error("failed to call XRPC repo.branches", "err", xrpcerr) ··· 260 259 user := rp.oauth.GetMultiAccountUser(r) 261 260 262 261 collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) { 263 - repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot) 262 + repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.RepoIdentifier(), repo.Knot) 264 263 if err != nil { 265 264 return nil, err 266 265 }
+3 -5
appview/repo/tags.go
··· 35 35 xrpcc := &indigoxrpc.Client{ 36 36 Host: host, 37 37 } 38 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 39 - xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 38 + xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, f.RepoIdentifier()) 40 39 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 41 40 l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 42 41 rp.pages.Error503(w) ··· 98 97 xrpcc := &indigoxrpc.Client{ 99 98 Host: host, 100 99 } 101 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 102 100 tag := chi.URLParam(r, "tag") 103 101 104 - xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, repo, tag) 102 + xrpcBytes, err := tangled.RepoTag(r.Context(), xrpcc, f.RepoIdentifier(), tag) 105 103 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 106 104 // if we don't match an existing tag, and the tag we're trying 107 105 // to match is "latest", resolve to the most recent tag 108 106 if tag == "latest" { 109 - tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, repo) 107 + tagsBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 1, f.RepoIdentifier()) 110 108 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 111 109 l.Error("failed to call XRPC repo.tags for latest", "err", xrpcerr) 112 110 rp.pages.Error503(w)
+1 -2
appview/repo/tree.go
··· 41 41 xrpcc := &indigoxrpc.Client{ 42 42 Host: host, 43 43 } 44 - repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 45 - xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 44 + xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, f.RepoIdentifier()) 46 45 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 47 46 l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 48 47 rp.pages.Error503(w)
+6 -5
appview/repo/webhooks.go
··· 89 89 } 90 90 91 91 webhook := &models.Webhook{ 92 - RepoAt: f.RepoAt(), 93 - Url: url, 94 - Secret: secret, 95 - Active: active, 96 - Events: events, 92 + RepoAt: f.RepoAt(), 93 + RepoDid: f.RepoDid, 94 + Url: url, 95 + Secret: secret, 96 + Active: active, 97 + Events: events, 97 98 } 98 99 99 100 tx, err := rp.db.Begin()
+20 -6
appview/reporesolver/resolver.go
··· 30 30 31 31 // NOTE: this... should not even be here. the entire package will be removed in future refactor 32 32 func GetBaseRepoPath(r *http.Request, repo *models.Repo) string { 33 + if repo.RepoDid != "" { 34 + return repo.RepoDid 35 + } 33 36 var ( 34 37 user = chi.URLParam(r, "user") 35 38 name = chi.URLParam(r, "repo") 36 39 ) 37 40 if user == "" || name == "" { 38 - return repo.DidSlashRepo() 41 + return repo.RepoIdentifier() 39 42 } 40 43 return path.Join(user, name) 41 44 } ··· 71 74 roles := repoinfo.RolesInRepo{} 72 75 if user != nil && user.Active != nil { 73 76 isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt) 74 - roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 77 + roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.RepoIdentifier()) 75 78 } 76 79 77 80 stats := repo.RepoStats 78 81 if stats == nil { 79 - starCount, err := db.GetStarCount(rr.execer, repoAt) 80 - if err != 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 { 81 90 log.Println("failed to get star count for ", repoAt) 82 91 } 83 92 issueCount, err := db.GetIssueCount(rr.execer, repoAt) ··· 98 107 var sourceRepo *models.Repo 99 108 var err error 100 109 if repo.Source != "" { 101 - sourceRepo, err = db.GetRepoByAtUri(rr.execer, 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 + } 102 115 if err != nil { 103 - log.Println("failed to get repo by at uri", err) 116 + log.Println("failed to get source repo", err) 104 117 } 105 118 } 106 119 ··· 108 121 // this is basically a models.Repo 109 122 OwnerDid: ownerId.DID.String(), 110 123 OwnerHandle: ownerId.Handle.String(), 124 + RepoDid: repo.RepoDid, 111 125 Name: repo.Name, 112 126 Rkey: repo.Rkey, 113 127 Description: repo.Description,
+7 -20
appview/state/git_http.go
··· 12 12 ) 13 13 14 14 func (s *State) InfoRefs(w http.ResponseWriter, r *http.Request) { 15 - user := r.Context().Value("resolvedId").(identity.Identity) 16 15 repo := r.Context().Value("repo").(*models.Repo) 17 16 18 17 scheme := "https" ··· 20 19 scheme = "http" 21 20 } 22 21 23 - targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 22 + targetURL := fmt.Sprintf("%s://%s/%s/info/refs?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 24 23 s.proxyRequest(w, r, targetURL) 25 24 26 25 } 27 26 28 27 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 28 repo := r.Context().Value("repo").(*models.Repo) 35 29 36 30 scheme := "https" ··· 38 32 scheme = "http" 39 33 } 40 34 41 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 35 + targetURL := fmt.Sprintf("%s://%s/%s/git-upload-archive?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 42 36 s.proxyRequest(w, r, targetURL) 43 37 } 44 38 45 39 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 40 repo := r.Context().Value("repo").(*models.Repo) 52 41 53 42 scheme := "https" ··· 55 44 scheme = "http" 56 45 } 57 46 58 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 47 + targetURL := fmt.Sprintf("%s://%s/%s/git-upload-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 59 48 s.proxyRequest(w, r, targetURL) 60 49 } 61 50 62 51 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 52 repo := r.Context().Value("repo").(*models.Repo) 69 53 70 54 scheme := "https" ··· 72 56 scheme = "http" 73 57 } 74 58 75 - targetURL := fmt.Sprintf("%s://%s/%s/%s/git-receive-pack?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 59 + targetURL := fmt.Sprintf("%s://%s/%s/git-receive-pack?%s", scheme, repo.Knot, repo.RepoIdentifier(), r.URL.RawQuery) 76 60 s.proxyRequest(w, r, targetURL) 77 61 } 78 62 ··· 90 74 proxyReq.Header = r.Header 91 75 92 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 + } 93 80 proxyReq.Header.Add("x-tangled-repo-owner-handle", repoOwnerHandle) 94 81 95 82 // Execute request
+35 -34
appview/state/knotstream.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "database/sql" 5 6 "encoding/json" 6 7 "errors" 7 8 "fmt" ··· 86 87 return err 87 88 } 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 + 89 95 knownKnots, err := enforcer.GetKnotsForUser(record.CommitterDid) 90 96 if err != nil { 91 97 return err ··· 96 102 97 103 logger.Info("processing gitRefUpdate event", 98 104 "repo_did", record.RepoDid, 99 - "repo_name", record.RepoName, 100 105 "ref", record.Ref, 101 106 "old_sha", record.OldSha, 102 107 "new_sha", record.NewSha) 103 108 104 - // trigger webhook notifications first (before other ops that might fail) 105 109 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 { 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 { 115 122 notifier.Push(ctx, &repos[0], record.Ref, record.OldSha, record.NewSha, record.CommitterDid) 116 123 } 117 124 ··· 167 174 168 175 func updateRepoLanguages(d *db.DB, record tangled.GitRefUpdate) error { 169 176 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) 177 + return fmt.Errorf("empty language data for repo: %s/%s", record.OwnerDid, record.RepoName) 171 178 } 172 179 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) 180 + if record.RepoDid == "" { 181 + return fmt.Errorf("gitRefUpdate missing repoDid for language update") 181 182 } 182 - if len(repos) != 1 { 183 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 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) 184 187 } 185 - repo := repos[0] 188 + repo := *r 186 189 187 190 ref := plumbing.ReferenceName(record.Ref) 188 191 if !ref.IsBranch() { ··· 197 200 198 201 langs = append(langs, models.RepoLanguage{ 199 202 RepoAt: repo.RepoAt(), 203 + RepoDid: repo.RepoDid, 200 204 Ref: ref.Short(), 201 205 IsDefaultRef: record.Meta.IsDefaultRef, 202 206 Language: l.Lang, ··· 235 239 return fmt.Errorf("empty repo: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 236 240 } 237 241 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) 242 + if record.TriggerMetadata.Repo.RepoDid == "" { 243 + return fmt.Errorf("pipeline missing repoDid: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 247 244 } 248 - if len(repos) != 1 { 249 - return fmt.Errorf("incorrect number of repos returned: %d (expected 1)", len(repos)) 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) 250 249 } 250 + repos := []models.Repo{*repo} 251 251 if repos[0].Spindle == "" { 252 252 return fmt.Errorf("repo does not have a spindle configured yet: nsid %s, rkey %s", msg.Nsid, msg.Rkey) 253 253 } ··· 285 285 Knot: source.Key(), 286 286 RepoOwner: syntax.DID(record.TriggerMetadata.Repo.Did), 287 287 RepoName: record.TriggerMetadata.Repo.Repo, 288 + RepoDid: repos[0].RepoDid, 288 289 TriggerId: int(triggerId), 289 290 Sha: sha, 290 291 }
+28 -2
appview/state/router.go
··· 1 1 package state 2 2 3 3 import ( 4 + "context" 5 + "database/sql" 6 + "errors" 4 7 "net/http" 5 8 "strings" 6 9 7 10 "github.com/go-chi/chi/v5" 11 + "tangled.org/core/appview/db" 8 12 "tangled.org/core/appview/issues" 9 13 "tangled.org/core/appview/knots" 10 14 "tangled.org/core/appview/labels" ··· 45 49 if len(pathParts) > 0 { 46 50 firstPart := pathParts[0] 47 51 48 - // if using a DID or handle, just continue as per usual 49 - if userutil.IsDid(firstPart) || userutil.IsHandle(firstPart) { 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) { 50 76 userRouter.ServeHTTP(w, r) 51 77 return 52 78 }
+18 -5
appview/state/star.go
··· 12 12 "tangled.org/core/appview/db" 13 13 "tangled.org/core/appview/models" 14 14 "tangled.org/core/appview/pages" 15 + "tangled.org/core/orm" 15 16 "tangled.org/core/tid" 16 17 ) 17 18 ··· 40 41 case http.MethodPost: 41 42 createdAt := time.Now().Format(time.RFC3339) 42 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 + 43 57 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 58 Collection: tangled.FeedStarNSID, 45 59 Repo: currentUser.Active.Did, 46 60 Rkey: rkey, 47 - Record: &lexutil.LexiconTypeDecoder{ 48 - Val: &tangled.FeedStar{ 49 - Subject: subjectUri.String(), 50 - CreatedAt: createdAt, 51 - }}, 61 + Record: &lexutil.LexiconTypeDecoder{Val: starRecord}, 52 62 }) 53 63 if err != nil { 54 64 log.Println("failed to create atproto record", err) ··· 60 70 Did: currentUser.Active.Did, 61 71 RepoAt: subjectUri, 62 72 Rkey: rkey, 73 + } 74 + if repoHasDid { 75 + star.SubjectDid = repo.RepoDid 63 76 } 64 77 65 78 err = db.AddStar(s.db, star)
+92 -38
appview/state/state.go
··· 41 41 "github.com/bluesky-social/indigo/atproto/syntax" 42 42 lexutil "github.com/bluesky-social/indigo/lex/util" 43 43 "github.com/bluesky-social/indigo/xrpc" 44 - securejoin "github.com/cyphar/filepath-securejoin" 44 + 45 45 "github.com/go-chi/chi/v5" 46 46 "github.com/posthog/posthog-go" 47 47 ) ··· 432 432 return 433 433 } 434 434 435 - // create atproto record for this repo 436 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 + 437 479 repo := &models.Repo{ 438 480 Did: user.Active.Did, 439 481 Name: repoName, ··· 442 484 Description: description, 443 485 Created: time.Now(), 444 486 Labels: s.config.Label.DefaultLabelDefs, 487 + RepoDid: repoDid, 445 488 } 446 489 record := repo.AsRecord() 447 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 + 448 525 atpClient, err := s.oauth.AuthorizedClient(r) 449 526 if err != nil { 450 527 l.Info("PDS write failed", "err", err) 528 + cleanupKnot() 451 529 s.pages.Notice(w, "repo", "Failed to write record to PDS.") 452 530 return 453 531 } ··· 462 540 }) 463 541 if err != nil { 464 542 l.Info("PDS write failed", "err", err) 543 + cleanupKnot() 465 544 s.pages.Notice(w, "repo", "Failed to announce repository creation.") 466 545 return 467 546 } ··· 477 556 return 478 557 } 479 558 480 - // The rollback function reverts a few things on failure: 481 - // - the pending txn 482 - // - the ACLs 483 - // - the atproto record created 484 559 rollback := func() { 485 560 err1 := tx.Rollback() 486 561 err2 := s.enforcer.E.LoadPolicy() 487 562 err3 := rollbackRecord(context.Background(), aturi, atpClient) 488 563 489 - // ignore txn complete errors, this is okay 490 564 if errors.Is(err1, sql.ErrTxDone) { 491 565 err1 = nil 492 566 } 493 567 494 568 if errs := errors.Join(err1, err2, err3); errs != nil { 495 569 l.Error("failed to rollback changes", "errs", errs) 496 - return 497 570 } 498 - } 499 - defer rollback() 500 571 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 572 + if aturi != "" { 573 + cleanupKnot() 574 + } 524 575 } 576 + defer rollback() 525 577 526 578 err = db.AddRepo(tx, repo) 527 579 if err != nil { ··· 530 582 return 531 583 } 532 584 533 - // acls 534 - p, _ := securejoin.SecureJoin(user.Active.Did, repoName) 535 - err = s.enforcer.AddRepo(user.Active.Did, domain, p) 585 + rbacPath := repo.RepoIdentifier() 586 + err = s.enforcer.AddRepo(user.Active.Did, domain, rbacPath) 536 587 if err != nil { 537 588 l.Error("acl setup failed", "err", err) 538 589 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 553 604 return 554 605 } 555 606 556 - // reset the ATURI because the transaction completed successfully 557 607 aturi = "" 558 608 559 609 s.notifier.NewRepo(r.Context(), repo) 560 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 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 + } 561 615 } 562 616 } 563 617
+1 -1
appview/validator/label.go
··· 109 109 // validate permissions: only collaborators can apply labels currently 110 110 // 111 111 // TODO: introduce a repo:triage permission 112 - ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.DidSlashRepo()) 112 + ok, err := v.enforcer.IsPushAllowed(labelOp.Did, repo.Knot, repo.RepoIdentifier()) 113 113 if err != nil { 114 114 return fmt.Errorf("failed to enforce permissions: %w", err) 115 115 }
+34 -32
go.mod
··· 18 18 github.com/cloudflare/cloudflare-go v0.115.0 19 19 github.com/cyphar/filepath-securejoin v0.4.1 20 20 github.com/dgraph-io/ristretto v0.2.0 21 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038 21 22 github.com/docker/docker v28.2.2+incompatible 22 23 github.com/dustin/go-humanize v1.0.1 23 24 github.com/gliderlabs/ssh v0.3.8 ··· 31 32 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 32 33 github.com/hiddeco/sshsig v0.2.0 33 34 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 35 + github.com/ipfs/go-cid v0.6.0 36 + github.com/mattn/go-sqlite3 v1.14.34 36 37 github.com/microcosm-cc/bluemonday v1.0.27 37 38 github.com/openbao/openbao/api/v2 v2.3.0 38 39 github.com/posthog/posthog-go v1.5.5 ··· 41 42 github.com/sethvargo/go-envconfig v1.1.0 42 43 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c 43 44 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 45 + github.com/stretchr/testify v1.11.1 46 + github.com/urfave/cli/v3 v3.6.2 46 47 github.com/whyrusleeping/cbor-gen v0.3.1 47 48 github.com/yuin/goldmark v1.7.13 48 49 github.com/yuin/goldmark-emoji v1.0.6 49 50 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 50 51 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 51 52 go.abhg.dev/goldmark/mermaid v0.6.0 52 - golang.org/x/crypto v0.40.0 53 + golang.org/x/crypto v0.48.0 53 54 golang.org/x/image v0.31.0 54 - golang.org/x/net v0.42.0 55 + golang.org/x/net v0.50.0 55 56 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 56 57 gopkg.in/yaml.v3 v3.0.1 57 58 ) ··· 116 117 github.com/go-test/deep v1.1.1 // indirect 117 118 github.com/goccy/go-json v0.10.5 // indirect 118 119 github.com/gogo/protobuf v1.3.2 // indirect 119 - github.com/golang-jwt/jwt/v5 v5.2.3 // indirect 120 + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect 120 121 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 121 122 github.com/golang/mock v1.6.0 // indirect 122 123 github.com/golang/protobuf v1.5.4 // indirect ··· 136 137 github.com/hashicorp/hcl v1.0.1-vault-7 // indirect 137 138 github.com/hexops/gotextdiff v1.0.3 // indirect 138 139 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 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 142 143 github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 143 144 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 144 145 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-ipld-format v0.6.3 // indirect 146 147 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-log/v2 v2.9.1 // indirect 148 149 github.com/ipfs/go-metrics-interface v0.3.0 // indirect 149 150 github.com/json-iterator/go v1.1.12 // indirect 150 151 github.com/kevinburke/ssh_config v1.2.0 // indirect ··· 168 169 github.com/multiformats/go-base36 v0.2.0 // indirect 169 170 github.com/multiformats/go-multibase v0.2.0 // indirect 170 171 github.com/multiformats/go-multihash v0.2.3 // indirect 171 - github.com/multiformats/go-varint v0.0.7 // indirect 172 + github.com/multiformats/go-varint v0.1.0 // indirect 172 173 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 173 174 github.com/onsi/gomega v1.37.0 // indirect 174 175 github.com/opencontainers/go-digest v1.0.0 // indirect ··· 178 179 github.com/pkg/errors v0.9.1 // indirect 179 180 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 180 181 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_golang v1.23.2 // indirect 182 183 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 184 + github.com/prometheus/common v0.67.5 // indirect 185 + github.com/prometheus/procfs v0.19.2 // indirect 185 186 github.com/rivo/uniseg v0.4.7 // indirect 186 187 github.com/ryanuber/go-glob v1.0.0 // indirect 187 188 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 193 194 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 194 195 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 195 196 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 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 203 204 go.uber.org/atomic v1.11.0 // indirect 204 205 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 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 210 212 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 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 215 217 gopkg.in/fsnotify.v1 v1.4.7 // indirect 216 218 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 217 219 gopkg.in/warnings.v0 v0.1.2 // indirect
+80 -74
go.sum
··· 89 89 github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= 90 90 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 91 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= 92 94 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 93 95 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 94 96 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= ··· 138 140 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 139 141 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 140 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= 141 145 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 142 146 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 143 147 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= ··· 209 213 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 210 214 github.com/goki/freetype v1.0.5 h1:yi2lQeUhXnBgSMqYd0vVmPw6RnnfIeTP3N4uvaJXd7A= 211 215 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= 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= 214 218 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 215 219 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 216 220 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= ··· 260 264 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 261 265 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 262 266 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= 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= 265 269 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 266 270 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 267 271 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= ··· 294 298 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 295 299 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 296 300 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= 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= 305 309 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 306 310 github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 307 311 github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= ··· 312 316 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 313 317 github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= 314 318 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= 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= 317 321 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 318 322 github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 319 323 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= 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= 322 326 github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 323 327 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 324 328 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= ··· 350 354 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 351 355 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 352 356 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= 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= 355 359 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 356 360 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 357 361 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= ··· 387 391 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 388 392 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 389 393 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= 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= 392 396 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 393 397 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 394 398 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= ··· 443 447 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 444 448 github.com/posthog/posthog-go v1.5.5 h1:2o3j7IrHbTIfxRtj4MPaXKeimuTYg49onNzNBZbwksM= 445 449 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= 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= 448 452 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 449 453 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/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= 454 458 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 455 459 github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 456 460 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= ··· 494 498 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 495 499 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 496 500 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= 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= 499 503 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= 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= 502 506 github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 503 507 github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 504 508 github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= ··· 534 538 go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I= 535 539 go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 536 540 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= 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= 557 561 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 558 562 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 559 563 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 566 570 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 567 571 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 568 572 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= 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= 571 577 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 572 578 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 573 579 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= ··· 575 581 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 576 582 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 577 583 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= 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= 582 588 golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= 583 589 golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= 584 590 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 611 617 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 612 618 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 613 619 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= 620 + golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= 621 + golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= 616 622 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 617 623 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 618 624 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 620 626 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 621 627 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 622 628 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= 629 + golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 630 + golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 625 631 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 626 632 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 627 633 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 652 658 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 653 659 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 654 660 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= 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= 657 663 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 658 664 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 659 665 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= ··· 663 669 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 664 670 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 665 671 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= 672 + golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= 673 + golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= 668 674 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 669 675 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 670 676 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ··· 675 681 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 676 682 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 677 683 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= 684 + golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= 685 + golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 680 686 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 681 687 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 682 688 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 702 708 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 703 709 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 704 710 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/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= 711 717 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 712 718 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 713 719 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= ··· 717 723 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 718 724 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 719 725 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= 726 + google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 727 + google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 722 728 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 723 729 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 724 730 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+172
knotserver/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 + "fmt" 6 7 "log/slog" 8 + "os" 7 9 "strings" 8 10 11 + securejoin "github.com/cyphar/filepath-securejoin" 9 12 _ "github.com/mattn/go-sqlite3" 10 13 "tangled.org/core/log" 11 14 ) ··· 65 68 primary key (rkey, nsid) 66 69 ); 67 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 + 68 77 create table if not exists migrations ( 69 78 id integer primary key autoincrement, 70 79 name text unique ··· 74 83 return nil, err 75 84 } 76 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 + 77 159 return &DB{ 78 160 db: db, 79 161 logger: logger, 80 162 }, nil 81 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 5 "fmt" 6 6 "io" 7 7 "net/http" 8 - "path/filepath" 9 8 "strings" 10 9 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 10 "github.com/go-chi/chi/v5" 13 11 "tangled.org/core/knotserver/git/service" 14 12 ) 15 13 16 - func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 14 + func (h *Knot) resolveRepoPath(r *http.Request) (string, string, error) { 17 15 did := chi.URLParam(r, "did") 18 16 name := chi.URLParam(r, "name") 19 - repoName, err := securejoin.SecureJoin(did, 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) 20 31 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 32 + return "", "", fmt.Errorf("repo not found: %w", err) 24 33 } 34 + return repoPath, name, nil 35 + } 25 36 26 - repoPath, err := securejoin.SecureJoin(h.c.Repo.ScanPath, repoName) 37 + func (h *Knot) InfoRefs(w http.ResponseWriter, r *http.Request) { 38 + repoPath, name, err := h.resolveRepoPath(r) 27 39 if err != nil { 28 40 gitError(w, "repository not found", http.StatusNotFound) 29 - h.l.Error("git: failed to secure join repo path", "handler", "InfoRefs", "error", err) 41 + h.l.Error("git: failed to resolve repo path", "handler", "InfoRefs", "error", err) 30 42 return 31 43 } 32 44 ··· 57 69 } 58 70 59 71 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)) 72 + repo, _, err := h.resolveRepoPath(r) 63 73 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) 74 + gitError(w, "repository not found", http.StatusNotFound) 75 + h.l.Error("git: failed to resolve repo path", "handler", "UploadArchive", "error", err) 66 76 return 67 77 } 68 78 ··· 104 114 } 105 115 106 116 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)) 117 + repo, _, err := h.resolveRepoPath(r) 110 118 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) 119 + gitError(w, "repository not found", http.StatusNotFound) 120 + h.l.Error("git: failed to resolve repo path", "handler", "UploadPack", "error", err) 113 121 return 114 122 } 115 123 ··· 153 161 } 154 162 155 163 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)) 164 + _, name, err := h.resolveRepoPath(r) 159 165 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) 166 + gitError(w, "repository not found", http.StatusNotFound) 167 + h.l.Error("git: failed to resolve repo path", "handler", "ReceivePack", "error", err) 162 168 return 163 169 } 164 170
+95 -57
knotserver/ingester.go
··· 14 14 "github.com/bluesky-social/indigo/atproto/syntax" 15 15 "github.com/bluesky-social/indigo/xrpc" 16 16 "github.com/bluesky-social/jetstream/pkg/models" 17 - securejoin "github.com/cyphar/filepath-securejoin" 18 17 "tangled.org/core/api/tangled" 19 18 "tangled.org/core/knotserver/db" 20 19 "tangled.org/core/knotserver/git" ··· 102 101 return fmt.Errorf("ignoring pull record: target repo is nil") 103 102 } 104 103 105 - l = l.With("target_repo", record.Target.Repo) 104 + l = l.With("target_repo", record.Target.Repo, "target_repo_did", record.Target.RepoDid) 106 105 l = l.With("target_branch", record.Target.Branch) 107 106 108 107 if record.Source == nil { 109 108 return fmt.Errorf("ignoring pull record: not a branch-based pull request") 110 109 } 111 110 112 - if record.Source.Repo != nil { 111 + if record.Source.Repo != nil || record.Source.RepoDid != nil { 113 112 return fmt.Errorf("ignoring pull record: fork based pull") 114 113 } 115 114 116 - repoAt, err := syntax.ParseATURI(record.Target.Repo) 117 - if err != nil { 118 - return fmt.Errorf("failed to parse ATURI: %w", err) 119 - } 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 + } 120 124 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 - } 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 + } 126 145 127 - xrpcc := xrpc.Client{ 128 - Host: ident.PDSEndpoint(), 129 - } 146 + repo := resp.Value.Val.(*tangled.Repo) 130 147 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 - } 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 + } 135 151 136 - repo := resp.Value.Val.(*tangled.Repo) 152 + ownerDid = ident.DID.String() 153 + repoName = repo.Name 137 154 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 - } 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 + } 141 159 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 - } 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 + } 146 165 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) 166 + default: 167 + return fmt.Errorf("ignoring pull record: target has neither repo nor repoDid") 150 168 } 151 169 152 170 gr, err := git.Open(repoPath, record.Source.Sha) ··· 189 207 Kind: string(workflow.TriggerKindPullRequest), 190 208 PullRequest: &trigger, 191 209 Repo: &tangled.Pipeline_TriggerRepo{ 192 - Did: ident.DID.String(), 193 - Knot: repo.Knot, 194 - Repo: repo.Name, 210 + Did: ownerDid, 211 + Knot: h.c.Server.Hostname, 212 + Repo: repoName, 213 + RepoDid: repoDid, 195 214 }, 196 215 }, 197 216 } ··· 226 245 return fmt.Errorf("failed to unmarshal record: %w", err) 227 246 } 228 247 229 - repoAt, err := syntax.ParseATURI(record.Repo) 230 - if err != nil { 231 - return err 232 - } 233 - 234 248 subjectId, err := h.resolver.ResolveIdent(ctx, record.Subject) 235 249 if err != nil || subjectId.Handle.IsInvalidHandle() { 236 250 return err 237 251 } 238 252 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 - } 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 245 264 246 - xrpcc := xrpc.Client{ 247 - Host: owner.PDSEndpoint(), 248 - } 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 + } 249 271 250 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 251 - if err != nil { 252 - return err 253 - } 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 + } 254 276 255 - repo := resp.Value.Val.(*tangled.Repo) 256 - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 277 + xrpcc := xrpc.Client{ 278 + Host: owner.PDSEndpoint(), 279 + } 257 280 258 - // check perms for this user 259 - ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo) 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) 260 298 if err != nil { 261 299 return fmt.Errorf("failed to check permissions: %w", err) 262 300 } 263 301 if !ok { 264 - return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", didSlashRepo) 302 + return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", rbacResource) 265 303 } 266 304 267 305 if err := h.db.AddDid(subjectId.DID.String()); err != nil { ··· 269 307 } 270 308 h.jc.AddDid(subjectId.DID.String()) 271 309 272 - if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, didSlashRepo); err != nil { 310 + if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, rbacResource); err != nil { 273 311 return err 274 312 } 275 313
+110 -74
knotserver/internal.go
··· 3 3 import ( 4 4 "context" 5 5 "encoding/json" 6 - "errors" 7 6 "fmt" 8 7 "log/slog" 9 8 "net/http" 10 9 "path/filepath" 11 10 "strings" 12 11 13 - securejoin "github.com/cyphar/filepath-securejoin" 14 12 "github.com/go-chi/chi/v5" 15 13 "github.com/go-chi/chi/v5/middleware" 16 14 "github.com/go-git/go-git/v5/plumbing" ··· 72 70 // the body will be qualified repository path on success/push-denied 73 71 // or an error message when process failed 74 72 func (h *InternalHandle) Guard(w http.ResponseWriter, r *http.Request) { 75 - l := h.l.With("handler", "PostReceiveHook") 73 + l := h.l.With("handler", "Guard") 76 74 77 75 var ( 78 76 incomingUser = r.URL.Query().Get("user") ··· 87 85 return 88 86 } 89 87 90 - // did:foo/repo-name or 91 - // handle/repo-name or 92 - // any of the above with a leading slash (/) 93 88 components := strings.Split(strings.TrimPrefix(strings.Trim(repo, "'"), "/"), "/") 94 89 l.Info("command components", "components", components) 95 90 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] 91 + var rbacResource string 92 + var diskRelative string 104 93 105 - resolver := idresolver.DefaultResolver(h.c.Server.PlcUrl) 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 106 113 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") 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>") 112 154 return 113 155 } 114 - repoOwnerDid := repoOwnerIdent.DID.String() 115 - 116 - qualifiedRepo, _ := securejoin.SecureJoin(repoOwnerDid, repoName) 117 156 118 157 if gitCommand == "git-receive-pack" { 119 - ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, qualifiedRepo) 158 + ok, err := h.e.IsPushAllowed(incomingUser, rbac.ThisServer, rbacResource) 120 159 if err != nil || !ok { 121 160 w.WriteHeader(http.StatusForbidden) 122 161 fmt.Fprint(w, repo) ··· 125 164 } 126 165 127 166 w.WriteHeader(http.StatusOK) 128 - fmt.Fprint(w, qualifiedRepo) 167 + fmt.Fprint(w, diskRelative) 129 168 } 130 169 131 170 type PushOptions struct { ··· 140 179 gitRelativeDir, err := filepath.Rel(h.c.Repo.ScanPath, gitAbsoluteDir) 141 180 if err != nil { 142 181 l.Error("failed to calculate relative git dir", "scanPath", h.c.Repo.ScanPath, "gitAbsoluteDir", gitAbsoluteDir) 182 + w.WriteHeader(http.StatusInternalServerError) 143 183 return 144 184 } 145 185 146 - parts := strings.SplitN(gitRelativeDir, "/", 2) 147 - if len(parts) != 2 { 148 - l.Error("invalid git dir", "gitRelativeDir", gitRelativeDir) 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) 149 190 return 150 191 } 151 - repoDid := parts[0] 152 - repoName := parts[1] 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 + } 153 199 154 200 gitUserDid := r.Header.Get("X-Git-User-Did") 155 201 ··· 176 222 } 177 223 178 224 for _, line := range lines { 179 - err := h.insertRefUpdate(line, gitUserDid, repoDid, repoName) 225 + err := h.insertRefUpdate(line, gitUserDid, ownerDid, repoName, repoDid) 180 226 if err != nil { 181 227 l.Error("failed to insert op", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 182 - // non-fatal 183 228 } 184 229 185 - err = h.emitCompareLink(&resp.Messages, line, repoDid, repoName) 230 + err = h.emitCompareLink(&resp.Messages, line, ownerDid, repoName, repoDid) 186 231 if err != nil { 187 232 l.Error("failed to reply with compare link", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 188 - // non-fatal 189 233 } 190 234 191 - err = h.triggerPipeline(&resp.Messages, line, gitUserDid, repoDid, repoName, pushOptions) 235 + err = h.triggerPipeline(&resp.Messages, line, gitUserDid, ownerDid, repoName, repoDid, pushOptions) 192 236 if err != nil { 193 237 l.Error("failed to trigger pipeline", "err", err, "line", line, "did", gitUserDid, "repo", gitRelativeDir) 194 - // non-fatal 195 238 } 196 239 } 197 240 198 241 writeJSON(w, resp) 199 242 } 200 243 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 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) 210 248 } 211 249 212 250 gr, err := git.Open(repoPath, line.Ref) ··· 214 252 return fmt.Errorf("failed to open git repo at ref %s: %w", line.Ref, err) 215 253 } 216 254 217 - var errs error 218 255 meta, err := gr.RefUpdateMeta(line) 219 - errors.Join(errs, err) 256 + if err != nil { 257 + return fmt.Errorf("failed to get ref update metadata: %w", err) 258 + } 220 259 221 260 metaRecord := meta.AsRecord() 222 261 ··· 225 264 NewSha: line.NewSha.String(), 226 265 Ref: line.Ref, 227 266 CommitterDid: gitUserDid, 228 - RepoDid: repoDid, 267 + OwnerDid: ownerDid, 229 268 RepoName: repoName, 269 + RepoDid: repoDid, 230 270 Meta: &metaRecord, 231 271 } 272 + 232 273 eventJson, err := json.Marshal(refUpdate) 233 274 if err != nil { 234 275 return err ··· 240 281 EventJson: string(eventJson), 241 282 } 242 283 243 - return errors.Join(errs, h.db.InsertEvent(event, h.n)) 284 + return h.db.InsertEvent(event, h.n) 244 285 } 245 286 246 287 func (h *InternalHandle) triggerPipeline( 247 288 clientMsgs *[]string, 248 289 line git.PostReceiveLine, 249 290 gitUserDid string, 291 + ownerDid string, 292 + repoName string, 250 293 repoDid string, 251 - repoName string, 252 294 pushOptions PushOptions, 253 295 ) error { 254 296 if pushOptions.skipCi { 255 297 return nil 256 298 } 257 299 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 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) 266 303 } 267 304 268 305 gr, err := git.Open(repoPath, line.Ref) ··· 299 336 NewSha: line.NewSha.String(), 300 337 } 301 338 339 + triggerRepo := &tangled.Pipeline_TriggerRepo{ 340 + Did: ownerDid, 341 + Knot: h.c.Server.Hostname, 342 + Repo: repoName, 343 + RepoDid: repoDid, 344 + } 345 + 302 346 compiler := workflow.Compiler{ 303 347 Trigger: tangled.Pipeline_TriggerMetadata{ 304 348 Kind: string(workflow.TriggerKindPush), 305 349 Push: &trigger, 306 - Repo: &tangled.Pipeline_TriggerRepo{ 307 - Did: repoDid, 308 - Knot: h.c.Server.Hostname, 309 - Repo: repoName, 310 - }, 350 + Repo: triggerRepo, 311 351 }, 312 352 } 313 353 ··· 348 388 func (h *InternalHandle) emitCompareLink( 349 389 clientMsgs *[]string, 350 390 line git.PostReceiveLine, 391 + ownerDid string, 392 + repoName string, 351 393 repoDid string, 352 - repoName string, 353 394 ) error { 354 395 // this is a second push to a branch, don't reply with the link again 355 396 if !line.OldSha.IsZero() { ··· 365 406 366 407 pushedRef := plumbing.ReferenceName(line.Ref) 367 408 368 - userIdent, err := h.res.ResolveIdent(context.Background(), repoDid) 369 - user := repoDid 409 + userIdent, err := h.res.ResolveIdent(context.Background(), ownerDid) 410 + user := ownerDid 370 411 if err == nil { 371 412 user = userIdent.Handle.String() 372 413 } 373 414 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 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) 382 418 } 383 419 384 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 81 82 82 r.Route("/{did}", func(r chi.Router) { 83 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 + 84 90 r.Route("/{name}", func(r chi.Router) { 85 - // routes for git operations 86 91 r.Get("/info/refs", h.InfoRefs) 87 92 r.Post("/git-upload-archive", h.UploadArchive) 88 93 r.Post("/git-upload-pack", h.UploadPack) ··· 136 141 } 137 142 138 143 suffix := strings.TrimPrefix(r.URL.Path, "/"+didOrHandle) 139 - newPath := fmt.Sprintf("/%s/%s?%s", id.DID.String(), suffix, r.URL.RawQuery) 144 + newPath := "/" + id.DID.String() + suffix 145 + if r.URL.RawQuery != "" { 146 + newPath += "?" + r.URL.RawQuery 147 + } 140 148 http.Redirect(w, r, newPath, http.StatusTemporaryRedirect) 141 149 }) 142 150 }
+124 -24
knotserver/xrpc/create_repo.go
··· 1 1 package xrpc 2 2 3 3 import ( 4 + "context" 4 5 "encoding/json" 5 6 "errors" 6 7 "fmt" 7 8 "net/http" 8 - "path/filepath" 9 + "os" 9 10 "strings" 11 + "time" 10 12 11 - comatproto "github.com/bluesky-social/indigo/api/atproto" 12 13 "github.com/bluesky-social/indigo/atproto/syntax" 13 - "github.com/bluesky-social/indigo/xrpc" 14 14 securejoin "github.com/cyphar/filepath-securejoin" 15 15 gogit "github.com/go-git/go-git/v5" 16 16 "tangled.org/core/api/tangled" 17 17 "tangled.org/core/hook" 18 18 "tangled.org/core/knotserver/git" 19 + "tangled.org/core/knotserver/repodid" 19 20 "tangled.org/core/rbac" 20 21 xrpcerr "tangled.org/core/xrpc/errors" 21 22 ) ··· 49 50 return 50 51 } 51 52 52 - rkey := data.Rkey 53 + repoName := data.Name 53 54 54 - ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String()) 55 - if err != nil || ident.Handle.IsInvalidHandle() { 56 - fail(xrpcerr.GenericError(err)) 55 + if repoName == "" { 56 + fail(xrpcerr.GenericError(fmt.Errorf("repository name is required"))) 57 57 return 58 58 } 59 59 60 - xrpcc := xrpc.Client{ 61 - Host: ident.PDSEndpoint(), 60 + defaultBranch := h.Config.Repo.MainBranch 61 + if data.DefaultBranch != nil && *data.DefaultBranch != "" { 62 + defaultBranch = *data.DefaultBranch 62 63 } 63 64 64 - resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) 65 - if err != nil { 65 + if err := validateRepoName(repoName); err != nil { 66 + l.Error("creating repo", "error", err.Error()) 66 67 fail(xrpcerr.GenericError(err)) 67 68 return 68 69 } 69 70 70 - repo := resp.Value.Val.(*tangled.Repo) 71 + var repoDid string 72 + var prepared *repodid.PreparedDID 71 73 72 - defaultBranch := h.Config.Repo.MainBranch 73 - if data.DefaultBranch != nil && *data.DefaultBranch != "" { 74 - defaultBranch = *data.DefaultBranch 74 + knotServiceUrl := "https://" + h.Config.Server.Hostname 75 + if h.Config.Server.Dev { 76 + knotServiceUrl = "http://" + h.Config.Server.Hostname 75 77 } 76 78 77 - if err := validateRepoName(repo.Name); err != nil { 78 - l.Error("creating repo", "error", err.Error()) 79 - fail(xrpcerr.GenericError(err)) 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) 80 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 + } 81 140 } 82 141 83 - relativeRepoPath := filepath.Join(actorDid.String(), repo.Name) 84 - repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath) 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 + } 85 159 86 160 if data.Source != nil && *data.Source != "" { 87 161 err = git.Fork(repoPath, *data.Source, h.Config) 88 162 if err != nil { 89 163 l.Error("forking repo", "error", err.Error()) 164 + cleanupAll() 90 165 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 91 166 return 92 167 } ··· 94 169 err = git.InitBare(repoPath, defaultBranch) 95 170 if err != nil { 96 171 l.Error("initializing bare repo", "error", err.Error()) 172 + cleanupAll() 97 173 if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { 98 174 fail(xrpcerr.RepoExistsError("repository already exists")) 99 175 return 100 - } else { 101 - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 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) 102 187 return 103 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 104 203 } 105 204 } 106 205 107 206 // add perms for this user to access the repo 108 - err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath) 207 + err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, rbacPath) 109 208 if err != nil { 110 209 l.Error("adding repo permissions", "error", err.Error()) 210 + cleanupAll() 111 211 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 112 212 return 113 213 } ··· 120 220 repoPath, 121 221 ) 122 222 123 - w.WriteHeader(http.StatusOK) 223 + writeJson(w, &tangled.RepoCreate_Output{RepoDid: &repoDid}) 124 224 } 125 225 126 226 func validateRepoName(name string) error {
+10 -7
knotserver/xrpc/delete_branch.go
··· 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 11 "tangled.org/core/api/tangled" 13 12 "tangled.org/core/knotserver/git" 14 13 "tangled.org/core/rbac" ··· 57 56 } 58 57 59 58 repo := resp.Value.Val.(*tangled.Repo) 60 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 59 + repoDid, err := x.Db.GetRepoDid(ident.DID.String(), repo.Name) 61 60 if err != nil { 62 - fail(xrpcerr.GenericError(err)) 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) 63 67 return 64 68 } 65 69 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) 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) 68 72 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 69 73 return 70 74 } 71 75 72 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 73 - gr, err := git.PlainOpen(path) 76 + gr, err := git.PlainOpen(repoPath) 74 77 if err != nil { 75 78 fail(xrpcerr.GenericError(err)) 76 79 return
+15 -9
knotserver/xrpc/delete_repo.go
··· 5 5 "fmt" 6 6 "net/http" 7 7 "os" 8 - "path/filepath" 9 8 10 9 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 10 "github.com/bluesky-social/indigo/atproto/syntax" 12 11 "github.com/bluesky-social/indigo/xrpc" 13 - securejoin "github.com/cyphar/filepath-securejoin" 14 12 "tangled.org/core/api/tangled" 15 13 "tangled.org/core/rbac" 16 14 xrpcerr "tangled.org/core/xrpc/errors" ··· 61 59 return 62 60 } 63 61 64 - relativeRepoPath := filepath.Join(did, name) 65 - isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 62 + repoDid, err := x.Db.GetRepoDid(did, name) 66 63 if err != nil { 67 - fail(xrpcerr.GenericError(err)) 64 + fail(xrpcerr.RepoNotFoundError) 68 65 return 69 66 } 70 - if !isDeleteAllowed { 71 - fail(xrpcerr.AccessControlError(actorDid.String())) 67 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 68 + if err != nil { 69 + fail(xrpcerr.RepoNotFoundError) 72 70 return 73 71 } 74 72 75 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 73 + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, repoDid) 76 74 if err != nil { 77 75 fail(xrpcerr.GenericError(err)) 76 + return 77 + } 78 + if !isDeleteAllowed { 79 + fail(xrpcerr.AccessControlError(actorDid.String())) 78 80 return 79 81 } 80 82 ··· 85 87 return 86 88 } 87 89 88 - err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 90 + err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, repoDid) 89 91 if err != nil { 90 92 l.Error("failed to delete repo from enforcer", "error", err.Error()) 91 93 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 92 94 return 95 + } 96 + 97 + if err := x.Db.DeleteRepoKey(repoDid); err != nil { 98 + l.Error("failed to delete repo key", "error", err.Error()) 93 99 } 94 100 95 101 w.WriteHeader(http.StatusOK)
+11 -9
knotserver/xrpc/fork_status.go
··· 7 7 "path/filepath" 8 8 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 10 "tangled.org/core/api/tangled" 12 11 "tangled.org/core/knotserver/git" 13 12 "tangled.org/core/rbac" ··· 51 50 name = filepath.Base(source) 52 51 } 53 52 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) 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) 59 61 return 60 62 } 61 63 62 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 63 - if err != nil { 64 - fail(xrpcerr.GenericError(err)) 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) 65 67 return 66 68 } 67 69
+11 -10
knotserver/xrpc/fork_sync.go
··· 4 4 "encoding/json" 5 5 "fmt" 6 6 "net/http" 7 - "path/filepath" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 9 "tangled.org/core/api/tangled" 12 10 "tangled.org/core/knotserver/git" 13 11 "tangled.org/core/rbac" ··· 42 40 return 43 41 } 44 42 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) 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) 50 51 return 51 52 } 52 53 53 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 54 - if err != nil { 55 - fail(xrpcerr.GenericError(err)) 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) 56 57 return 57 58 } 58 59
+8 -10
knotserver/xrpc/hidden_ref.go
··· 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 11 "tangled.org/core/api/tangled" 13 12 "tangled.org/core/knotserver/git" 14 13 "tangled.org/core/rbac" ··· 63 62 } 64 63 65 64 repo := resp.Value.Val.(*tangled.Repo) 66 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 65 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 67 66 if err != nil { 68 - fail(xrpcerr.GenericError(err)) 67 + fail(xrpcerr.RepoNotFoundError) 69 68 return 70 69 } 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) 70 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 71 + if err != nil { 72 + fail(xrpcerr.RepoNotFoundError) 75 73 return 76 74 } 77 75 78 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 79 - if err != nil { 80 - fail(xrpcerr.GenericError(err)) 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) 81 79 return 82 80 } 83 81
+8 -10
knotserver/xrpc/merge.go
··· 7 7 "net/http" 8 8 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 10 "tangled.org/core/api/tangled" 12 11 "tangled.org/core/knotserver/git" 13 12 "tangled.org/core/patchutil" ··· 43 42 return 44 43 } 45 44 46 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 45 + repoDid, err := x.Db.GetRepoDid(did, name) 47 46 if err != nil { 48 - fail(xrpcerr.GenericError(err)) 47 + fail(xrpcerr.RepoNotFoundError) 49 48 return 50 49 } 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) 50 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 51 + if err != nil { 52 + fail(xrpcerr.RepoNotFoundError) 55 53 return 56 54 } 57 55 58 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 59 - if err != nil { 60 - fail(xrpcerr.GenericError(err)) 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) 61 59 return 62 60 } 63 61
+4 -6
knotserver/xrpc/merge_check.go
··· 6 6 "fmt" 7 7 "net/http" 8 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 9 "tangled.org/core/api/tangled" 11 10 "tangled.org/core/knotserver/git" 12 11 "tangled.org/core/patchutil" ··· 34 33 return 35 34 } 36 35 37 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 36 + repoDid, err := x.Db.GetRepoDid(did, name) 38 37 if err != nil { 39 - fail(xrpcerr.GenericError(err)) 38 + fail(xrpcerr.RepoNotFoundError) 40 39 return 41 40 } 42 - 43 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 41 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) 44 42 if err != nil { 45 - fail(xrpcerr.GenericError(err)) 43 + fail(xrpcerr.RepoNotFoundError) 46 44 return 47 45 } 48 46
+9 -6
knotserver/xrpc/set_default_branch.go
··· 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 "github.com/bluesky-social/indigo/xrpc" 11 - securejoin "github.com/cyphar/filepath-securejoin" 12 11 "tangled.org/core/api/tangled" 13 12 "tangled.org/core/knotserver/git" 14 13 "tangled.org/core/rbac" ··· 59 58 } 60 59 61 60 repo := resp.Value.Val.(*tangled.Repo) 62 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 61 + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) 63 62 if err != nil { 64 - fail(xrpcerr.GenericError(err)) 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) 65 69 return 66 70 } 67 71 68 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 72 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { 69 73 l.Error("insufficent permissions", "did", actorDid.String()) 70 74 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 71 75 return 72 76 } 73 77 74 - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) 75 - gr, err := git.PlainOpen(path) 78 + gr, err := git.PlainOpen(repoPath) 76 79 if err != nil { 77 80 fail(xrpcerr.GenericError(err)) 78 81 return
+3 -25
knotserver/xrpc/xrpc.go
··· 6 6 "net/http" 7 7 "strings" 8 8 9 - securejoin "github.com/cyphar/filepath-securejoin" 10 9 "tangled.org/core/api/tangled" 11 10 "tangled.org/core/idresolver" 12 11 "tangled.org/core/jetstream" ··· 78 77 return r 79 78 } 80 79 81 - // parseRepoParam parses a repo parameter in 'did/repoName' format and returns 82 - // the full repository path on disk 83 80 func (x *Xrpc) parseRepoParam(repo string) (string, error) { 84 - if repo == "" { 81 + if repo == "" || !strings.HasPrefix(repo, "did:") { 85 82 return "", xrpcerr.NewXrpcError( 86 83 xrpcerr.WithTag("InvalidRequest"), 87 - xrpcerr.WithMessage("missing repo parameter"), 84 + xrpcerr.WithMessage("missing or invalid repo parameter, expected a repo DID"), 88 85 ) 89 86 } 90 87 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) 88 + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo) 105 89 if err != nil { 106 90 return "", xrpcerr.RepoNotFoundError 107 91 } 108 - 109 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) 110 - if err != nil { 111 - return "", xrpcerr.RepoNotFoundError 112 - } 113 - 114 92 return repoPath, nil 115 93 } 116 94
+9
lexicons/actor/profile.json
··· 60 60 "maxGraphemes": 40, 61 61 "maxLength": 400 62 62 }, 63 + "pinnedRepositoryDids": { 64 + "type": "array", 65 + "minLength": 0, 66 + "maxLength": 6, 67 + "items": { 68 + "type": "string", 69 + "format": "did" 70 + } 71 + }, 63 72 "pinnedRepositories": { 64 73 "type": "array", 65 74 "description": "Any ATURI, it is up to appviews to validate these fields.",
+4
lexicons/feed/reaction.json
··· 19 19 "type": "string", 20 20 "format": "at-uri" 21 21 }, 22 + "subjectDid": { 23 + "type": "string", 24 + "format": "did" 25 + }, 22 26 "reaction": { 23 27 "type": "string", 24 28 "enum": [ "👍", "👎", "😆", "🎉", "🫤", "❤️", "🚀", "👀" ]
+4 -1
lexicons/feed/star.json
··· 10 10 "record": { 11 11 "type": "object", 12 12 "required": [ 13 - "subject", 14 13 "createdAt" 15 14 ], 16 15 "properties": { 17 16 "subject": { 18 17 "type": "string", 19 18 "format": "at-uri" 19 + }, 20 + "subjectDid": { 21 + "type": "string", 22 + "format": "did" 20 23 }, 21 24 "createdAt": { 22 25 "type": "string",
+7 -1
lexicons/git/refUpdate.json
··· 11 11 "required": [ 12 12 "ref", 13 13 "committerDid", 14 + "ownerDid", 14 15 "repoDid", 15 16 "repoName", 16 17 "oldSha", ··· 29 30 "description": "did of the user that pushed this ref", 30 31 "format": "did" 31 32 }, 33 + "ownerDid": { 34 + "type": "string", 35 + "description": "did of the owner of the repo", 36 + "format": "did" 37 + }, 32 38 "repoDid": { 33 39 "type": "string", 34 - "description": "did of the owner of the repo", 40 + "description": "DID of the repo itself", 35 41 "format": "did" 36 42 }, 37 43 "repoName": {
+5 -1
lexicons/issue/issue.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["repo", "title", "createdAt"], 12 + "required": ["title", "createdAt"], 13 13 "properties": { 14 14 "repo": { 15 15 "type": "string", 16 16 "format": "at-uri" 17 + }, 18 + "repoDid": { 19 + "type": "string", 20 + "format": "did" 17 21 }, 18 22 "title": { 19 23 "type": "string"
+4
lexicons/label/op.json
··· 21 21 "format": "at-uri", 22 22 "description": "The subject (task, pull or discussion) of this label. Appviews may apply a `scope` check and refuse this op." 23 23 }, 24 + "subjectDid": { 25 + "type": "string", 26 + "format": "did" 27 + }, 24 28 "performedAt": { 25 29 "type": "string", 26 30 "format": "datetime"
+6
lexicons/pipeline/pipeline.json
··· 66 66 "required": [ 67 67 "knot", 68 68 "did", 69 + "repoDid", 69 70 "repo", 70 71 "defaultBranch" 71 72 ], ··· 75 76 }, 76 77 "did": { 77 78 "type": "string", 79 + "format": "did" 80 + }, 81 + "repoDid": { 82 + "type": "string", 83 + "description": "DID of the repo itself", 78 84 "format": "did" 79 85 }, 80 86 "repo": {
+8 -1
lexicons/pulls/pull.json
··· 65 65 "target": { 66 66 "type": "object", 67 67 "required": [ 68 - "repo", 69 68 "branch" 70 69 ], 71 70 "properties": { 72 71 "repo": { 73 72 "type": "string", 74 73 "format": "at-uri" 74 + }, 75 + "repoDid": { 76 + "type": "string", 77 + "format": "did" 75 78 }, 76 79 "branch": { 77 80 "type": "string" ··· 96 99 "repo": { 97 100 "type": "string", 98 101 "format": "at-uri" 102 + }, 103 + "repoDid": { 104 + "type": "string", 105 + "format": "did" 99 106 } 100 107 } 101 108 }
+4 -1
lexicons/repo/artifact.json
··· 11 11 "type": "object", 12 12 "required": [ 13 13 "name", 14 - "repo", 15 14 "tag", 16 15 "createdAt", 17 16 "artifact" ··· 25 24 "type": "string", 26 25 "format": "at-uri", 27 26 "description": "repo that this artifact is being uploaded to" 27 + }, 28 + "repoDid": { 29 + "type": "string", 30 + "format": "did" 28 31 }, 29 32 "tag": { 30 33 "type": "bytes",
+4 -1
lexicons/repo/collaborator.json
··· 11 11 "type": "object", 12 12 "required": [ 13 13 "subject", 14 - "repo", 15 14 "createdAt" 16 15 ], 17 16 "properties": { ··· 23 22 "type": "string", 24 23 "description": "repo to add this user to", 25 24 "format": "at-uri" 25 + }, 26 + "repoDid": { 27 + "type": "string", 28 + "format": "did" 26 29 }, 27 30 "createdAt": { 28 31 "type": "string",
+23 -1
lexicons/repo/create.json
··· 10 10 "schema": { 11 11 "type": "object", 12 12 "required": [ 13 - "rkey" 13 + "rkey", 14 + "name" 14 15 ], 15 16 "properties": { 16 17 "rkey": { 17 18 "type": "string", 18 19 "description": "Rkey of the repository record" 19 20 }, 21 + "name": { 22 + "type": "string", 23 + "description": "Name of the repository" 24 + }, 20 25 "defaultBranch": { 21 26 "type": "string", 22 27 "description": "Default branch to push to" ··· 24 29 "source": { 25 30 "type": "string", 26 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" 27 49 } 28 50 } 29 51 }
+5
lexicons/repo/repo.json
··· 60 60 "format": "at-uri" 61 61 } 62 62 }, 63 + "repoDid": { 64 + "type": "string", 65 + "format": "did", 66 + "description": "DID of the repo itself, if assigned" 67 + }, 63 68 "createdAt": { 64 69 "type": "string", 65 70 "format": "datetime"
+3
nix/gomod2nix.toml
··· 171 171 [mod."github.com/dgryski/go-rendezvous"] 172 172 version = "v0.0.0-20200823014737-9f7001d12a5f" 173 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=" 174 177 [mod."github.com/distribution/reference"] 175 178 version = "v0.6.0" 176 179 hash = "sha256-gr4tL+qz4jKyAtl8LINcxMSanztdt+pybj1T+2ulQv4="
+1 -1
spindle/engine/engine.go
··· 25 25 // extract secrets 26 26 var allSecrets []secrets.UnlockedSecret 27 27 if didSlashRepo, err := securejoin.SecureJoin(pipeline.RepoOwner, pipeline.RepoName); err == nil { 28 - if res, err := vault.GetSecretsUnlocked(ctx, secrets.DidSlashRepo(didSlashRepo)); err == nil { 28 + if res, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(didSlashRepo)); err == nil { 29 29 allSecrets = res 30 30 } 31 31 }
+32 -26
spindle/ingester.go
··· 228 228 return err 229 229 } 230 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 - } 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 + } 236 240 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 - } 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 + } 243 245 244 - xrpcc := xrpc.Client{ 245 - Host: owner.PDSEndpoint(), 246 - } 246 + xrpcc := xrpc.Client{ 247 + Host: owner.PDSEndpoint(), 248 + } 247 249 248 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 249 - if err != nil { 250 - return err 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 251 262 } 252 263 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 { 264 + if ok, err := s.e.IsCollaboratorInviteAllowed(ownerDid, rbac.ThisServer, rbacResource); !ok || err != nil { 258 265 return fmt.Errorf("insufficient permissions: %w", err) 259 266 } 260 267 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) 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) 265 271 } 266 272 267 273 return nil
+4 -4
spindle/secrets/manager.go
··· 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 ) 11 11 12 - type DidSlashRepo string 12 + type RepoIdentifier string 13 13 14 14 type Secret[T any] struct { 15 15 Key string 16 16 Value T 17 - Repo DidSlashRepo 17 + Repo RepoIdentifier 18 18 CreatedAt time.Time 19 19 CreatedBy syntax.DID 20 20 } ··· 29 29 type Manager interface { 30 30 AddSecret(ctx context.Context, secret UnlockedSecret) error 31 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) 32 + GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) 33 + GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) 34 34 } 35 35 36 36 // stopper interface for managers that need cleanup
+5 -5
spindle/secrets/openbao.go
··· 149 149 return nil 150 150 } 151 151 152 - func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) { 152 + func (v *OpenBaoManager) GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) { 153 153 repoPath := v.buildRepoPath(repo) 154 154 155 155 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) ··· 224 224 return secrets, nil 225 225 } 226 226 227 - func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) { 227 + func (v *OpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) { 228 228 repoPath := v.buildRepoPath(repo) 229 229 230 230 secretsList, err := v.client.Logical().ListWithContext(ctx, fmt.Sprintf("%s/metadata/%s", v.mountPath, repoPath)) ··· 307 307 } 308 308 309 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 310 + func (v *OpenBaoManager) buildRepoPath(repo RepoIdentifier) string { 311 + // convert RepoIdentifier to a safe path by replacing special characters 312 312 repoPath := strings.ReplaceAll(string(repo), "/", "_") 313 313 repoPath = strings.ReplaceAll(repoPath, ":", "_") 314 314 repoPath = strings.ReplaceAll(repoPath, ".", "_") ··· 316 316 } 317 317 318 318 // buildSecretPath creates a path for a specific secret 319 - func (v *OpenBaoManager) buildSecretPath(repo DidSlashRepo, key string) string { 319 + func (v *OpenBaoManager) buildSecretPath(repo RepoIdentifier, key string) string { 320 320 return path.Join(v.buildRepoPath(repo), key) 321 321 }
+17 -17
spindle/secrets/openbao_test.go
··· 32 32 m.errorToReturn = nil 33 33 } 34 34 35 - func (m *MockOpenBaoManager) buildKey(repo DidSlashRepo, key string) string { 35 + func (m *MockOpenBaoManager) buildKey(repo RepoIdentifier, key string) string { 36 36 return string(repo) + "_" + key 37 37 } 38 38 ··· 64 64 return nil 65 65 } 66 66 67 - func (m *MockOpenBaoManager) GetSecretsLocked(ctx context.Context, repo DidSlashRepo) ([]LockedSecret, error) { 67 + func (m *MockOpenBaoManager) GetSecretsLocked(ctx context.Context, repo RepoIdentifier) ([]LockedSecret, error) { 68 68 if m.shouldError { 69 69 return nil, m.errorToReturn 70 70 } ··· 84 84 return result, nil 85 85 } 86 86 87 - func (m *MockOpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo DidSlashRepo) ([]UnlockedSecret, error) { 87 + func (m *MockOpenBaoManager) GetSecretsUnlocked(ctx context.Context, repo RepoIdentifier) ([]UnlockedSecret, error) { 88 88 if m.shouldError { 89 89 return nil, m.errorToReturn 90 90 } ··· 103 103 return UnlockedSecret{ 104 104 Key: key, 105 105 Value: value, 106 - Repo: DidSlashRepo(repo), 106 + Repo: RepoIdentifier(repo), 107 107 CreatedAt: time.Now(), 108 108 CreatedBy: syntax.DID(createdBy), 109 109 } ··· 173 173 174 174 tests := []struct { 175 175 name string 176 - repo DidSlashRepo 176 + repo RepoIdentifier 177 177 key string 178 178 expected string 179 179 }{ 180 180 { 181 181 name: "simple repo path", 182 - repo: DidSlashRepo("did:plc:foo/repo"), 182 + repo: RepoIdentifier("did:plc:foo/repo"), 183 183 key: "api_key", 184 184 expected: "repos/did_plc_foo_repo/api_key", 185 185 }, 186 186 { 187 187 name: "complex repo path with dots", 188 - repo: DidSlashRepo("did:web:example.com/my-repo"), 188 + repo: RepoIdentifier("did:web:example.com/my-repo"), 189 189 key: "secret_key", 190 190 expected: "repos/did_web_example_com_my-repo/secret_key", 191 191 }, ··· 204 204 205 205 tests := []struct { 206 206 name string 207 - repo DidSlashRepo 207 + repo RepoIdentifier 208 208 expected string 209 209 }{ 210 210 { ··· 310 310 }, 311 311 removeSecret: Secret[any]{ 312 312 Key: "API_KEY", 313 - Repo: DidSlashRepo("did:plc:test/repo1"), 313 + Repo: RepoIdentifier("did:plc:test/repo1"), 314 314 }, 315 315 expectError: false, 316 316 }, ··· 319 319 setupSecrets: []UnlockedSecret{}, 320 320 removeSecret: Secret[any]{ 321 321 Key: "API_KEY", 322 - Repo: DidSlashRepo("did:plc:test/repo1"), 322 + Repo: RepoIdentifier("did:plc:test/repo1"), 323 323 }, 324 324 expectError: true, 325 325 }, ··· 352 352 tests := []struct { 353 353 name string 354 354 setupSecrets []UnlockedSecret 355 - queryRepo DidSlashRepo 355 + queryRepo RepoIdentifier 356 356 expectedCount int 357 357 expectedKeys []string 358 358 expectError bool ··· 364 364 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 365 365 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 366 366 }, 367 - queryRepo: DidSlashRepo("did:plc:test/repo1"), 367 + queryRepo: RepoIdentifier("did:plc:test/repo1"), 368 368 expectedCount: 2, 369 369 expectedKeys: []string{"API_KEY", "DB_PASSWORD"}, 370 370 expectError: false, ··· 372 372 { 373 373 name: "get secrets from empty repo", 374 374 setupSecrets: []UnlockedSecret{}, 375 - queryRepo: DidSlashRepo("did:plc:test/empty"), 375 + queryRepo: RepoIdentifier("did:plc:test/empty"), 376 376 expectedCount: 0, 377 377 expectedKeys: []string{}, 378 378 expectError: false, ··· 417 417 tests := []struct { 418 418 name string 419 419 setupSecrets []UnlockedSecret 420 - queryRepo DidSlashRepo 420 + queryRepo RepoIdentifier 421 421 expectedCount int 422 422 expectedSecrets map[string]string // key -> value 423 423 expectError bool ··· 429 429 createTestSecretForOpenBao("did:plc:test/repo1", "DB_PASSWORD", "dbpass456", "did:plc:creator"), 430 430 createTestSecretForOpenBao("did:plc:test/repo2", "OTHER_KEY", "other789", "did:plc:creator"), 431 431 }, 432 - queryRepo: DidSlashRepo("did:plc:test/repo1"), 432 + queryRepo: RepoIdentifier("did:plc:test/repo1"), 433 433 expectedCount: 2, 434 434 expectedSecrets: map[string]string{ 435 435 "API_KEY": "secret123", ··· 440 440 { 441 441 name: "get secrets from empty repo", 442 442 setupSecrets: []UnlockedSecret{}, 443 - queryRepo: DidSlashRepo("did:plc:test/empty"), 443 + queryRepo: RepoIdentifier("did:plc:test/empty"), 444 444 expectedCount: 0, 445 445 expectedSecrets: map[string]string{}, 446 446 expectError: false, ··· 521 521 name: "complete workflow", 522 522 scenario: func(t *testing.T, mock *MockOpenBaoManager) { 523 523 ctx := context.Background() 524 - repo := DidSlashRepo("did:plc:test/integration") 524 + repo := RepoIdentifier("did:plc:test/integration") 525 525 526 526 // Start with empty repo 527 527 secrets, err := mock.GetSecretsLocked(ctx, repo)
+2 -2
spindle/secrets/sqlite.go
··· 107 107 return nil 108 108 } 109 109 110 - func (s *SqliteManager) GetSecretsLocked(ctx context.Context, didSlashRepo DidSlashRepo) ([]LockedSecret, error) { 110 + func (s *SqliteManager) GetSecretsLocked(ctx context.Context, didSlashRepo RepoIdentifier) ([]LockedSecret, error) { 111 111 query := fmt.Sprintf(` 112 112 select repo, key, created_at, created_by from %s where repo = ?; 113 113 `, s.tableName) ··· 139 139 return ls, nil 140 140 } 141 141 142 - func (s *SqliteManager) GetSecretsUnlocked(ctx context.Context, didSlashRepo DidSlashRepo) ([]UnlockedSecret, error) { 142 + func (s *SqliteManager) GetSecretsUnlocked(ctx context.Context, didSlashRepo RepoIdentifier) ([]UnlockedSecret, error) { 143 143 query := fmt.Sprintf(` 144 144 select repo, key, value, created_at, created_by from %s where repo = ?; 145 145 `, s.tableName)
+21 -21
spindle/secrets/sqlite_test.go
··· 22 22 return UnlockedSecret{ 23 23 Key: key, 24 24 Value: value, 25 - Repo: DidSlashRepo(repo), 25 + Repo: RepoIdentifier(repo), 26 26 CreatedAt: time.Now(), 27 27 CreatedBy: syntax.DID(createdBy), 28 28 } ··· 147 147 }, 148 148 removeSecret: Secret[any]{ 149 149 Key: "api_key", 150 - Repo: DidSlashRepo("did:plc:foo/repo"), 150 + Repo: RepoIdentifier("did:plc:foo/repo"), 151 151 }, 152 152 expectError: nil, 153 153 }, ··· 158 158 }, 159 159 removeSecret: Secret[any]{ 160 160 Key: "non_existent_key", 161 - Repo: DidSlashRepo("did:plc:foo/repo"), 161 + Repo: RepoIdentifier("did:plc:foo/repo"), 162 162 }, 163 163 expectError: ErrKeyNotFound, 164 164 }, ··· 167 167 setupSecrets: []UnlockedSecret{}, 168 168 removeSecret: Secret[any]{ 169 169 Key: "any_key", 170 - Repo: DidSlashRepo("did:plc:foo/repo"), 170 + Repo: RepoIdentifier("did:plc:foo/repo"), 171 171 }, 172 172 expectError: ErrKeyNotFound, 173 173 }, ··· 178 178 }, 179 179 removeSecret: Secret[any]{ 180 180 Key: "api_key", 181 - Repo: DidSlashRepo("other.com/repo"), 181 + Repo: RepoIdentifier("other.com/repo"), 182 182 }, 183 183 expectError: ErrKeyNotFound, 184 184 }, ··· 209 209 tests := []struct { 210 210 name string 211 211 setupSecrets []UnlockedSecret 212 - queryRepo DidSlashRepo 212 + queryRepo RepoIdentifier 213 213 expectedCount int 214 214 expectedKeys []string 215 215 expectError bool ··· 221 221 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 222 222 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 223 223 }, 224 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 224 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 225 225 expectedCount: 2, 226 226 expectedKeys: []string{"key1", "key2"}, 227 227 expectError: false, ··· 232 232 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 233 233 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 234 234 }, 235 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 235 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 236 236 expectedCount: 1, 237 237 expectedKeys: []string{"single_key"}, 238 238 expectError: false, ··· 242 242 setupSecrets: []UnlockedSecret{ 243 243 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 244 244 }, 245 - queryRepo: DidSlashRepo("nonexistent.com/repo"), 245 + queryRepo: RepoIdentifier("nonexistent.com/repo"), 246 246 expectedCount: 0, 247 247 expectedKeys: []string{}, 248 248 expectError: false, ··· 250 250 { 251 251 name: "get secrets from empty database", 252 252 setupSecrets: []UnlockedSecret{}, 253 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 253 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 254 254 expectedCount: 0, 255 255 expectedKeys: []string{}, 256 256 expectError: false, ··· 311 311 tests := []struct { 312 312 name string 313 313 setupSecrets []UnlockedSecret 314 - queryRepo DidSlashRepo 314 + queryRepo RepoIdentifier 315 315 expectedCount int 316 316 expectedSecrets map[string]string // key -> value 317 317 expectError bool ··· 323 323 createTestSecret("did:plc:foo/repo", "key2", "value2", "did:plc:user2"), 324 324 createTestSecret("other.com/repo", "key3", "value3", "did:plc:user3"), 325 325 }, 326 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 326 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 327 327 expectedCount: 2, 328 328 expectedSecrets: map[string]string{ 329 329 "key1": "value1", ··· 337 337 createTestSecret("did:plc:foo/repo", "single_key", "single_value", "did:plc:user1"), 338 338 createTestSecret("other.com/repo", "other_key", "other_value", "did:plc:user2"), 339 339 }, 340 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 340 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 341 341 expectedCount: 1, 342 342 expectedSecrets: map[string]string{ 343 343 "single_key": "single_value", ··· 349 349 setupSecrets: []UnlockedSecret{ 350 350 createTestSecret("did:plc:foo/repo", "key1", "value1", "did:plc:user1"), 351 351 }, 352 - queryRepo: DidSlashRepo("nonexistent.com/repo"), 352 + queryRepo: RepoIdentifier("nonexistent.com/repo"), 353 353 expectedCount: 0, 354 354 expectedSecrets: map[string]string{}, 355 355 expectError: false, ··· 357 357 { 358 358 name: "get unlocked secrets from empty database", 359 359 setupSecrets: []UnlockedSecret{}, 360 - queryRepo: DidSlashRepo("did:plc:foo/repo"), 360 + queryRepo: RepoIdentifier("did:plc:foo/repo"), 361 361 expectedCount: 0, 362 362 expectedSecrets: map[string]string{}, 363 363 expectError: false, ··· 429 429 return m.AddSecret(context.Background(), secret) 430 430 }, 431 431 func(m Manager) error { 432 - _, err := m.GetSecretsLocked(context.Background(), DidSlashRepo("interface.test/repo")) 432 + _, err := m.GetSecretsLocked(context.Background(), RepoIdentifier("interface.test/repo")) 433 433 return err 434 434 }, 435 435 func(m Manager) error { 436 - _, err := m.GetSecretsUnlocked(context.Background(), DidSlashRepo("interface.test/repo")) 436 + _, err := m.GetSecretsUnlocked(context.Background(), RepoIdentifier("interface.test/repo")) 437 437 return err 438 438 }, 439 439 func(m Manager) error { 440 440 secret := Secret[any]{ 441 441 Key: "test_key", 442 - Repo: DidSlashRepo("interface.test/repo"), 442 + Repo: RepoIdentifier("interface.test/repo"), 443 443 } 444 444 return m.RemoveSecret(context.Background(), secret) 445 445 }, ··· 498 498 { 499 499 name: "multi-repo secret management", 500 500 scenario: func(t *testing.T, manager *SqliteManager) { 501 - repo1 := DidSlashRepo("example1.com/repo") 502 - repo2 := DidSlashRepo("example2.com/repo") 501 + repo1 := RepoIdentifier("example1.com/repo") 502 + repo2 := RepoIdentifier("example2.com/repo") 503 503 504 504 secrets := []UnlockedSecret{ 505 505 createTestSecret(string(repo1), "db_password", "super_secret_123", "did:plc:admin"), ··· 543 543 { 544 544 name: "empty database operations", 545 545 scenario: func(t *testing.T, manager *SqliteManager) { 546 - repo := DidSlashRepo("empty.test/repo") 546 + repo := RepoIdentifier("empty.test/repo") 547 547 548 548 // Operations on empty database should not error 549 549 locked, err := manager.GetSecretsLocked(context.Background(), repo)
+1 -1
spindle/xrpc/add_secret.go
··· 75 75 } 76 76 77 77 secret := secrets.UnlockedSecret{ 78 - Repo: secrets.DidSlashRepo(didPath), 78 + Repo: secrets.RepoIdentifier(didPath), 79 79 Key: data.Key, 80 80 Value: data.Value, 81 81 CreatedAt: time.Now(),
+1 -1
spindle/xrpc/list_secrets.go
··· 69 69 return 70 70 } 71 71 72 - ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.DidSlashRepo(didPath)) 72 + ls, err := x.Vault.GetSecretsLocked(r.Context(), secrets.RepoIdentifier(didPath)) 73 73 if err != nil { 74 74 l.Error("failed to get secret from vault", "did", actorDid.String(), "err", err) 75 75 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+1 -1
spindle/xrpc/remove_secret.go
··· 69 69 } 70 70 71 71 secret := secrets.Secret[any]{ 72 - Repo: secrets.DidSlashRepo(didPath), 72 + Repo: secrets.RepoIdentifier(didPath), 73 73 Key: data.Key, 74 74 } 75 75 err = x.Vault.RemoveSecret(r.Context(), secret)