Monorepo for Tangled

appview, knotserver: all new git repos are created with DIDs

+1921 -612
+1 -1
api/tangled/actorprofile.go
··· 13 13 ) 14 14 15 15 func init() { 16 - // util.RegisterType("sh.tangled.actor.profile", &ActorProfile{}) 16 + util.RegisterType("sh.tangled.actor.profile", &ActorProfile{}) 17 17 } // 18 18 // RECORDTYPE: ActorProfile 19 19 type ActorProfile struct {
+210 -95
api/tangled/cbor_gen.go
··· 949 949 cw := cbg.NewCborWriter(w) 950 950 fieldCount := 4 951 951 952 + if t.Subject == nil { 953 + fieldCount-- 954 + } 955 + 952 956 if t.SubjectDid == nil { 953 957 fieldCount-- 954 958 } ··· 977 981 } 978 982 979 983 // t.Subject (string) (string) 980 - if len("subject") > 1000000 { 981 - return xerrors.Errorf("Value in field \"subject\" was too long") 982 - } 984 + if t.Subject != nil { 983 985 984 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 985 - return err 986 - } 987 - if _, err := cw.WriteString(string("subject")); err != nil { 988 - return err 989 - } 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 + } 990 996 991 - if len(t.Subject) > 1000000 { 992 - return xerrors.Errorf("Value in field t.Subject was too long") 993 - } 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 + } 994 1005 995 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 996 - return err 997 - } 998 - if _, err := cw.WriteString(string(t.Subject)); err != nil { 999 - 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 + } 1000 1013 } 1001 1014 1002 1015 // t.CreatedAt (string) (string) ··· 1112 1125 case "subject": 1113 1126 1114 1127 { 1115 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1128 + b, err := cr.ReadByte() 1116 1129 if err != nil { 1117 1130 return err 1118 1131 } 1132 + if b != cbg.CborNull[0] { 1133 + if err := cr.UnreadByte(); err != nil { 1134 + return err 1135 + } 1119 1136 1120 - 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 + } 1121 1144 } 1122 1145 // t.CreatedAt (string) (string) 1123 1146 case "createdAt": ··· 6922 6945 cw := cbg.NewCborWriter(w) 6923 6946 fieldCount := 7 6924 6947 6948 + if t.Repo == nil { 6949 + fieldCount-- 6950 + } 6951 + 6925 6952 if t.RepoDid == nil { 6926 6953 fieldCount-- 6927 6954 } ··· 6986 7013 } 6987 7014 6988 7015 // t.Repo (string) (string) 6989 - if len("repo") > 1000000 { 6990 - return xerrors.Errorf("Value in field \"repo\" was too long") 6991 - } 7016 + if t.Repo != nil { 6992 7017 6993 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 6994 - return err 6995 - } 6996 - if _, err := cw.WriteString(string("repo")); err != nil { 6997 - return err 6998 - } 7018 + if len("repo") > 1000000 { 7019 + return xerrors.Errorf("Value in field \"repo\" was too long") 7020 + } 6999 7021 7000 - if len(t.Repo) > 1000000 { 7001 - return xerrors.Errorf("Value in field t.Repo was too long") 7002 - } 7022 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7023 + return err 7024 + } 7025 + if _, err := cw.WriteString(string("repo")); err != nil { 7026 + return err 7027 + } 7003 7028 7004 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 7005 - return err 7006 - } 7007 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 7008 - return err 7029 + if t.Repo == nil { 7030 + if _, err := cw.Write(cbg.CborNull); err != nil { 7031 + return err 7032 + } 7033 + } else { 7034 + if len(*t.Repo) > 1000000 { 7035 + return xerrors.Errorf("Value in field t.Repo was too long") 7036 + } 7037 + 7038 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 7039 + return err 7040 + } 7041 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 7042 + return err 7043 + } 7044 + } 7009 7045 } 7010 7046 7011 7047 // t.LexiconTypeID (string) (string) ··· 7179 7215 case "repo": 7180 7216 7181 7217 { 7182 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7218 + b, err := cr.ReadByte() 7183 7219 if err != nil { 7184 7220 return err 7185 7221 } 7222 + if b != cbg.CborNull[0] { 7223 + if err := cr.UnreadByte(); err != nil { 7224 + return err 7225 + } 7186 7226 7187 - t.Repo = string(sval) 7227 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7228 + if err != nil { 7229 + return err 7230 + } 7231 + 7232 + t.Repo = (*string)(&sval) 7233 + } 7188 7234 } 7189 7235 // t.LexiconTypeID (string) (string) 7190 7236 case "$type": ··· 7269 7315 cw := cbg.NewCborWriter(w) 7270 7316 fieldCount := 5 7271 7317 7318 + if t.Repo == nil { 7319 + fieldCount-- 7320 + } 7321 + 7272 7322 if t.RepoDid == nil { 7273 7323 fieldCount-- 7274 7324 } ··· 7278 7328 } 7279 7329 7280 7330 // t.Repo (string) (string) 7281 - if len("repo") > 1000000 { 7282 - return xerrors.Errorf("Value in field \"repo\" was too long") 7283 - } 7331 + if t.Repo != nil { 7284 7332 7285 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7286 - return err 7287 - } 7288 - if _, err := cw.WriteString(string("repo")); err != nil { 7289 - return err 7290 - } 7333 + if len("repo") > 1000000 { 7334 + return xerrors.Errorf("Value in field \"repo\" was too long") 7335 + } 7336 + 7337 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7338 + return err 7339 + } 7340 + if _, err := cw.WriteString(string("repo")); err != nil { 7341 + return err 7342 + } 7291 7343 7292 - if len(t.Repo) > 1000000 { 7293 - return xerrors.Errorf("Value in field t.Repo was too long") 7294 - } 7344 + if t.Repo == nil { 7345 + if _, err := cw.Write(cbg.CborNull); err != nil { 7346 + return err 7347 + } 7348 + } else { 7349 + if len(*t.Repo) > 1000000 { 7350 + return xerrors.Errorf("Value in field t.Repo was too long") 7351 + } 7295 7352 7296 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 7297 - return err 7298 - } 7299 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 7300 - return err 7353 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 7354 + return err 7355 + } 7356 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 7357 + return err 7358 + } 7359 + } 7301 7360 } 7302 7361 7303 7362 // t.LexiconTypeID (string) (string) ··· 7444 7503 case "repo": 7445 7504 7446 7505 { 7447 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7506 + b, err := cr.ReadByte() 7448 7507 if err != nil { 7449 7508 return err 7450 7509 } 7510 + if b != cbg.CborNull[0] { 7511 + if err := cr.UnreadByte(); err != nil { 7512 + return err 7513 + } 7451 7514 7452 - t.Repo = string(sval) 7515 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7516 + if err != nil { 7517 + return err 7518 + } 7519 + 7520 + t.Repo = (*string)(&sval) 7521 + } 7453 7522 } 7454 7523 // t.LexiconTypeID (string) (string) 7455 7524 case "$type": ··· 7537 7606 fieldCount-- 7538 7607 } 7539 7608 7609 + if t.Repo == nil { 7610 + fieldCount-- 7611 + } 7612 + 7540 7613 if t.RepoDid == nil { 7541 7614 fieldCount-- 7542 7615 } ··· 7578 7651 } 7579 7652 7580 7653 // t.Repo (string) (string) 7581 - if len("repo") > 1000000 { 7582 - return xerrors.Errorf("Value in field \"repo\" was too long") 7583 - } 7654 + if t.Repo != nil { 7584 7655 7585 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7586 - return err 7587 - } 7588 - if _, err := cw.WriteString(string("repo")); err != nil { 7589 - return err 7590 - } 7656 + if len("repo") > 1000000 { 7657 + return xerrors.Errorf("Value in field \"repo\" was too long") 7658 + } 7659 + 7660 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 7661 + return err 7662 + } 7663 + if _, err := cw.WriteString(string("repo")); err != nil { 7664 + return err 7665 + } 7591 7666 7592 - if len(t.Repo) > 1000000 { 7593 - return xerrors.Errorf("Value in field t.Repo was too long") 7594 - } 7667 + if t.Repo == nil { 7668 + if _, err := cw.Write(cbg.CborNull); err != nil { 7669 + return err 7670 + } 7671 + } else { 7672 + if len(*t.Repo) > 1000000 { 7673 + return xerrors.Errorf("Value in field t.Repo was too long") 7674 + } 7595 7675 7596 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 7597 - return err 7598 - } 7599 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 7600 - return err 7676 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 7677 + return err 7678 + } 7679 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 7680 + return err 7681 + } 7682 + } 7601 7683 } 7602 7684 7603 7685 // t.LexiconTypeID (string) (string) ··· 7837 7919 case "repo": 7838 7920 7839 7921 { 7840 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 7922 + b, err := cr.ReadByte() 7841 7923 if err != nil { 7842 7924 return err 7843 7925 } 7926 + if b != cbg.CborNull[0] { 7927 + if err := cr.UnreadByte(); err != nil { 7928 + return err 7929 + } 7844 7930 7845 - t.Repo = string(sval) 7931 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 7932 + if err != nil { 7933 + return err 7934 + } 7935 + 7936 + t.Repo = (*string)(&sval) 7937 + } 7846 7938 } 7847 7939 // t.LexiconTypeID (string) (string) 7848 7940 case "$type": ··· 9904 9996 cw := cbg.NewCborWriter(w) 9905 9997 fieldCount := 3 9906 9998 9999 + if t.Repo == nil { 10000 + fieldCount-- 10001 + } 10002 + 9907 10003 if t.RepoDid == nil { 9908 10004 fieldCount-- 9909 10005 } ··· 9913 10009 } 9914 10010 9915 10011 // t.Repo (string) (string) 9916 - if len("repo") > 1000000 { 9917 - return xerrors.Errorf("Value in field \"repo\" was too long") 9918 - } 10012 + if t.Repo != nil { 9919 10013 9920 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 9921 - return err 9922 - } 9923 - if _, err := cw.WriteString(string("repo")); err != nil { 9924 - return err 9925 - } 10014 + if len("repo") > 1000000 { 10015 + return xerrors.Errorf("Value in field \"repo\" was too long") 10016 + } 10017 + 10018 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 10019 + return err 10020 + } 10021 + if _, err := cw.WriteString(string("repo")); err != nil { 10022 + return err 10023 + } 9926 10024 9927 - if len(t.Repo) > 1000000 { 9928 - return xerrors.Errorf("Value in field t.Repo was too long") 9929 - } 10025 + if t.Repo == nil { 10026 + if _, err := cw.Write(cbg.CborNull); err != nil { 10027 + return err 10028 + } 10029 + } else { 10030 + if len(*t.Repo) > 1000000 { 10031 + return xerrors.Errorf("Value in field t.Repo was too long") 10032 + } 9930 10033 9931 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 9932 - return err 9933 - } 9934 - if _, err := cw.WriteString(string(t.Repo)); err != nil { 9935 - return err 10034 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 10035 + return err 10036 + } 10037 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 10038 + return err 10039 + } 10040 + } 9936 10041 } 9937 10042 9938 10043 // t.Branch (string) (string) ··· 10037 10142 case "repo": 10038 10143 10039 10144 { 10040 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 10145 + b, err := cr.ReadByte() 10041 10146 if err != nil { 10042 10147 return err 10043 10148 } 10149 + if b != cbg.CborNull[0] { 10150 + if err := cr.UnreadByte(); err != nil { 10151 + return err 10152 + } 10044 10153 10045 - t.Repo = string(sval) 10154 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 10155 + if err != nil { 10156 + return err 10157 + } 10158 + 10159 + t.Repo = (*string)(&sval) 10160 + } 10046 10161 } 10047 10162 // t.Branch (string) (string) 10048 10163 case "branch":
+5 -1
api/tangled/feedreaction.go
··· 4 4 5 5 // schema: sh.tangled.feed.reaction 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 FeedReactionNSID = "sh.tangled.feed.reaction" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.feed.reaction", &FeedReaction{}) 16 + util.RegisterType("sh.tangled.feed.reaction", &FeedReaction{}) 13 17 } // 14 18 // RECORDTYPE: FeedReaction 15 19 type FeedReaction struct {
+6 -2
api/tangled/feedstar.go
··· 4 4 5 5 // schema: sh.tangled.feed.star 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 FeedStarNSID = "sh.tangled.feed.star" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.feed.star", &FeedStar{}) 16 + util.RegisterType("sh.tangled.feed.star", &FeedStar{}) 13 17 } // 14 18 // RECORDTYPE: FeedStar 15 19 type FeedStar struct { 16 20 LexiconTypeID string `json:"$type,const=sh.tangled.feed.star" cborgen:"$type,const=sh.tangled.feed.star"` 17 21 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 18 - Subject string `json:"subject" cborgen:"subject"` 22 + Subject *string `json:"subject,omitempty" cborgen:"subject,omitempty"` 19 23 SubjectDid *string `json:"subjectDid,omitempty" cborgen:"subjectDid,omitempty"` 20 24 }
+5 -1
api/tangled/gitrefUpdate.go
··· 4 4 5 5 // schema: sh.tangled.git.refUpdate 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 GitRefUpdateNSID = "sh.tangled.git.refUpdate" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.git.refUpdate", &GitRefUpdate{}) 16 + util.RegisterType("sh.tangled.git.refUpdate", &GitRefUpdate{}) 13 17 } // 14 18 // RECORDTYPE: GitRefUpdate 15 19 type GitRefUpdate struct {
+5 -1
api/tangled/graphfollow.go
··· 4 4 5 5 // schema: sh.tangled.graph.follow 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 GraphFollowNSID = "sh.tangled.graph.follow" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.graph.follow", &GraphFollow{}) 16 + util.RegisterType("sh.tangled.graph.follow", &GraphFollow{}) 13 17 } // 14 18 // RECORDTYPE: GraphFollow 15 19 type GraphFollow struct {
+5 -1
api/tangled/issuecomment.go
··· 4 4 5 5 // schema: sh.tangled.repo.issue.comment 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 RepoIssueCommentNSID = "sh.tangled.repo.issue.comment" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.repo.issue.comment", &RepoIssueComment{}) 16 + util.RegisterType("sh.tangled.repo.issue.comment", &RepoIssueComment{}) 13 17 } // 14 18 // RECORDTYPE: RepoIssueComment 15 19 type RepoIssueComment struct {
+5 -1
api/tangled/issuestate.go
··· 4 4 5 5 // schema: sh.tangled.repo.issue.state 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 RepoIssueStateNSID = "sh.tangled.repo.issue.state" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.repo.issue.state", &RepoIssueState{}) 16 + util.RegisterType("sh.tangled.repo.issue.state", &RepoIssueState{}) 13 17 } // 14 18 // RECORDTYPE: RepoIssueState 15 19 type RepoIssueState struct {
+5 -1
api/tangled/knotmember.go
··· 4 4 5 5 // schema: sh.tangled.knot.member 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 KnotMemberNSID = "sh.tangled.knot.member" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.knot.member", &KnotMember{}) 16 + util.RegisterType("sh.tangled.knot.member", &KnotMember{}) 13 17 } // 14 18 // RECORDTYPE: KnotMember 15 19 type KnotMember struct {
+5 -1
api/tangled/labeldefinition.go
··· 4 4 5 5 // schema: sh.tangled.label.definition 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 LabelDefinitionNSID = "sh.tangled.label.definition" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.label.definition", &LabelDefinition{}) 16 + util.RegisterType("sh.tangled.label.definition", &LabelDefinition{}) 13 17 } // 14 18 // RECORDTYPE: LabelDefinition 15 19 type LabelDefinition struct {
+5 -1
api/tangled/labelop.go
··· 4 4 5 5 // schema: sh.tangled.label.op 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 LabelOpNSID = "sh.tangled.label.op" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.label.op", &LabelOp{}) 16 + util.RegisterType("sh.tangled.label.op", &LabelOp{}) 13 17 } // 14 18 // RECORDTYPE: LabelOp 15 19 type LabelOp struct {
+5 -1
api/tangled/pipelinestatus.go
··· 4 4 5 5 // schema: sh.tangled.pipeline.status 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 PipelineStatusNSID = "sh.tangled.pipeline.status" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.pipeline.status", &PipelineStatus{}) 16 + util.RegisterType("sh.tangled.pipeline.status", &PipelineStatus{}) 13 17 } // 14 18 // RECORDTYPE: PipelineStatus 15 19 type PipelineStatus struct {
+5 -1
api/tangled/pullcomment.go
··· 4 4 5 5 // schema: sh.tangled.repo.pull.comment 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 RepoPullCommentNSID = "sh.tangled.repo.pull.comment" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.repo.pull.comment", &RepoPullComment{}) 16 + util.RegisterType("sh.tangled.repo.pull.comment", &RepoPullComment{}) 13 17 } // 14 18 // RECORDTYPE: RepoPullComment 15 19 type RepoPullComment struct {
+5 -1
api/tangled/pullstatus.go
··· 4 4 5 5 // schema: sh.tangled.repo.pull.status 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 RepoPullStatusNSID = "sh.tangled.repo.pull.status" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.repo.pull.status", &RepoPullStatus{}) 16 + util.RegisterType("sh.tangled.repo.pull.status", &RepoPullStatus{}) 13 17 } // 14 18 // RECORDTYPE: RepoPullStatus 15 19 type RepoPullStatus struct {
+2 -2
api/tangled/repoartifact.go
··· 13 13 ) 14 14 15 15 func init() { 16 - // util.RegisterType("sh.tangled.repo.artifact", &RepoArtifact{}) 16 + util.RegisterType("sh.tangled.repo.artifact", &RepoArtifact{}) 17 17 } // 18 18 // RECORDTYPE: RepoArtifact 19 19 type RepoArtifact struct { ··· 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 29 RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 30 30 // tag: hash of the tag object that this artifact is attached to (only annotated tags are supported) 31 31 Tag util.LexBytes `json:"tag,omitempty" cborgen:"tag,omitempty"`
+6 -2
api/tangled/repocollaborator.go
··· 4 4 5 5 // schema: sh.tangled.repo.collaborator 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 RepoCollaboratorNSID = "sh.tangled.repo.collaborator" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.repo.collaborator", &RepoCollaborator{}) 16 + util.RegisterType("sh.tangled.repo.collaborator", &RepoCollaborator{}) 13 17 } // 14 18 // RECORDTYPE: RepoCollaborator 15 19 type RepoCollaborator struct { 16 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.collaborator" cborgen:"$type,const=sh.tangled.repo.collaborator"` 17 21 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 18 22 // repo: repo to add this user to 19 - Repo string `json:"repo" cborgen:"repo"` 23 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 20 24 RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 21 25 Subject string `json:"subject" cborgen:"subject"` 22 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 }
+6 -2
api/tangled/repoissue.go
··· 4 4 5 5 // schema: sh.tangled.repo.issue 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 RepoIssueNSID = "sh.tangled.repo.issue" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.repo.issue", &RepoIssue{}) 16 + util.RegisterType("sh.tangled.repo.issue", &RepoIssue{}) 13 17 } // 14 18 // RECORDTYPE: RepoIssue 15 19 type RepoIssue struct { ··· 18 22 CreatedAt string `json:"createdAt" cborgen:"createdAt"` 19 23 Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 20 24 References []string `json:"references,omitempty" cborgen:"references,omitempty"` 21 - Repo string `json:"repo" cborgen:"repo"` 25 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 22 26 RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 23 27 Title string `json:"title" cborgen:"title"` 24 28 }
+2 -2
api/tangled/repopull.go
··· 13 13 ) 14 14 15 15 func init() { 16 - // util.RegisterType("sh.tangled.repo.pull", &RepoPull{}) 16 + util.RegisterType("sh.tangled.repo.pull", &RepoPull{}) 17 17 } // 18 18 // RECORDTYPE: RepoPull 19 19 type RepoPull struct { ··· 42 42 // RepoPull_Target is a "target" in the sh.tangled.repo.pull schema. 43 43 type RepoPull_Target struct { 44 44 Branch string `json:"branch" cborgen:"branch"` 45 - Repo string `json:"repo" cborgen:"repo"` 45 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 46 46 RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 47 47 }
+5 -1
api/tangled/spindlemember.go
··· 4 4 5 5 // schema: sh.tangled.spindle.member 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 SpindleMemberNSID = "sh.tangled.spindle.member" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.spindle.member", &SpindleMember{}) 16 + util.RegisterType("sh.tangled.spindle.member", &SpindleMember{}) 13 17 } // 14 18 // RECORDTYPE: SpindleMember 15 19 type SpindleMember struct {
+5 -1
api/tangled/tangledknot.go
··· 4 4 5 5 // schema: sh.tangled.knot 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 KnotNSID = "sh.tangled.knot" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.knot", &Knot{}) 16 + util.RegisterType("sh.tangled.knot", &Knot{}) 13 17 } // 14 18 // RECORDTYPE: Knot 15 19 type Knot struct {
+5 -1
api/tangled/tangledpipeline.go
··· 4 4 5 5 // schema: sh.tangled.pipeline 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 PipelineNSID = "sh.tangled.pipeline" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.pipeline", &Pipeline{}) 16 + util.RegisterType("sh.tangled.pipeline", &Pipeline{}) 13 17 } // 14 18 // RECORDTYPE: Pipeline 15 19 type Pipeline struct {
+5 -1
api/tangled/tangledpublicKey.go
··· 4 4 5 5 // schema: sh.tangled.publicKey 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 PublicKeyNSID = "sh.tangled.publicKey" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.publicKey", &PublicKey{}) 16 + util.RegisterType("sh.tangled.publicKey", &PublicKey{}) 13 17 } // 14 18 // RECORDTYPE: PublicKey 15 19 type PublicKey struct {
+7 -2
api/tangled/tangledrepo.go
··· 4 4 5 5 // schema: sh.tangled.repo 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 RepoNSID = "sh.tangled.repo" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.repo", &Repo{}) 16 + util.RegisterType("sh.tangled.repo", &Repo{}) 13 17 } // 14 18 // RECORDTYPE: Repo 15 19 type Repo struct { ··· 21 25 // labels: List of labels that this repo subscribes to 22 26 Labels []string `json:"labels,omitempty" cborgen:"labels,omitempty"` 23 27 // name: name of the repo 24 - Name string `json:"name" cborgen:"name"` 28 + Name string `json:"name" cborgen:"name"` 29 + // repoDid: DID of the repo itself, if assigned 25 30 RepoDid *string `json:"repoDid,omitempty" cborgen:"repoDid,omitempty"` 26 31 // source: source of the repo 27 32 Source *string `json:"source,omitempty" cborgen:"source,omitempty"`
+5 -1
api/tangled/tangledspindle.go
··· 4 4 5 5 // schema: sh.tangled.spindle 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 SpindleNSID = "sh.tangled.spindle" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.spindle", &Spindle{}) 16 + util.RegisterType("sh.tangled.spindle", &Spindle{}) 13 17 } // 14 18 // RECORDTYPE: Spindle 15 19 type Spindle struct {
+5 -1
api/tangled/tangledstring.go
··· 4 4 5 5 // schema: sh.tangled.string 6 6 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 7 11 const ( 8 12 StringNSID = "sh.tangled.string" 9 13 ) 10 14 11 15 func init() { 12 - // util.RegisterType("sh.tangled.string", &String{}) 16 + util.RegisterType("sh.tangled.string", &String{}) 13 17 } // 14 18 // RECORDTYPE: String 15 19 type String struct {
+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 }
+202
appview/db/db.go
··· 1371 1371 return err 1372 1372 }) 1373 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 + 1374 1576 return &DB{ 1375 1577 db, 1376 1578 logger,
+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
+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,
+61 -4
appview/db/repos.go
··· 218 218 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 219 219 } 220 220 221 + var repoDids []any 222 + repoDidToAt := make(map[string]syntax.ATURI) 223 + for atUri, r := range repoMap { 224 + if r.RepoDid != "" { 225 + repoDids = append(repoDids, r.RepoDid) 226 + repoDidToAt[r.RepoDid] = atUri 227 + } 228 + } 229 + if len(repoDids) > 0 { 230 + didInClause := strings.TrimSuffix(strings.Repeat("?, ", len(repoDids)), ", ") 231 + didStarQuery := fmt.Sprintf( 232 + `select subject_did, count(1) 233 + from stars 234 + where subject_did in (%s) and (subject_at is null or subject_at not in (%s)) 235 + group by subject_did`, 236 + didInClause, inClause, 237 + ) 238 + didStarArgs := append(repoDids, args...) 239 + rows, err = e.Query(didStarQuery, didStarArgs...) 240 + if err != nil { 241 + return nil, fmt.Errorf("failed to execute did-star-count query: %w ", err) 242 + } 243 + defer rows.Close() 244 + 245 + for rows.Next() { 246 + var subjectDid string 247 + var count int 248 + if err := rows.Scan(&subjectDid, &count); err != nil { 249 + log.Println("err", "err", err) 250 + continue 251 + } 252 + if atUri, ok := repoDidToAt[subjectDid]; ok { 253 + if r, ok := repoMap[atUri]; ok { 254 + r.RepoStats.StarCount += count 255 + } 256 + } 257 + } 258 + if err = rows.Err(); err != nil { 259 + return nil, fmt.Errorf("failed to execute did-star-count query: %w ", err) 260 + } 261 + } 262 + 221 263 issueCountQuery := fmt.Sprintf( 222 264 `select 223 265 repo_at, ··· 358 400 var nullableWebsite sql.NullString 359 401 var nullableTopicStr sql.NullString 360 402 var nullableRepoDid sql.NullString 403 + var nullableSource sql.NullString 404 + var nullableSpindle sql.NullString 361 405 362 - row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, repo_did from repos where at_uri = ?`, atUri) 406 + row := e.QueryRow(`select id, did, name, knot, created, rkey, description, website, topics, source, spindle, repo_did from repos where at_uri = ?`, atUri) 363 407 364 408 var createdAt string 365 - if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableRepoDid); err != nil { 409 + if err := row.Scan(&repo.Id, &repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.Rkey, &nullableDescription, &nullableWebsite, &nullableTopicStr, &nullableSource, &nullableSpindle, &nullableRepoDid); err != nil { 366 410 return nil, err 367 411 } 368 412 createdAtTime, _ := time.Parse(time.RFC3339, createdAt) ··· 377 421 if nullableTopicStr.Valid { 378 422 repo.Topics = strings.Fields(nullableTopicStr.String) 379 423 } 424 + if nullableSource.Valid { 425 + repo.Source = nullableSource.String 426 + } 427 + if nullableSpindle.Valid { 428 + repo.Spindle = nullableSpindle.String 429 + } 380 430 if nullableRepoDid.Valid { 381 431 repo.RepoDid = nullableRepoDid.String 382 432 } ··· 448 498 } 449 499 if err != nil { 450 500 return nil, err 501 + } 502 + if strings.HasPrefix(source, "did:") { 503 + return GetRepoByDid(e, source) 451 504 } 452 505 return GetRepoByAtUri(e, source) 453 506 } ··· 634 687 whereClause = " where " + strings.Join(conditions, " and ") 635 688 } 636 689 637 - query := fmt.Sprintf(`select id, repo_at, label_at from repo_labels %s`, whereClause) 690 + query := fmt.Sprintf(`select id, repo_at, label_at, repo_did from repo_labels %s`, whereClause) 638 691 639 692 rows, err := e.Query(query, args...) 640 693 if err != nil { ··· 645 698 var labels []models.RepoLabel 646 699 for rows.Next() { 647 700 var label models.RepoLabel 701 + var labelRepoDid sql.NullString 648 702 649 - err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt) 703 + err := rows.Scan(&label.Id, &label.RepoAt, &label.LabelAt, &labelRepoDid) 650 704 if err != nil { 651 705 return nil, err 706 + } 707 + if labelRepoDid.Valid { 708 + label.RepoDid = labelRepoDid.String 652 709 } 653 710 654 711 labels = append(labels, label)
+11
appview/db/star.go
··· 82 82 return stars, nil 83 83 } 84 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 + 85 96 // getStarStatuses returns a map of repo URIs to star status for a given user 86 97 // This is an internal helper function to avoid N+1 queries 87 98 func getStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) {
+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)
+39 -24
appview/ingester.go
··· 118 118 return err 119 119 } 120 120 121 - subjectUri, err = syntax.ParseATURI(record.Subject) 122 - if err != nil { 123 - l.Error("invalid record", "err", err) 124 - return err 125 - } 126 121 star := &models.Star{ 127 - Did: did, 128 - RepoAt: subjectUri, 129 - Rkey: e.Commit.RKey, 122 + Did: did, 123 + Rkey: e.Commit.RKey, 130 124 } 131 - if record.SubjectDid != nil { 125 + 126 + switch { 127 + case record.SubjectDid != nil: 132 128 star.SubjectDid = *record.SubjectDid 133 - } 134 - if star.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 135 141 repo, repoErr := db.GetRepoByAtUri(i.Db, subjectUri.String()) 136 142 if repoErr == nil && repo.RepoDid != "" { 137 143 star.SubjectDid = repo.RepoDid 138 - if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.FeedStarNSID, e.Commit.RKey, record.Subject); enqErr != nil { 144 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.FeedStarNSID, e.Commit.RKey, *record.Subject); enqErr != nil { 139 145 l.Warn("failed to enqueue PDS rewrite for star", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 140 146 } 141 147 } 148 + default: 149 + l.Error("star record has neither subject nor subjectDid") 150 + return fmt.Errorf("star record has neither subject nor subjectDid") 142 151 } 143 152 err = db.AddStar(i.Db, star) 144 153 case jmodels.CommitOperationDelete: ··· 235 244 return err 236 245 } 237 246 238 - repoAt, err := syntax.ParseATURI(record.Repo) 239 - if err != nil { 240 - return err 241 - } 242 - 243 247 var repo *models.Repo 244 248 if record.RepoDid != nil && *record.RepoDid != "" { 245 249 repo, err = db.GetRepoByDid(i.Db, *record.RepoDid) ··· 247 251 return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, err) 248 252 } 249 253 } 250 - if repo == nil { 254 + if repo == nil && record.Repo != nil { 255 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 256 + if parseErr != nil { 257 + return parseErr 258 + } 251 259 repo, err = db.GetRepoByAtUri(i.Db, repoAt.String()) 252 260 if err != nil { 253 261 return err 254 262 } 263 + } 264 + if repo == nil { 265 + return fmt.Errorf("artifact record has neither valid repoDid nor repo field") 255 266 } 256 267 257 268 ok, err := i.Enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") ··· 263 274 if repoDid == "" && record.RepoDid != nil { 264 275 repoDid = *record.RepoDid 265 276 } 266 - if repoDid != "" && (record.RepoDid == nil || *record.RepoDid == "") { 267 - if enqErr := db.EnqueuePdsRewrite(i.Db, did, repoDid, tangled.RepoArtifactNSID, e.Commit.RKey, record.Repo); enqErr != nil { 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 { 268 279 l.Warn("failed to enqueue PDS rewrite for artifact", "err", enqErr, "did", did, "repoDid", repoDid) 269 280 } 270 281 } ··· 277 288 artifact := models.Artifact{ 278 289 Did: did, 279 290 Rkey: e.Commit.RKey, 280 - RepoAt: repoAt, 291 + RepoAt: repo.RepoAt(), 281 292 RepoDid: repoDid, 282 293 Tag: plumbing.Hash(record.Tag), 283 294 CreatedAt: createdAt, ··· 857 868 858 869 issue := models.IssueFromRecord(did, rkey, record) 859 870 871 + if issue.RepoDid == "" && issue.RepoAt == "" { 872 + return fmt.Errorf("issue record has neither repo nor repoDid") 873 + } 874 + 860 875 if err := i.Validator.ValidateIssue(&issue); err != nil { 861 876 return fmt.Errorf("failed to validate issue: %w", err) 862 877 } 863 878 864 - if issue.RepoDid == "" { 865 - repo, repoErr := db.GetRepoByAtUri(i.Db, record.Repo) 879 + if issue.RepoDid == "" && record.Repo != nil { 880 + repo, repoErr := db.GetRepoByAtUri(i.Db, *record.Repo) 866 881 if repoErr == nil && repo.RepoDid != "" { 867 882 issue.RepoDid = repo.RepoDid 868 - if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.RepoIssueNSID, rkey, record.Repo); enqErr != nil { 883 + if enqErr := db.EnqueuePdsRewrite(i.Db, did, repo.RepoDid, tangled.RepoIssueNSID, rkey, *record.Repo); enqErr != nil { 869 884 l.Warn("failed to enqueue PDS rewrite for issue", "err", enqErr, "did", did, "repoDid", repo.RepoDid) 870 885 } 871 886 }
+1
appview/issues/issues.go
··· 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,
+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
+16 -2
appview/models/issue.go
··· 45 45 for i, uri := range i.References { 46 46 references[i] = string(uri) 47 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 + } 48 56 return tangled.RepoIssue{ 49 - Repo: i.RepoAt.String(), 57 + Repo: repo, 58 + RepoDid: repoDid, 50 59 Title: i.Title, 51 60 Body: &i.Body, 52 61 Mentions: mentions, ··· 162 171 body = *record.Body 163 172 } 164 173 174 + var repoAt syntax.ATURI 175 + if record.Repo != nil { 176 + repoAt = syntax.ATURI(*record.Repo) 177 + } 178 + 165 179 repoDid := "" 166 180 if record.RepoDid != nil { 167 181 repoDid = *record.RepoDid 168 182 } 169 183 170 184 return Issue{ 171 - RepoAt: syntax.ATURI(record.Repo), 185 + RepoAt: repoAt, 172 186 RepoDid: repoDid, 173 187 Did: did, 174 188 Rkey: rkey,
+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
+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
+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
+24 -8
appview/pulls/pulls.go
··· 863 863 comment := &models.PullComment{ 864 864 OwnerDid: user.Active.Did, 865 865 RepoAt: f.RepoAt().String(), 866 + RepoDid: f.RepoDid, 866 867 PullId: pull.PullId, 867 868 Body: body, 868 869 CommentAt: atResp.Uri, ··· 1223 1224 forkAtUriStr := forkAtUri.String() 1224 1225 1225 1226 pullSource := &models.PullSource{ 1226 - Branch: sourceBranch, 1227 - RepoAt: &forkAtUri, 1227 + Branch: sourceBranch, 1228 + RepoAt: &forkAtUri, 1229 + RepoDid: fork.RepoDid, 1228 1230 } 1229 1231 recordPullSource := &tangled.RepoPull_Source{ 1230 1232 Branch: sourceBranch, 1231 - Repo: &forkAtUriStr, 1232 1233 Sha: sourceRev, 1234 + } 1235 + if fork.RepoDid != "" { 1236 + recordPullSource.RepoDid = &fork.RepoDid 1237 + } else { 1238 + recordPullSource.Repo = &forkAtUriStr 1233 1239 } 1234 1240 1235 1241 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) ··· 1313 1319 TargetBranch: targetBranch, 1314 1320 OwnerDid: user.Active.Did, 1315 1321 RepoAt: repo.RepoAt(), 1322 + RepoDid: repo.RepoDid, 1316 1323 Rkey: rkey, 1317 1324 Mentions: mentions, 1318 1325 References: references, ··· 1347 1354 Rkey: rkey, 1348 1355 Record: &lexutil.LexiconTypeDecoder{ 1349 1356 Val: &tangled.RepoPull{ 1350 - Title: title, 1351 - Target: &tangled.RepoPull_Target{ 1352 - Repo: string(repo.RepoAt()), 1353 - Branch: targetBranch, 1354 - }, 1357 + Title: title, 1358 + Target: repoPullTarget(repo, targetBranch), 1355 1359 PatchBlob: blob.Blob, 1356 1360 Source: recordPullSource, 1357 1361 CreatedAt: time.Now().Format(time.RFC3339), ··· 2528 2532 TargetBranch: targetBranch, 2529 2533 OwnerDid: user.Active.Did, 2530 2534 RepoAt: repo.RepoAt(), 2535 + RepoDid: repo.RepoDid, 2531 2536 Rkey: rkey, 2532 2537 Mentions: mentions, 2533 2538 References: references, ··· 2559 2564 } 2560 2565 2561 2566 func ptrPullState(s models.PullState) *models.PullState { return &s } 2567 + 2568 + func repoPullTarget(repo *models.Repo, branch string) *tangled.RepoPull_Target { 2569 + target := &tangled.RepoPull_Target{Branch: branch} 2570 + if repo.RepoDid != "" { 2571 + target.RepoDid = &repo.RepoDid 2572 + } else { 2573 + s := string(repo.RepoAt()) 2574 + target.Repo = &s 2575 + } 2576 + return target 2577 + }
+18 -7
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), ··· 358 353 359 354 return tag, nil 360 355 } 356 + 357 + func repoArtifactRecord(f *models.Repo, blob *lexutil.LexBlob, createdAt time.Time, name string, tag []byte) *tangled.RepoArtifact { 358 + rec := &tangled.RepoArtifact{ 359 + Artifact: blob, 360 + CreatedAt: createdAt.Format(time.RFC3339), 361 + Name: name, 362 + Tag: tag, 363 + } 364 + if f.RepoDid != "" { 365 + rec.RepoDid = &f.RepoDid 366 + } else { 367 + s := f.RepoAt().String() 368 + rec.Repo = &s 369 + } 370 + return rec 371 + }
+1
appview/repo/index.go
··· 199 199 for _, lang := range ls.Languages { 200 200 langs = append(langs, models.RepoLanguage{ 201 201 RepoAt: repo.RepoAt(), 202 + RepoDid: repo.RepoDid, 202 203 Ref: currentRef, 203 204 IsDefaultRef: isDefaultRef, 204 205 Language: lang.Name,
+121 -50
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 - }) 316 + }); err != nil { 317 + fail("Failed to subscribe to label.", err) 318 + return 319 + } 317 320 318 321 err = tx.Commit() 319 322 if err != nil { ··· 748 751 Repo: currentUser.Active.Did, 749 752 Rkey: rkey, 750 753 Record: &lexutil.LexiconTypeDecoder{ 751 - Val: &tangled.RepoCollaborator{ 752 - Subject: collaboratorIdent.DID.String(), 753 - Repo: string(f.RepoAt()), 754 - CreatedAt: createdAt.Format(time.RFC3339), 755 - }}, 754 + Val: repoCollaboratorRecord(f, collaboratorIdent.DID.String(), createdAt), 755 + }, 756 756 }) 757 757 // invalid record 758 758 if err != nil { ··· 798 798 Rkey: rkey, 799 799 SubjectDid: collaboratorIdent.DID, 800 800 RepoAt: f.RepoAt(), 801 + RepoDid: f.RepoDid, 801 802 Created: createdAt, 802 803 }) 803 804 if err != nil { ··· 1063 1064 forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Name) 1064 1065 l = l.With("cloneUrl", forkSourceUrl) 1065 1066 1066 - sourceAt := f.RepoAt().String() 1067 + rkey := tid.TID() 1068 + 1069 + // TODO: this could coordinate better with the knot to recieve a clone status 1070 + client, err := rp.oauth.ServiceClient( 1071 + r, 1072 + oauth.WithService(targetKnot), 1073 + oauth.WithLxm(tangled.RepoCreateNSID), 1074 + oauth.WithDev(rp.config.Core.Dev), 1075 + oauth.WithTimeout(time.Second*20), 1076 + ) 1077 + if err != nil { 1078 + l.Error("could not create service client", "err", err) 1079 + rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1080 + return 1081 + } 1067 1082 1068 - // create an atproto record for this fork 1069 - rkey := tid.TID() 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 + 1070 1117 repo := &models.Repo{ 1071 1118 Did: user.Active.Did, 1072 1119 Name: forkName, 1073 1120 Knot: targetKnot, 1074 1121 Rkey: rkey, 1075 - Source: sourceAt, 1122 + Source: forkSource, 1076 1123 Description: f.Description, 1077 1124 Created: time.Now(), 1078 1125 Labels: rp.config.Label.DefaultLabelDefs, 1126 + RepoDid: repoDid, 1079 1127 } 1080 1128 record := repo.AsRecord() 1081 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 + 1082 1164 atpClient, err := rp.oauth.AuthorizedClient(r) 1083 1165 if err != nil { 1084 1166 l.Error("failed to create xrpcclient", "err", err) 1167 + cleanupKnot() 1085 1168 rp.pages.Notice(w, "repo", "Failed to fork repository.") 1086 1169 return 1087 1170 } ··· 1096 1179 }) 1097 1180 if err != nil { 1098 1181 l.Error("failed to write to PDS", "err", err) 1182 + cleanupKnot() 1099 1183 rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1100 1184 return 1101 1185 } ··· 1111 1195 return 1112 1196 } 1113 1197 1114 - // The rollback function reverts a few things on failure: 1115 - // - the pending txn 1116 - // - the ACLs 1117 - // - the atproto record created 1118 1198 rollback := func() { 1119 1199 err1 := tx.Rollback() 1120 1200 err2 := rp.enforcer.E.LoadPolicy() 1121 1201 err3 := rollbackRecord(context.Background(), aturi, atpClient) 1122 1202 1123 - // ignore txn complete errors, this is okay 1124 1203 if errors.Is(err1, sql.ErrTxDone) { 1125 1204 err1 = nil 1126 1205 } 1127 1206 1128 1207 if errs := errors.Join(err1, err2, err3); errs != nil { 1129 1208 l.Error("failed to rollback changes", "errs", errs) 1130 - return 1209 + } 1210 + 1211 + if aturi != "" { 1212 + cleanupKnot() 1131 1213 } 1132 1214 } 1133 1215 defer rollback() 1134 1216 1135 - // TODO: this could coordinate better with the knot to recieve a clone status 1136 - client, err := rp.oauth.ServiceClient( 1137 - r, 1138 - oauth.WithService(targetKnot), 1139 - oauth.WithLxm(tangled.RepoCreateNSID), 1140 - oauth.WithDev(rp.config.Core.Dev), 1141 - oauth.WithTimeout(time.Second*20), // big repos take time to clone 1142 - ) 1143 - if err != nil { 1144 - l.Error("could not create service client", "err", err) 1145 - rp.pages.Notice(w, "repo", "Failed to connect to knot server.") 1146 - return 1147 - } 1148 - 1149 - err = tangled.RepoCreate( 1150 - r.Context(), 1151 - client, 1152 - &tangled.RepoCreate_Input{ 1153 - Rkey: rkey, 1154 - Source: &forkSourceUrl, 1155 - }, 1156 - ) 1157 - if err := xrpcclient.HandleXrpcErr(err); err != nil { 1158 - rp.pages.Notice(w, "repo", err.Error()) 1159 - return 1160 - } 1161 - 1162 1217 err = db.AddRepo(tx, repo) 1163 1218 if err != nil { 1164 1219 l.Error("failed to AddRepo", "err", err) ··· 1166 1221 return 1167 1222 } 1168 1223 1169 - // acls 1170 - p, _ := securejoin.SecureJoin(user.Active.Did, forkName) 1171 - err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p) 1224 + rbacPath := repo.DidSlashRepo() 1225 + err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, rbacPath) 1172 1226 if err != nil { 1173 1227 l.Error("failed to add ACLs", "err", err) 1174 1228 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1189 1243 return 1190 1244 } 1191 1245 1192 - // reset the ATURI because the transaction completed successfully 1193 1246 aturi = "" 1194 1247 1195 1248 rp.notifier.NewRepo(r.Context(), repo) 1196 - 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 + } 1197 1254 } 1198 1255 } 1199 1256 ··· 1218 1275 }) 1219 1276 return err 1220 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 + }
+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()
+8 -2
appview/reporesolver/resolver.go
··· 79 79 80 80 stats := repo.RepoStats 81 81 if stats == nil { 82 - starCount, err := db.GetStarCount(rr.execer, repoAt) 83 - 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 { 84 90 log.Println("failed to get star count for ", repoAt) 85 91 } 86 92 issueCount, err := db.GetIssueCount(rr.execer, repoAt)
+10 -4
appview/state/knotstream.go
··· 87 87 return err 88 88 } 89 89 90 - // backward compat: old knots emit owner DID as "repoDid" with no "ownerDid" field 90 + // backward compat: old knots emit owner DID as "repoDid" with no "ownerDid" field. 91 + // only apply this fixup if the DID is NOT a known repo DID in our DB. 91 92 if record.OwnerDid == "" && record.RepoDid != nil && *record.RepoDid != "" { 92 - record.OwnerDid = *record.RepoDid 93 - record.RepoDid = nil 93 + _, repoLookupErr := db.GetRepoByDid(d, *record.RepoDid) 94 + if repoLookupErr != nil { 95 + record.OwnerDid = *record.RepoDid 96 + record.RepoDid = nil 97 + } 94 98 } 95 99 96 100 knownKnots, err := enforcer.GetKnotsForUser(record.CommitterDid) ··· 118 122 case lookupErr == nil: 119 123 repos = []models.Repo{*repo} 120 124 case !errors.Is(lookupErr, sql.ErrNoRows): 121 - logger.Warn("failed to look up repo by DID", "err", lookupErr, "repoDid", *record.RepoDid) 125 + return fmt.Errorf("failed to look up repo by DID %s: %w", *record.RepoDid, lookupErr) 122 126 } 123 127 } 124 128 ··· 233 237 234 238 langs = append(langs, models.RepoLanguage{ 235 239 RepoAt: repo.RepoAt(), 240 + RepoDid: repo.RepoDid, 236 241 Ref: ref.Short(), 237 242 IsDefaultRef: record.Meta.IsDefaultRef, 238 243 Language: l.Lang, ··· 336 341 Knot: source.Key(), 337 342 RepoOwner: syntax.DID(record.TriggerMetadata.Repo.Did), 338 343 RepoName: record.TriggerMetadata.Repo.Repo, 344 + RepoDid: repos[0].RepoDid, 339 345 TriggerId: int(triggerId), 340 346 Sha: sha, 341 347 }
+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.DidSlashRepo() 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
+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 - golang.org/x/crypto v0.40.0 52 + golang.org/x/crypto v0.48.0 52 53 golang.org/x/image v0.31.0 53 - golang.org/x/net v0.42.0 54 + golang.org/x/net v0.50.0 54 55 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 55 56 gopkg.in/yaml.v3 v3.0.1 56 57 ) ··· 115 116 github.com/go-test/deep v1.1.1 // indirect 116 117 github.com/goccy/go-json v0.10.5 // indirect 117 118 github.com/gogo/protobuf v1.3.2 // indirect 118 - github.com/golang-jwt/jwt/v5 v5.2.3 // indirect 119 + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect 119 120 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 120 121 github.com/golang/mock v1.6.0 // indirect 121 122 github.com/golang/protobuf v1.5.4 // indirect ··· 135 136 github.com/hashicorp/hcl v1.0.1-vault-7 // indirect 136 137 github.com/hexops/gotextdiff v1.0.3 // indirect 137 138 github.com/ipfs/bbloom v0.0.4 // indirect 138 - github.com/ipfs/boxo v0.33.0 // indirect 139 - github.com/ipfs/go-block-format v0.2.2 // indirect 140 - github.com/ipfs/go-datastore v0.8.2 // indirect 139 + github.com/ipfs/boxo v0.36.0 // indirect 140 + github.com/ipfs/go-block-format v0.2.3 // indirect 141 + github.com/ipfs/go-datastore v0.9.0 // indirect 141 142 github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 142 143 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 143 144 github.com/ipfs/go-ipld-cbor v0.2.1 // indirect 144 - github.com/ipfs/go-ipld-format v0.6.2 // indirect 145 + github.com/ipfs/go-ipld-format v0.6.3 // indirect 145 146 github.com/ipfs/go-log v1.0.5 // indirect 146 - github.com/ipfs/go-log/v2 v2.6.0 // indirect 147 + github.com/ipfs/go-log/v2 v2.9.1 // indirect 147 148 github.com/ipfs/go-metrics-interface v0.3.0 // indirect 148 149 github.com/json-iterator/go v1.1.12 // indirect 149 150 github.com/kevinburke/ssh_config v1.2.0 // indirect ··· 167 168 github.com/multiformats/go-base36 v0.2.0 // indirect 168 169 github.com/multiformats/go-multibase v0.2.0 // indirect 169 170 github.com/multiformats/go-multihash v0.2.3 // indirect 170 - github.com/multiformats/go-varint v0.0.7 // indirect 171 + github.com/multiformats/go-varint v0.1.0 // indirect 171 172 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 172 173 github.com/onsi/gomega v1.37.0 // indirect 173 174 github.com/opencontainers/go-digest v1.0.0 // indirect ··· 177 178 github.com/pkg/errors v0.9.1 // indirect 178 179 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 179 180 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 180 - github.com/prometheus/client_golang v1.22.0 // indirect 181 + github.com/prometheus/client_golang v1.23.2 // indirect 181 182 github.com/prometheus/client_model v0.6.2 // indirect 182 - github.com/prometheus/common v0.64.0 // indirect 183 - github.com/prometheus/procfs v0.16.1 // indirect 183 + github.com/prometheus/common v0.67.5 // indirect 184 + github.com/prometheus/procfs v0.19.2 // indirect 184 185 github.com/rivo/uniseg v0.4.7 // indirect 185 186 github.com/ryanuber/go-glob v1.0.0 // indirect 186 187 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect ··· 192 193 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 193 194 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 194 195 go.etcd.io/bbolt v1.4.0 // indirect 195 - go.opentelemetry.io/auto/sdk v1.1.0 // indirect 196 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect 197 - go.opentelemetry.io/otel v1.37.0 // indirect 198 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect 199 - go.opentelemetry.io/otel/metric v1.37.0 // indirect 200 - go.opentelemetry.io/otel/trace v1.37.0 // indirect 201 - go.opentelemetry.io/proto/otlp v1.6.0 // indirect 196 + go.opentelemetry.io/auto/sdk v1.2.1 // indirect 197 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect 198 + go.opentelemetry.io/otel v1.40.0 // indirect 199 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect 200 + go.opentelemetry.io/otel/metric v1.40.0 // indirect 201 + go.opentelemetry.io/otel/trace v1.40.0 // indirect 202 + go.opentelemetry.io/proto/otlp v1.9.0 // indirect 202 203 go.uber.org/atomic v1.11.0 // indirect 203 204 go.uber.org/multierr v1.11.0 // indirect 204 - go.uber.org/zap v1.27.0 // indirect 205 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect 206 - golang.org/x/sync v0.17.0 // indirect 207 - golang.org/x/sys v0.34.0 // indirect 208 - golang.org/x/text v0.29.0 // indirect 205 + go.uber.org/zap v1.27.1 // indirect 206 + go.yaml.in/yaml/v2 v2.4.3 // indirect 207 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect 208 + golang.org/x/sync v0.19.0 // indirect 209 + golang.org/x/sys v0.41.0 // indirect 210 + golang.org/x/text v0.34.0 // indirect 209 211 golang.org/x/time v0.12.0 // indirect 210 - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect 211 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect 212 - google.golang.org/grpc v1.73.0 // indirect 213 - google.golang.org/protobuf v1.36.6 // indirect 212 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect 213 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect 214 + google.golang.org/grpc v1.78.0 // indirect 215 + google.golang.org/protobuf v1.36.11 // indirect 214 216 gopkg.in/fsnotify.v1 v1.4.7 // indirect 215 217 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 216 218 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= ··· 132 134 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 133 135 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 134 136 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 137 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038 h1:AGh+Vn9fXhf9eo8erG1CK4+LACduPo64P1OICQLDv88= 138 + github.com/did-method-plc/go-didplc v0.0.0-20250716171643-635da8b4e038/go.mod h1:ddIXqTTSXWtj5kMsHAPj8SvbIx2GZdAkBFgFa6e6+CM= 135 139 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 136 140 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 137 141 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= ··· 195 199 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 196 200 github.com/goki/freetype v1.0.5 h1:yi2lQeUhXnBgSMqYd0vVmPw6RnnfIeTP3N4uvaJXd7A= 197 201 github.com/goki/freetype v1.0.5/go.mod h1:wKmKxddbzKmeci9K96Wknn5kjTWLyfC8tKOqAFbEX8E= 198 - github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0= 199 - github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 202 + github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 203 + github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 200 204 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 201 205 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 202 206 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= ··· 246 250 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 247 251 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= 248 252 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= 249 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 250 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 253 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= 254 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= 251 255 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 252 256 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 253 257 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= ··· 280 284 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 281 285 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 282 286 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 283 - github.com/ipfs/boxo v0.33.0 h1:9ow3chwkDzMj0Deq4AWRUEI7WnIIV7SZhPTzzG2mmfw= 284 - github.com/ipfs/boxo v0.33.0/go.mod h1:3IPh7YFcCIcKp6o02mCHovrPntoT5Pctj/7j4syh/RM= 285 - github.com/ipfs/go-block-format v0.2.2 h1:uecCTgRwDIXyZPgYspaLXoMiMmxQpSx2aq34eNc4YvQ= 286 - github.com/ipfs/go-block-format v0.2.2/go.mod h1:vmuefuWU6b+9kIU0vZJgpiJt1yicQz9baHXE8qR+KB8= 287 - github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 288 - github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 289 - github.com/ipfs/go-datastore v0.8.2 h1:Jy3wjqQR6sg/LhyY0NIePZC3Vux19nLtg7dx0TVqr6U= 290 - github.com/ipfs/go-datastore v0.8.2/go.mod h1:W+pI1NsUsz3tcsAACMtfC+IZdnQTnC/7VfPoJBQuts0= 287 + github.com/ipfs/boxo v0.36.0 h1:DarrMBM46xCs6GU6Vz+AL8VUyXykqHAqZYx8mR0Oics= 288 + github.com/ipfs/boxo v0.36.0/go.mod h1:92hnRXfP5ScKEIqlq9Ns7LR1dFXEVADKWVGH0fjk83k= 289 + github.com/ipfs/go-block-format v0.2.3 h1:mpCuDaNXJ4wrBJLrtEaGFGXkferrw5eqVvzaHhtFKQk= 290 + github.com/ipfs/go-block-format v0.2.3/go.mod h1:WJaQmPAKhD3LspLixqlqNFxiZ3BZ3xgqxxoSR/76pnA= 291 + github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30= 292 + github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ= 293 + github.com/ipfs/go-datastore v0.9.0 h1:WocriPOayqalEsueHv6SdD4nPVl4rYMfYGLD4bqCZ+w= 294 + github.com/ipfs/go-datastore v0.9.0/go.mod h1:uT77w/XEGrvJWwHgdrMr8bqCN6ZTW9gzmi+3uK+ouHg= 291 295 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 292 296 github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 293 297 github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= ··· 298 302 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 299 303 github.com/ipfs/go-ipld-cbor v0.2.1 h1:H05yEJbK/hxg0uf2AJhyerBDbjOuHX4yi+1U/ogRa7E= 300 304 github.com/ipfs/go-ipld-cbor v0.2.1/go.mod h1:x9Zbeq8CoE5R2WicYgBMcr/9mnkQ0lHddYWJP2sMV3A= 301 - github.com/ipfs/go-ipld-format v0.6.2 h1:bPZQ+A05ol0b3lsJSl0bLvwbuQ+HQbSsdGTy4xtYUkU= 302 - github.com/ipfs/go-ipld-format v0.6.2/go.mod h1:nni2xFdHKx5lxvXJ6brt/pndtGxKAE+FPR1rg4jTkyk= 305 + github.com/ipfs/go-ipld-format v0.6.3 h1:9/lurLDTotJpZSuL++gh3sTdmcFhVkCwsgx2+rAh4j8= 306 + github.com/ipfs/go-ipld-format v0.6.3/go.mod h1:74ilVN12NXVMIV+SrBAyC05UJRk0jVvGqdmrcYZvCBk= 303 307 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 304 308 github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 305 309 github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 306 - github.com/ipfs/go-log/v2 v2.6.0 h1:2Nu1KKQQ2ayonKp4MPo6pXCjqw1ULc9iohRqWV5EYqg= 307 - github.com/ipfs/go-log/v2 v2.6.0/go.mod h1:p+Efr3qaY5YXpx9TX7MoLCSEZX5boSWj9wh86P5HJa8= 310 + github.com/ipfs/go-log/v2 v2.9.1 h1:3JXwHWU31dsCpvQ+7asz6/QsFJHqFr4gLgQ0FWteujk= 311 + github.com/ipfs/go-log/v2 v2.9.1/go.mod h1:evFx7sBiohUN3AG12mXlZBw5hacBQld3ZPHrowlJYoo= 308 312 github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6q/JR9V40TU= 309 313 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= 310 314 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= ··· 336 340 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 337 341 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 338 342 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 339 - github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 340 - github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 343 + github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= 344 + github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 341 345 github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 342 346 github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 343 347 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= ··· 373 377 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 374 378 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 375 379 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 376 - github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 377 - github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 380 + github.com/multiformats/go-varint v0.1.0 h1:i2wqFp4sdl3IcIxfAonHQV9qU5OsZ4Ts9IOoETFs5dI= 381 + github.com/multiformats/go-varint v0.1.0/go.mod h1:5KVAVXegtfmNQQm/lCY+ATvDzvJJhSkUlGQV9wgObdI= 378 382 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 379 383 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 380 384 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= ··· 429 433 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 430 434 github.com/posthog/posthog-go v1.5.5 h1:2o3j7IrHbTIfxRtj4MPaXKeimuTYg49onNzNBZbwksM= 431 435 github.com/posthog/posthog-go v1.5.5/go.mod h1:3RqUmSnPuwmeVj/GYrS75wNGqcAKdpODiwc83xZWgdE= 432 - github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 433 - github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 436 + github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= 437 + github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 434 438 github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 435 439 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 436 - github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= 437 - github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= 438 - github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 439 - github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 440 + github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= 441 + github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= 442 + github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= 443 + github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 440 444 github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= 441 445 github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 442 446 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= ··· 480 484 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 481 485 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 482 486 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 483 - github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 484 - github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 487 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 488 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 485 489 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 486 - github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I= 487 - github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= 490 + github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= 491 + github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= 488 492 github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= 489 493 github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= 490 494 github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= ··· 518 522 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 519 523 go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= 520 524 go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= 521 - go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 522 - go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 523 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU= 524 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY= 525 - go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 526 - go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 527 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= 528 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= 529 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= 530 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= 531 - go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 532 - go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 533 - go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= 534 - go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= 535 - go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= 536 - go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= 537 - go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 538 - go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 539 - go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI= 540 - go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= 525 + go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 526 + go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 527 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= 528 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= 529 + go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= 530 + go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= 531 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= 532 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= 533 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4= 534 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4= 535 + go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= 536 + go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= 537 + go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= 538 + go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= 539 + go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= 540 + go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= 541 + go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= 542 + go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= 543 + go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= 544 + go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= 541 545 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 542 546 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 543 547 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 550 554 go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 551 555 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 552 556 go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 553 - go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 554 - go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 557 + go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= 558 + go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 559 + go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 560 + go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 555 561 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 556 562 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 557 563 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= ··· 559 565 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 560 566 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 561 567 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 562 - golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= 563 - golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= 564 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= 565 - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= 568 + golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= 569 + golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= 570 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= 571 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= 566 572 golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= 567 573 golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= 568 574 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= ··· 595 601 golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 596 602 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 597 603 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 598 - golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 599 - golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 604 + golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= 605 + golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= 600 606 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 601 607 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 602 608 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 604 610 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 605 611 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 606 612 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 607 - golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 608 - golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 613 + golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 614 + golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 609 615 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 610 616 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 611 617 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 636 642 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 637 643 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 638 644 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 639 - golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 640 - golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 645 + golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= 646 + golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 641 647 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 642 648 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 643 649 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= ··· 647 653 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 648 654 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 649 655 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 650 - golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 651 - golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 656 + golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= 657 + golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= 652 658 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 653 659 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 654 660 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= ··· 659 665 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 660 666 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 661 667 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 662 - golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 663 - golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 668 + golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= 669 + golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 664 670 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 665 671 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 666 672 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 686 692 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 687 693 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= 688 694 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= 689 - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= 690 - google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= 691 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= 692 - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 693 - google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= 694 - google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 695 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= 696 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= 697 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= 698 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= 699 + google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= 700 + google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= 695 701 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 696 702 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 697 703 google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= ··· 701 707 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 702 708 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 703 709 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 704 - google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 705 - google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 710 + google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 711 + google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 706 712 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 707 713 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 708 714 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+157 -17
knotserver/db/db.go
··· 95 95 return count > 0 96 96 } 97 97 98 - runMigration := func(name string, fn func() error) { 98 + runMigration := func(name string, fn func() error) error { 99 99 if migrationCheck(name) { 100 - return 100 + return nil 101 101 } 102 102 if err := fn(); err != nil { 103 - logger.Warn("migration failed", "name", name, "err", err) 104 - return 103 + return fmt.Errorf("migration %q failed: %w", name, err) 105 104 } 106 - conn.ExecContext(ctx, `INSERT INTO migrations (name) VALUES (?)`, name) 105 + _, err := conn.ExecContext(ctx, `INSERT INTO migrations (name) VALUES (?)`, name) 106 + if err != nil { 107 + return fmt.Errorf("recording migration %q: %w", name, err) 108 + } 109 + return nil 107 110 } 108 111 109 - runMigration("add-owner-did-to-repo-keys", func() error { 112 + if err := runMigration("add-owner-did-to-repo-keys", func() error { 110 113 _, mErr := conn.ExecContext(ctx, `ALTER TABLE repo_keys ADD COLUMN owner_did TEXT`) 111 114 return mErr 112 - }) 115 + }); err != nil { 116 + return nil, err 117 + } 113 118 114 - runMigration("add-repo-name-to-repo-keys", func() error { 119 + if err := runMigration("add-repo-name-to-repo-keys", func() error { 115 120 _, mErr := conn.ExecContext(ctx, `ALTER TABLE repo_keys ADD COLUMN repo_name TEXT`) 116 121 return mErr 117 - }) 122 + }); err != nil { 123 + return nil, err 124 + } 118 125 119 - runMigration("add-unique-owner-repo-on-repo-keys", func() error { 126 + if err := runMigration("add-unique-owner-repo-on-repo-keys", func() error { 120 127 _, mErr := conn.ExecContext(ctx, `CREATE UNIQUE INDEX IF NOT EXISTS idx_repo_keys_owner_repo ON repo_keys(owner_did, repo_name)`) 121 128 return mErr 122 - }) 129 + }); err != nil { 130 + return nil, err 131 + } 132 + 133 + if err := runMigration("add-key-type-and-nullable-signing-key", func() error { 134 + tx, txErr := conn.BeginTx(ctx, nil) 135 + if txErr != nil { 136 + return txErr 137 + } 138 + defer tx.Rollback() 139 + 140 + _, mErr := tx.ExecContext(ctx, ` 141 + create table repo_keys_new ( 142 + repo_did text primary key, 143 + signing_key blob, 144 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 145 + owner_did text, 146 + repo_name text, 147 + key_type text not null default 'k256' 148 + ); 149 + insert into repo_keys_new 150 + select repo_did, signing_key, created_at, owner_did, repo_name, 'k256' 151 + from repo_keys; 152 + drop table repo_keys; 153 + alter table repo_keys_new rename to repo_keys; 154 + create unique index if not exists idx_repo_keys_owner_repo 155 + on repo_keys(owner_did, repo_name); 156 + `) 157 + if mErr != nil { 158 + return mErr 159 + } 160 + return tx.Commit() 161 + }); err != nil { 162 + return nil, err 163 + } 164 + 165 + if err := runMigration("add-fk-repo-at-history", func() error { 166 + tx, txErr := conn.BeginTx(ctx, nil) 167 + if txErr != nil { 168 + return txErr 169 + } 170 + defer tx.Rollback() 171 + 172 + _, mErr := tx.ExecContext(ctx, ` 173 + create table repo_at_history_new ( 174 + old_repo_at text primary key, 175 + repo_did text not null, 176 + created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 177 + foreign key (repo_did) references repo_keys(repo_did) on delete cascade 178 + ); 179 + insert into repo_at_history_new 180 + select old_repo_at, repo_did, created_at 181 + from repo_at_history; 182 + drop table repo_at_history; 183 + alter table repo_at_history_new rename to repo_at_history; 184 + `) 185 + if mErr != nil { 186 + return mErr 187 + } 188 + return tx.Commit() 189 + }); err != nil { 190 + return nil, err 191 + } 123 192 124 193 return &DB{ 125 194 db: db, ··· 127 196 }, nil 128 197 } 129 198 199 + func (d *DB) StoreRepoKey(repoDid string, signingKey []byte, ownerDid, repoName string) error { 200 + _, err := d.db.Exec( 201 + `INSERT INTO repo_keys (repo_did, signing_key, owner_did, repo_name, key_type) VALUES (?, ?, ?, ?, 'k256')`, 202 + repoDid, signingKey, ownerDid, repoName, 203 + ) 204 + return err 205 + } 206 + 207 + func (d *DB) StoreRepoDidWeb(repoDid, ownerDid, repoName string) error { 208 + _, err := d.db.Exec( 209 + `INSERT INTO repo_keys (repo_did, signing_key, owner_did, repo_name, key_type) VALUES (?, NULL, ?, ?, 'web')`, 210 + repoDid, ownerDid, repoName, 211 + ) 212 + return err 213 + } 214 + 215 + func (d *DB) DeleteRepoKey(repoDid string) error { 216 + _, err := d.db.Exec(`DELETE FROM repo_keys WHERE repo_did = ?`, repoDid) 217 + return err 218 + } 219 + 220 + func (d *DB) RepoDidExists(repoDid string) (bool, error) { 221 + var count int 222 + err := d.db.QueryRow(`SELECT count(1) FROM repo_keys WHERE repo_did = ?`, repoDid).Scan(&count) 223 + return count > 0, err 224 + } 225 + 130 226 func (d *DB) ResolveAtUri(atUri string) (string, error) { 131 227 var repoDid string 132 228 err := d.db.QueryRow( ··· 145 241 return repoDid, err 146 242 } 147 243 244 + func (d *DB) GetRepoSigningKey(repoDid string) ([]byte, error) { 245 + var signingKey []byte 246 + err := d.db.QueryRow( 247 + `SELECT signing_key FROM repo_keys WHERE repo_did = ? AND key_type = 'k256'`, 248 + repoDid, 249 + ).Scan(&signingKey) 250 + if err != nil { 251 + return nil, fmt.Errorf("retrieving signing key for %s: %w", repoDid, err) 252 + } 253 + if signingKey == nil { 254 + return nil, fmt.Errorf("signing key for %s is null (did:web repo?)", repoDid) 255 + } 256 + return signingKey, nil 257 + } 258 + 148 259 func (d *DB) GetRepoKeyOwner(repoDid string) (ownerDid string, repoName string, err error) { 149 260 var nullOwner, nullName sql.NullString 150 261 err = d.db.QueryRow( ··· 167 278 did, lookupErr := d.GetRepoDid(ownerDid, repoName) 168 279 if lookupErr == nil && did != "" { 169 280 didPath, joinErr := securejoin.SecureJoin(scanPath, did) 170 - if joinErr == nil { 281 + if joinErr != nil { 282 + d.logger.Error("securejoin failed for repo DID path", 283 + "repoDid", did, "scanPath", scanPath, "error", joinErr) 284 + } else { 171 285 if _, statErr := os.Stat(didPath); statErr == nil { 172 286 return didPath, did, nil 173 287 } 288 + d.logger.Warn("repo DID in database but directory missing on disk, falling back to legacy path", 289 + "repoDid", did, "expectedPath", didPath, "owner", ownerDid, "repo", repoName) 174 290 } 175 291 } 176 - relative, _ := securejoin.SecureJoin(ownerDid, repoName) 177 - fallback, _ := securejoin.SecureJoin(scanPath, relative) 292 + relative, relErr := securejoin.SecureJoin(ownerDid, repoName) 293 + if relErr != nil { 294 + return "", "", fmt.Errorf("securejoin failed for legacy path %s/%s: %w", ownerDid, repoName, relErr) 295 + } 296 + fallback, fallbackErr := securejoin.SecureJoin(scanPath, relative) 297 + if fallbackErr != nil { 298 + return "", "", fmt.Errorf("securejoin failed for legacy path %s/%s: %w", scanPath, relative, fallbackErr) 299 + } 178 300 if _, statErr := os.Stat(fallback); statErr != nil { 179 301 return "", "", fmt.Errorf("repo not found on disk: %s/%s", ownerDid, repoName) 180 302 } ··· 188 310 } 189 311 190 312 didPath, joinErr := securejoin.SecureJoin(scanPath, repoDid) 191 - if joinErr == nil { 313 + if joinErr != nil { 314 + d.logger.Error("securejoin failed for repo DID path", 315 + "repoDid", repoDid, "scanPath", scanPath, "error", joinErr) 316 + } else { 192 317 if _, statErr := os.Stat(didPath); statErr == nil { 193 318 repoPath = didPath 194 319 return 195 320 } 321 + d.logger.Warn("repo DID directory missing on disk, falling back to legacy path", 322 + "repoDid", repoDid, "expectedPath", didPath, "owner", ownerDid, "repo", repoName) 196 323 } 197 324 198 - relative, _ := securejoin.SecureJoin(ownerDid, repoName) 199 - repoPath, _ = securejoin.SecureJoin(scanPath, relative) 325 + relative, relErr := securejoin.SecureJoin(ownerDid, repoName) 326 + if relErr != nil { 327 + err = fmt.Errorf("securejoin failed for legacy path %s/%s: %w", ownerDid, repoName, relErr) 328 + return 329 + } 330 + fallback, fallbackErr := securejoin.SecureJoin(scanPath, relative) 331 + if fallbackErr != nil { 332 + err = fmt.Errorf("securejoin failed for legacy path %s/%s: %w", scanPath, relative, fallbackErr) 333 + return 334 + } 335 + if _, statErr := os.Stat(fallback); statErr != nil { 336 + err = fmt.Errorf("repo not found on disk for DID %s: %s", repoDid, fallback) 337 + return 338 + } 339 + repoPath = fallback 200 340 return 201 341 }
+89 -56
knotserver/ingester.go
··· 102 102 return fmt.Errorf("ignoring pull record: target repo is nil") 103 103 } 104 104 105 - l = l.With("target_repo", record.Target.Repo) 105 + l = l.With("target_repo", record.Target.Repo, "target_repo_did", record.Target.RepoDid) 106 106 l = l.With("target_branch", record.Target.Branch) 107 107 108 108 if record.Source == nil { 109 109 return fmt.Errorf("ignoring pull record: not a branch-based pull request") 110 110 } 111 111 112 - if record.Source.Repo != nil { 112 + if record.Source.Repo != nil || record.Source.RepoDid != nil { 113 113 return fmt.Errorf("ignoring pull record: fork based pull") 114 114 } 115 115 116 - repoAt, err := syntax.ParseATURI(record.Target.Repo) 117 - if err != nil { 118 - return fmt.Errorf("failed to parse ATURI: %w", err) 119 - } 116 + var repoPath, ownerDid, repoName string 117 + switch { 118 + case record.Target.RepoDid != nil && *record.Target.RepoDid != "": 119 + var lookupErr error 120 + repoPath, ownerDid, repoName, lookupErr = h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, *record.Target.RepoDid) 121 + if lookupErr != nil { 122 + return fmt.Errorf("unknown target repo DID %s: %w", *record.Target.RepoDid, lookupErr) 123 + } 124 + 125 + case record.Target.Repo != nil: 126 + // TODO: get rid of this PDS fetch once all repos have DIDs 127 + repoAt, parseErr := syntax.ParseATURI(*record.Target.Repo) 128 + if parseErr != nil { 129 + return fmt.Errorf("failed to parse ATURI: %w", parseErr) 130 + } 120 131 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 - } 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 + didSlashRepo, joinErr := securejoin.SecureJoin(ownerDid, repoName) 156 + if joinErr != nil { 157 + return fmt.Errorf("failed to construct relative repo path: %w", joinErr) 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 pathErr error 161 + repoPath, pathErr = securejoin.SecureJoin(h.c.Repo.ScanPath, didSlashRepo) 162 + if pathErr != nil { 163 + return fmt.Errorf("failed to construct absolute repo path: %w", pathErr) 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, 195 213 }, 196 214 }, 197 215 } ··· 226 244 return fmt.Errorf("failed to unmarshal record: %w", err) 227 245 } 228 246 229 - repoAt, err := syntax.ParseATURI(record.Repo) 230 - if err != nil { 231 - return err 232 - } 233 - 234 247 subjectId, err := h.resolver.ResolveIdent(ctx, record.Subject) 235 248 if err != nil || subjectId.Handle.IsInvalidHandle() { 236 249 return err 237 250 } 238 251 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 - } 252 + var rbacResource string 253 + switch { 254 + case record.RepoDid != nil && *record.RepoDid != "": 255 + ownerDid, _, lookupErr := h.db.GetRepoKeyOwner(*record.RepoDid) 256 + if lookupErr != nil { 257 + return fmt.Errorf("unknown repo DID %s: %w", *record.RepoDid, lookupErr) 258 + } 259 + if ownerDid != did { 260 + return fmt.Errorf("collaborator record author %s does not own repo %s", did, *record.RepoDid) 261 + } 262 + rbacResource = *record.RepoDid 245 263 246 - xrpcc := xrpc.Client{ 247 - Host: owner.PDSEndpoint(), 248 - } 264 + case record.Repo != nil: 265 + // TODO: get rid of this PDS fetch once all repos have DIDs 266 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 267 + if parseErr != nil { 268 + return parseErr 269 + } 249 270 250 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 251 - if err != nil { 252 - return err 253 - } 271 + owner, resolveErr := h.resolver.ResolveIdent(ctx, repoAt.Authority().String()) 272 + if resolveErr != nil || owner.Handle.IsInvalidHandle() { 273 + return fmt.Errorf("failed to resolve handle: %w", resolveErr) 274 + } 254 275 255 - repo := resp.Value.Val.(*tangled.Repo) 256 - didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name) 276 + xrpcc := xrpc.Client{ 277 + Host: owner.PDSEndpoint(), 278 + } 257 279 258 - // check perms for this user 259 - ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, didSlashRepo) 280 + resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 281 + if getErr != nil { 282 + return getErr 283 + } 284 + 285 + repo := resp.Value.Val.(*tangled.Repo) 286 + rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repo.Name) 287 + 288 + default: 289 + return fmt.Errorf("collaborator record has neither repo nor repoDid") 290 + } 291 + 292 + ok, err := h.e.IsCollaboratorInviteAllowed(did, rbac.ThisServer, rbacResource) 260 293 if err != nil { 261 294 return fmt.Errorf("failed to check permissions: %w", err) 262 295 } 263 296 if !ok { 264 - return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", didSlashRepo) 297 + return fmt.Errorf("insufficient permissions: %s, %s, %s", did, "IsCollaboratorInviteAllowed", rbacResource) 265 298 } 266 299 267 300 if err := h.db.AddDid(subjectId.DID.String()); err != nil { ··· 269 302 } 270 303 h.jc.AddDid(subjectId.DID.String()) 271 304 272 - if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, didSlashRepo); err != nil { 305 + if err := h.e.AddCollaborator(subjectId.DID.String(), rbac.ThisServer, rbacResource); err != nil { 273 306 return err 274 307 } 275 308
+25 -5
knotserver/internal.go
··· 95 95 switch { 96 96 case len(components) == 1 && strings.HasPrefix(components[0], "did:"): 97 97 repoDid := components[0] 98 - repoPath, ownerDid, repoName, lookupErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 98 + repoPath, _, _, lookupErr := h.db.ResolveRepoDIDOnDisk(h.c.Repo.ScanPath, repoDid) 99 99 if lookupErr != nil { 100 100 w.WriteHeader(http.StatusNotFound) 101 101 l.Error("repo DID not found", "repoDid", repoDid, "err", lookupErr) 102 102 fmt.Fprintln(w, "repo not found") 103 103 return 104 104 } 105 - rbacResource, _ = securejoin.SecureJoin(ownerDid, repoName) 105 + rbacResource = repoDid 106 106 rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath) 107 107 if relErr != nil { 108 108 w.WriteHeader(http.StatusInternalServerError) ··· 122 122 fmt.Fprintf(w, "error resolving handle: invalid handle\n") 123 123 return 124 124 } 125 - qualified, _ := securejoin.SecureJoin(repoOwnerIdent.DID.String(), components[1]) 126 - rbacResource = qualified 127 - diskRelative = qualified 125 + ownerDid := repoOwnerIdent.DID.String() 126 + repoName := components[1] 127 + repoPath, repoDid, lookupErr := h.db.ResolveRepoOnDisk(h.c.Repo.ScanPath, ownerDid, repoName) 128 + if lookupErr != nil { 129 + w.WriteHeader(http.StatusNotFound) 130 + l.Error("repo not found on disk", "owner", ownerDid, "name", repoName, "err", lookupErr) 131 + fmt.Fprintln(w, "repo not found") 132 + return 133 + } 134 + if repoDid != "" { 135 + rbacResource = repoDid 136 + } else { 137 + qualified, _ := securejoin.SecureJoin(ownerDid, repoName) 138 + rbacResource = qualified 139 + } 140 + rel, relErr := filepath.Rel(h.c.Repo.ScanPath, repoPath) 141 + if relErr != nil { 142 + w.WriteHeader(http.StatusInternalServerError) 143 + l.Error("failed to compute relative path", "repoPath", repoPath, "err", relErr) 144 + fmt.Fprintln(w, "internal error") 145 + return 146 + } 147 + diskRelative = rel 128 148 129 149 default: 130 150 w.WriteHeader(http.StatusBadRequest)
+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 + }
+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 {
+8 -5
knotserver/xrpc/delete_branch.go
··· 57 57 } 58 58 59 59 repo := resp.Value.Val.(*tangled.Repo) 60 - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) 60 + repoPath, repoDid, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, ident.DID.String(), repo.Name) 61 61 if err != nil { 62 62 fail(xrpcerr.GenericError(err)) 63 63 return 64 64 } 65 65 66 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 67 - l.Error("insufficent permissions", "did", actorDid.String(), "repo", didPath) 66 + rbacResource := repoDid 67 + if rbacResource == "" { 68 + rbacResource, _ = securejoin.SecureJoin(ident.DID.String(), repo.Name) 69 + } 70 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, rbacResource); !ok || err != nil { 71 + l.Error("insufficent permissions", "did", actorDid.String(), "repo", rbacResource) 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
+13 -10
knotserver/xrpc/delete_repo.go
··· 5 5 "fmt" 6 6 "net/http" 7 7 "os" 8 - "path/filepath" 8 + 9 + securejoin "github.com/cyphar/filepath-securejoin" 9 10 10 11 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 12 "github.com/bluesky-social/indigo/atproto/syntax" 12 13 "github.com/bluesky-social/indigo/xrpc" 13 - securejoin "github.com/cyphar/filepath-securejoin" 14 14 "tangled.org/core/api/tangled" 15 15 "tangled.org/core/rbac" 16 16 xrpcerr "tangled.org/core/xrpc/errors" ··· 61 61 return 62 62 } 63 63 64 - relativeRepoPath := filepath.Join(did, name) 65 - isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) 64 + repoPath, repoDid, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, did, name) 66 65 if err != nil { 67 66 fail(xrpcerr.GenericError(err)) 68 67 return 69 68 } 70 - if !isDeleteAllowed { 71 - fail(xrpcerr.AccessControlError(actorDid.String())) 72 - return 69 + 70 + rbacResource := repoDid 71 + if rbacResource == "" { 72 + rbacResource, _ = securejoin.SecureJoin(did, name) 73 73 } 74 - 75 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 74 + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, rbacResource) 76 75 if err != nil { 77 76 fail(xrpcerr.GenericError(err)) 77 + return 78 + } 79 + if !isDeleteAllowed { 80 + fail(xrpcerr.AccessControlError(actorDid.String())) 78 81 return 79 82 } 80 83 ··· 85 88 return 86 89 } 87 90 88 - err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) 91 + err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, rbacResource) 89 92 if err != nil { 90 93 l.Error("failed to delete repo from enforcer", "error", err.Error()) 91 94 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+12 -9
knotserver/xrpc/fork_status.go
··· 6 6 "net/http" 7 7 "path/filepath" 8 8 9 + securejoin "github.com/cyphar/filepath-securejoin" 10 + 9 11 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 12 "tangled.org/core/api/tangled" 12 13 "tangled.org/core/knotserver/git" 13 14 "tangled.org/core/rbac" ··· 51 52 name = filepath.Base(source) 52 53 } 53 54 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) 55 + repoPath, repoDid, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, did, name) 56 + if err != nil { 57 + fail(xrpcerr.GenericError(err)) 59 58 return 60 59 } 61 60 62 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 63 - if err != nil { 64 - fail(xrpcerr.GenericError(err)) 61 + rbacResource := repoDid 62 + if rbacResource == "" { 63 + rbacResource, _ = securejoin.SecureJoin(did, name) 64 + } 65 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, rbacResource); !ok || err != nil { 66 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", rbacResource) 67 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 65 68 return 66 69 } 67 70
+13 -10
knotserver/xrpc/fork_sync.go
··· 4 4 "encoding/json" 5 5 "fmt" 6 6 "net/http" 7 - "path/filepath" 7 + 8 + securejoin "github.com/cyphar/filepath-securejoin" 8 9 9 10 "github.com/bluesky-social/indigo/atproto/syntax" 10 - securejoin "github.com/cyphar/filepath-securejoin" 11 11 "tangled.org/core/api/tangled" 12 12 "tangled.org/core/knotserver/git" 13 13 "tangled.org/core/rbac" ··· 42 42 return 43 43 } 44 44 45 - relativeRepoPath := filepath.Join(did, name) 45 + repoPath, repoDid, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, did, name) 46 + if err != nil { 47 + fail(xrpcerr.GenericError(err)) 48 + return 49 + } 46 50 47 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 48 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 49 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 50 - return 51 + rbacResource := repoDid 52 + if rbacResource == "" { 53 + rbacResource, _ = securejoin.SecureJoin(did, name) 51 54 } 52 55 53 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 54 - if err != nil { 55 - fail(xrpcerr.GenericError(err)) 56 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, rbacResource); !ok || err != nil { 57 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", rbacResource) 58 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 56 59 return 57 60 } 58 61
+7 -9
knotserver/xrpc/hidden_ref.go
··· 63 63 } 64 64 65 65 repo := resp.Value.Val.(*tangled.Repo) 66 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 66 + repoPath, repoDid, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, actorDid.String(), repo.Name) 67 67 if err != nil { 68 68 fail(xrpcerr.GenericError(err)) 69 69 return 70 70 } 71 71 72 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 73 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", didPath) 74 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 75 - return 72 + rbacResource := repoDid 73 + if rbacResource == "" { 74 + rbacResource, _ = securejoin.SecureJoin(actorDid.String(), repo.Name) 76 75 } 77 - 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, rbacResource); !ok || err != nil { 77 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", rbacResource) 78 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 81 79 return 82 80 } 83 81
+7 -9
knotserver/xrpc/merge.go
··· 43 43 return 44 44 } 45 45 46 - relativeRepoPath, err := securejoin.SecureJoin(did, name) 46 + repoPath, repoDid, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, did, name) 47 47 if err != nil { 48 48 fail(xrpcerr.GenericError(err)) 49 49 return 50 50 } 51 51 52 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { 53 - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) 54 - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 55 - return 52 + rbacResource := repoDid 53 + if rbacResource == "" { 54 + rbacResource, _ = securejoin.SecureJoin(did, name) 56 55 } 57 - 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, rbacResource); !ok || err != nil { 57 + l.Error("insufficient permissions", "did", actorDid.String(), "repo", rbacResource) 58 + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) 61 59 return 62 60 } 63 61
+1 -8
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) 38 - if err != nil { 39 - fail(xrpcerr.GenericError(err)) 40 - return 41 - } 42 - 43 - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) 36 + repoPath, _, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, did, name) 44 37 if err != nil { 45 38 fail(xrpcerr.GenericError(err)) 46 39 return
+7 -4
knotserver/xrpc/set_default_branch.go
··· 59 59 } 60 60 61 61 repo := resp.Value.Val.(*tangled.Repo) 62 - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) 62 + repoPath, repoDid, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, actorDid.String(), repo.Name) 63 63 if err != nil { 64 64 fail(xrpcerr.GenericError(err)) 65 65 return 66 66 } 67 67 68 - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { 68 + rbacResource := repoDid 69 + if rbacResource == "" { 70 + rbacResource, _ = securejoin.SecureJoin(actorDid.String(), repo.Name) 71 + } 72 + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, rbacResource); !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
-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": {
+1 -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",
-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": {
-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"
-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 -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 }
+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="
+37 -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.RepoDid != nil && *record.RepoDid != "": 235 + rbacResource = *record.RepoDid 236 + ownerDid = e.Did 236 237 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 - } 238 + case record.Repo != nil: 239 + // TODO: get rid of this PDS fetch once all repos have DIDs 240 + repoAt, parseErr := syntax.ParseATURI(*record.Repo) 241 + if parseErr != nil { 242 + l.Info("rejecting record, invalid repoAt", "repoAt", *record.Repo) 243 + return nil 244 + } 245 + 246 + owner, resolveErr := s.res.ResolveIdent(ctx, repoAt.Authority().String()) 247 + if resolveErr != nil || owner.Handle.IsInvalidHandle() { 248 + return fmt.Errorf("failed to resolve handle: %w", resolveErr) 249 + } 250 + 251 + xrpcc := xrpc.Client{ 252 + Host: owner.PDSEndpoint(), 253 + } 254 + 255 + resp, getErr := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 256 + if getErr != nil { 257 + return getErr 258 + } 243 259 244 - xrpcc := xrpc.Client{ 245 - Host: owner.PDSEndpoint(), 246 - } 260 + repo := resp.Value.Val.(*tangled.Repo) 261 + rbacResource, _ = securejoin.SecureJoin(owner.DID.String(), repo.Name) 262 + ownerDid = owner.DID.String() 247 263 248 - resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String()) 249 - if err != nil { 250 - return err 264 + default: 265 + l.Info("rejecting collaborator record with neither repo nor repoDid") 266 + return nil 251 267 } 252 268 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 { 269 + if ok, err := s.e.IsCollaboratorInviteAllowed(ownerDid, rbac.ThisServer, rbacResource); !ok || err != nil { 258 270 return fmt.Errorf("insufficient permissions: %w", err) 259 271 } 260 272 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) 273 + if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, rbacResource); err != nil { 274 + l.Error("failed to add collaborator to enforcer", "error", err) 275 + return fmt.Errorf("failed to add collaborator: %w", err) 265 276 } 266 277 267 278 return nil