Monorepo for Tangled tangled.org

Compare changes

Choose any two refs to compare.

+6806 -4785
+2
api/tangled/actorprofile.go
··· 18 18 // RECORDTYPE: ActorProfile 19 19 type ActorProfile struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"` 21 + // avatar: Small image to be displayed next to posts from account. AKA, 'profile picture' 22 + Avatar *util.LexBlob `json:"avatar,omitempty" cborgen:"avatar,omitempty"` 21 23 // bluesky: Include link to this account on Bluesky. 22 24 Bluesky bool `json:"bluesky" cborgen:"bluesky"` 23 25 // description: Free-form profile description text.
+44 -1
api/tangled/cbor_gen.go
··· 26 26 } 27 27 28 28 cw := cbg.NewCborWriter(w) 29 - fieldCount := 8 29 + fieldCount := 9 30 + 31 + if t.Avatar == nil { 32 + fieldCount-- 33 + } 30 34 31 35 if t.Description == nil { 32 36 fieldCount-- ··· 144 148 return err 145 149 } 146 150 151 + } 152 + } 153 + 154 + // t.Avatar (util.LexBlob) (struct) 155 + if t.Avatar != nil { 156 + 157 + if len("avatar") > 1000000 { 158 + return xerrors.Errorf("Value in field \"avatar\" was too long") 159 + } 160 + 161 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("avatar"))); err != nil { 162 + return err 163 + } 164 + if _, err := cw.WriteString(string("avatar")); err != nil { 165 + return err 166 + } 167 + 168 + if err := t.Avatar.MarshalCBOR(cw); err != nil { 169 + return err 147 170 } 148 171 } 149 172 ··· 428 451 } 429 452 430 453 } 454 + } 455 + // t.Avatar (util.LexBlob) (struct) 456 + case "avatar": 457 + 458 + { 459 + 460 + b, err := cr.ReadByte() 461 + if err != nil { 462 + return err 463 + } 464 + if b != cbg.CborNull[0] { 465 + if err := cr.UnreadByte(); err != nil { 466 + return err 467 + } 468 + t.Avatar = new(util.LexBlob) 469 + if err := t.Avatar.UnmarshalCBOR(cr); err != nil { 470 + return xerrors.Errorf("unmarshaling t.Avatar pointer: %w", err) 471 + } 472 + } 473 + 431 474 } 432 475 // t.Bluesky (bool) (bool) 433 476 case "bluesky":
+9 -1
appview/db/db.go
··· 260 260 did text not null, 261 261 262 262 -- data 263 + avatar text, 263 264 description text not null, 264 265 include_bluesky integer not null default 0, 265 266 location text, ··· 1078 1079 // transfer data, constructing pull_at from pulls table 1079 1080 _, err = tx.Exec(` 1080 1081 insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1081 - select 1082 + select 1082 1083 ps.id, 1083 1084 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1084 1085 ps.round_number, ··· 1169 1170 1170 1171 create index if not exists idx_stars_created on stars(created); 1171 1172 create index if not exists idx_stars_subject_at_created on stars(subject_at, created); 1173 + `) 1174 + return err 1175 + }) 1176 + 1177 + orm.RunMigration(conn, logger, "add-avatar-to-profile", func(tx *sql.Tx) error { 1178 + _, err := tx.Exec(` 1179 + alter table profile add column avatar text; 1172 1180 `) 1173 1181 return err 1174 1182 })
+33 -3
appview/db/profile.go
··· 98 98 }) 99 99 } 100 100 101 + punchcard, err := MakePunchcard( 102 + e, 103 + orm.FilterEq("did", forDid), 104 + orm.FilterGte("date", time.Now().AddDate(0, -TimeframeMonths, 0)), 105 + ) 106 + if err != nil { 107 + return nil, fmt.Errorf("error getting commits by did: %w", err) 108 + } 109 + for _, punch := range punchcard.Punches { 110 + if punch.Date.After(now) { 111 + continue 112 + } 113 + 114 + monthsAgo := monthsBetween(punch.Date, now) 115 + if monthsAgo >= TimeframeMonths { 116 + // shouldn't happen; but times are weird 117 + continue 118 + } 119 + 120 + idx := monthsAgo 121 + timeline.ByMonth[idx].Commits += punch.Count 122 + } 123 + 101 124 return &timeline, nil 102 125 } 103 126 ··· 135 158 _, err = tx.Exec( 136 159 `insert or replace into profile ( 137 160 did, 161 + avatar, 138 162 description, 139 163 include_bluesky, 140 164 location, 141 165 pronouns 142 166 ) 143 - values (?, ?, ?, ?, ?)`, 167 + values (?, ?, ?, ?, ?, ?)`, 144 168 profile.Did, 169 + profile.Avatar, 145 170 profile.Description, 146 171 includeBskyValue, 147 172 profile.Location, ··· 324 349 func GetProfile(e Execer, did string) (*models.Profile, error) { 325 350 var profile models.Profile 326 351 var pronouns sql.Null[string] 352 + var avatar sql.Null[string] 327 353 328 354 profile.Did = did 329 355 330 356 includeBluesky := 0 331 357 332 358 err := e.QueryRow( 333 - `select description, include_bluesky, location, pronouns from profile where did = ?`, 359 + `select avatar, description, include_bluesky, location, pronouns from profile where did = ?`, 334 360 did, 335 - ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns) 361 + ).Scan(&avatar, &profile.Description, &includeBluesky, &profile.Location, &pronouns) 336 362 if err == sql.ErrNoRows { 337 363 profile := models.Profile{} 338 364 profile.Did = did ··· 349 375 350 376 if pronouns.Valid { 351 377 profile.Pronouns = pronouns.V 378 + } 379 + 380 + if avatar.Valid { 381 + profile.Avatar = avatar.V 352 382 } 353 383 354 384 rows, err := e.Query(`select link from profile_links where did = ?`, did)
+6
appview/ingester.go
··· 285 285 return err 286 286 } 287 287 288 + avatar := "" 289 + if record.Avatar != nil { 290 + avatar = record.Avatar.Ref.String() 291 + } 292 + 288 293 description := "" 289 294 if record.Description != nil { 290 295 description = *record.Description ··· 325 330 326 331 profile := models.Profile{ 327 332 Did: did, 333 + Avatar: avatar, 328 334 Description: description, 329 335 IncludeBluesky: includeBluesky, 330 336 Location: location,
+40 -41
appview/issues/issues.go
··· 81 81 82 82 func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 83 83 l := rp.logger.With("handler", "RepoSingleIssue") 84 - user := rp.oauth.GetUser(r) 84 + user := rp.oauth.GetMultiAccountUser(r) 85 85 f, err := rp.repoResolver.Resolve(r) 86 86 if err != nil { 87 87 l.Error("failed to get repo and knot", "err", err) ··· 102 102 103 103 userReactions := map[models.ReactionKind]bool{} 104 104 if user != nil { 105 - userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri()) 105 + userReactions = db.GetReactionStatusMap(rp.db, user.Active.Did, issue.AtUri()) 106 106 } 107 107 108 108 backlinks, err := db.GetBacklinks(rp.db, issue.AtUri()) ··· 129 129 } 130 130 131 131 rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 132 - LoggedInUser: user, 133 - RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 134 - Issue: issue, 135 - CommentList: issue.CommentList(), 136 - Backlinks: backlinks, 137 - OrderedReactionKinds: models.OrderedReactionKinds, 138 - Reactions: reactionMap, 139 - UserReacted: userReactions, 140 - LabelDefs: defs, 132 + LoggedInUser: user, 133 + RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 134 + Issue: issue, 135 + CommentList: issue.CommentList(), 136 + Backlinks: backlinks, 137 + Reactions: reactionMap, 138 + UserReacted: userReactions, 139 + LabelDefs: defs, 141 140 }) 142 141 } 143 142 144 143 func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) { 145 144 l := rp.logger.With("handler", "EditIssue") 146 - user := rp.oauth.GetUser(r) 145 + user := rp.oauth.GetMultiAccountUser(r) 147 146 148 147 issue, ok := r.Context().Value("issue").(*models.Issue) 149 148 if !ok { ··· 182 181 return 183 182 } 184 183 185 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Did, newIssue.Rkey) 184 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueNSID, user.Active.Did, newIssue.Rkey) 186 185 if err != nil { 187 186 l.Error("failed to get record", "err", err) 188 187 rp.pages.Notice(w, noticeId, "Failed to edit issue, no record found on PDS.") ··· 191 190 192 191 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 193 192 Collection: tangled.RepoIssueNSID, 194 - Repo: user.Did, 193 + Repo: user.Active.Did, 195 194 Rkey: newIssue.Rkey, 196 195 SwapRecord: ex.Cid, 197 196 Record: &lexutil.LexiconTypeDecoder{ ··· 292 291 293 292 func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) { 294 293 l := rp.logger.With("handler", "CloseIssue") 295 - user := rp.oauth.GetUser(r) 294 + user := rp.oauth.GetMultiAccountUser(r) 296 295 f, err := rp.repoResolver.Resolve(r) 297 296 if err != nil { 298 297 l.Error("failed to get repo and knot", "err", err) ··· 306 305 return 307 306 } 308 307 309 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 308 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 310 309 isRepoOwner := roles.IsOwner() 311 310 isCollaborator := roles.IsCollaborator() 312 - isIssueOwner := user.Did == issue.Did 311 + isIssueOwner := user.Active.Did == issue.Did 313 312 314 313 // TODO: make this more granular 315 314 if isIssueOwner || isRepoOwner || isCollaborator { ··· 326 325 issue.Open = false 327 326 328 327 // notify about the issue closure 329 - rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue) 328 + rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue) 330 329 331 330 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 332 331 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 340 339 341 340 func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) { 342 341 l := rp.logger.With("handler", "ReopenIssue") 343 - user := rp.oauth.GetUser(r) 342 + user := rp.oauth.GetMultiAccountUser(r) 344 343 f, err := rp.repoResolver.Resolve(r) 345 344 if err != nil { 346 345 l.Error("failed to get repo and knot", "err", err) ··· 354 353 return 355 354 } 356 355 357 - roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 356 + roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 358 357 isRepoOwner := roles.IsOwner() 359 358 isCollaborator := roles.IsCollaborator() 360 - isIssueOwner := user.Did == issue.Did 359 + isIssueOwner := user.Active.Did == issue.Did 361 360 362 361 if isCollaborator || isRepoOwner || isIssueOwner { 363 362 err := db.ReopenIssues( ··· 373 372 issue.Open = true 374 373 375 374 // notify about the issue reopen 376 - rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue) 375 + rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Active.Did), issue) 377 376 378 377 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 379 378 rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId)) ··· 387 386 388 387 func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) { 389 388 l := rp.logger.With("handler", "NewIssueComment") 390 - user := rp.oauth.GetUser(r) 389 + user := rp.oauth.GetMultiAccountUser(r) 391 390 f, err := rp.repoResolver.Resolve(r) 392 391 if err != nil { 393 392 l.Error("failed to get repo and knot", "err", err) ··· 416 415 mentions, references := rp.mentionsResolver.Resolve(r.Context(), body) 417 416 418 417 comment := models.IssueComment{ 419 - Did: user.Did, 418 + Did: user.Active.Did, 420 419 Rkey: tid.TID(), 421 420 IssueAt: issue.AtUri().String(), 422 421 ReplyTo: replyTo, ··· 495 494 496 495 func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) { 497 496 l := rp.logger.With("handler", "IssueComment") 498 - user := rp.oauth.GetUser(r) 497 + user := rp.oauth.GetMultiAccountUser(r) 499 498 500 499 issue, ok := r.Context().Value("issue").(*models.Issue) 501 500 if !ok { ··· 531 530 532 531 func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) { 533 532 l := rp.logger.With("handler", "EditIssueComment") 534 - user := rp.oauth.GetUser(r) 533 + user := rp.oauth.GetMultiAccountUser(r) 535 534 536 535 issue, ok := r.Context().Value("issue").(*models.Issue) 537 536 if !ok { ··· 557 556 } 558 557 comment := comments[0] 559 558 560 - if comment.Did != user.Did { 561 - l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did) 559 + if comment.Did != user.Active.Did { 560 + l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Active.Did) 562 561 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 563 562 return 564 563 } ··· 608 607 // rkey is optional, it was introduced later 609 608 if newComment.Rkey != "" { 610 609 // update the record on pds 611 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, comment.Rkey) 610 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Active.Did, comment.Rkey) 612 611 if err != nil { 613 612 l.Error("failed to get record", "err", err, "did", newComment.Did, "rkey", newComment.Rkey) 614 613 rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") ··· 617 616 618 617 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 619 618 Collection: tangled.RepoIssueCommentNSID, 620 - Repo: user.Did, 619 + Repo: user.Active.Did, 621 620 Rkey: newComment.Rkey, 622 621 SwapRecord: ex.Cid, 623 622 Record: &lexutil.LexiconTypeDecoder{ ··· 641 640 642 641 func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) { 643 642 l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder") 644 - user := rp.oauth.GetUser(r) 643 + user := rp.oauth.GetMultiAccountUser(r) 645 644 646 645 issue, ok := r.Context().Value("issue").(*models.Issue) 647 646 if !ok { ··· 677 676 678 677 func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) { 679 678 l := rp.logger.With("handler", "ReplyIssueComment") 680 - user := rp.oauth.GetUser(r) 679 + user := rp.oauth.GetMultiAccountUser(r) 681 680 682 681 issue, ok := r.Context().Value("issue").(*models.Issue) 683 682 if !ok { ··· 713 712 714 713 func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 715 714 l := rp.logger.With("handler", "DeleteIssueComment") 716 - user := rp.oauth.GetUser(r) 715 + user := rp.oauth.GetMultiAccountUser(r) 717 716 718 717 issue, ok := r.Context().Value("issue").(*models.Issue) 719 718 if !ok { ··· 739 738 } 740 739 comment := comments[0] 741 740 742 - if comment.Did != user.Did { 743 - l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Did) 741 + if comment.Did != user.Active.Did { 742 + l.Error("unauthorized action", "expectedDid", comment.Did, "gotDid", user.Active.Did) 744 743 http.Error(w, "you are not the author of this comment", http.StatusUnauthorized) 745 744 return 746 745 } ··· 769 768 } 770 769 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 771 770 Collection: tangled.RepoIssueCommentNSID, 772 - Repo: user.Did, 771 + Repo: user.Active.Did, 773 772 Rkey: comment.Rkey, 774 773 }) 775 774 if err != nil { ··· 807 806 808 807 page := pagination.FromContext(r.Context()) 809 808 810 - user := rp.oauth.GetUser(r) 809 + user := rp.oauth.GetMultiAccountUser(r) 811 810 f, err := rp.repoResolver.Resolve(r) 812 811 if err != nil { 813 812 l.Error("failed to get repo and knot", "err", err) ··· 884 883 } 885 884 886 885 rp.pages.RepoIssues(w, pages.RepoIssuesParams{ 887 - LoggedInUser: rp.oauth.GetUser(r), 886 + LoggedInUser: rp.oauth.GetMultiAccountUser(r), 888 887 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 889 888 Issues: issues, 890 889 IssueCount: totalIssues, ··· 897 896 898 897 func (rp *Issues) NewIssue(w http.ResponseWriter, r *http.Request) { 899 898 l := rp.logger.With("handler", "NewIssue") 900 - user := rp.oauth.GetUser(r) 899 + user := rp.oauth.GetMultiAccountUser(r) 901 900 902 901 f, err := rp.repoResolver.Resolve(r) 903 902 if err != nil { ··· 921 920 Title: r.FormValue("title"), 922 921 Body: body, 923 922 Open: true, 924 - Did: user.Did, 923 + Did: user.Active.Did, 925 924 Created: time.Now(), 926 925 Mentions: mentions, 927 926 References: references, ··· 945 944 } 946 945 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 947 946 Collection: tangled.RepoIssueNSID, 948 - Repo: user.Did, 947 + Repo: user.Active.Did, 949 948 Rkey: issue.Rkey, 950 949 Record: &lexutil.LexiconTypeDecoder{ 951 950 Val: &record,
+31 -48
appview/knots/knots.go
··· 40 40 Knotstream *eventconsumer.Consumer 41 41 } 42 42 43 - type tab = map[string]any 44 - 45 - var ( 46 - knotsTabs []tab = []tab{ 47 - {"Name": "profile", "Icon": "user"}, 48 - {"Name": "keys", "Icon": "key"}, 49 - {"Name": "emails", "Icon": "mail"}, 50 - {"Name": "notifications", "Icon": "bell"}, 51 - {"Name": "knots", "Icon": "volleyball"}, 52 - {"Name": "spindles", "Icon": "spool"}, 53 - } 54 - ) 55 - 56 43 func (k *Knots) Router() http.Handler { 57 44 r := chi.NewRouter() 58 45 ··· 70 57 } 71 58 72 59 func (k *Knots) knots(w http.ResponseWriter, r *http.Request) { 73 - user := k.OAuth.GetUser(r) 60 + user := k.OAuth.GetMultiAccountUser(r) 74 61 registrations, err := db.GetRegistrations( 75 62 k.Db, 76 - orm.FilterEq("did", user.Did), 63 + orm.FilterEq("did", user.Active.Did), 77 64 ) 78 65 if err != nil { 79 66 k.Logger.Error("failed to fetch knot registrations", "err", err) ··· 84 71 k.Pages.Knots(w, pages.KnotsParams{ 85 72 LoggedInUser: user, 86 73 Registrations: registrations, 87 - Tabs: knotsTabs, 88 - Tab: "knots", 89 74 }) 90 75 } 91 76 92 77 func (k *Knots) dashboard(w http.ResponseWriter, r *http.Request) { 93 78 l := k.Logger.With("handler", "dashboard") 94 79 95 - user := k.OAuth.GetUser(r) 96 - l = l.With("user", user.Did) 80 + user := k.OAuth.GetMultiAccountUser(r) 81 + l = l.With("user", user.Active.Did) 97 82 98 83 domain := chi.URLParam(r, "domain") 99 84 if domain == "" { ··· 103 88 104 89 registrations, err := db.GetRegistrations( 105 90 k.Db, 106 - orm.FilterEq("did", user.Did), 91 + orm.FilterEq("did", user.Active.Did), 107 92 orm.FilterEq("domain", domain), 108 93 ) 109 94 if err != nil { ··· 148 133 Members: members, 149 134 Repos: repoMap, 150 135 IsOwner: true, 151 - Tabs: knotsTabs, 152 - Tab: "knots", 153 136 }) 154 137 } 155 138 156 139 func (k *Knots) register(w http.ResponseWriter, r *http.Request) { 157 - user := k.OAuth.GetUser(r) 140 + user := k.OAuth.GetMultiAccountUser(r) 158 141 l := k.Logger.With("handler", "register") 159 142 160 143 noticeId := "register-error" ··· 175 158 return 176 159 } 177 160 l = l.With("domain", domain) 178 - l = l.With("user", user.Did) 161 + l = l.With("user", user.Active.Did) 179 162 180 163 tx, err := k.Db.Begin() 181 164 if err != nil { ··· 188 171 k.Enforcer.E.LoadPolicy() 189 172 }() 190 173 191 - err = db.AddKnot(tx, domain, user.Did) 174 + err = db.AddKnot(tx, domain, user.Active.Did) 192 175 if err != nil { 193 176 l.Error("failed to insert", "err", err) 194 177 fail() ··· 210 193 return 211 194 } 212 195 213 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 196 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain) 214 197 var exCid *string 215 198 if ex != nil { 216 199 exCid = ex.Cid ··· 219 202 // re-announce by registering under same rkey 220 203 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 221 204 Collection: tangled.KnotNSID, 222 - Repo: user.Did, 205 + Repo: user.Active.Did, 223 206 Rkey: domain, 224 207 Record: &lexutil.LexiconTypeDecoder{ 225 208 Val: &tangled.Knot{ ··· 250 233 } 251 234 252 235 // begin verification 253 - err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 236 + err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev) 254 237 if err != nil { 255 238 l.Error("verification failed", "err", err) 256 239 k.Pages.HxRefresh(w) 257 240 return 258 241 } 259 242 260 - err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 243 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did) 261 244 if err != nil { 262 245 l.Error("failed to mark verified", "err", err) 263 246 k.Pages.HxRefresh(w) ··· 275 258 } 276 259 277 260 func (k *Knots) delete(w http.ResponseWriter, r *http.Request) { 278 - user := k.OAuth.GetUser(r) 261 + user := k.OAuth.GetMultiAccountUser(r) 279 262 l := k.Logger.With("handler", "delete") 280 263 281 264 noticeId := "operation-error" ··· 294 277 // get record from db first 295 278 registrations, err := db.GetRegistrations( 296 279 k.Db, 297 - orm.FilterEq("did", user.Did), 280 + orm.FilterEq("did", user.Active.Did), 298 281 orm.FilterEq("domain", domain), 299 282 ) 300 283 if err != nil { ··· 322 305 323 306 err = db.DeleteKnot( 324 307 tx, 325 - orm.FilterEq("did", user.Did), 308 + orm.FilterEq("did", user.Active.Did), 326 309 orm.FilterEq("domain", domain), 327 310 ) 328 311 if err != nil { ··· 350 333 351 334 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 352 335 Collection: tangled.KnotNSID, 353 - Repo: user.Did, 336 + Repo: user.Active.Did, 354 337 Rkey: domain, 355 338 }) 356 339 if err != nil { ··· 382 365 } 383 366 384 367 func (k *Knots) retry(w http.ResponseWriter, r *http.Request) { 385 - user := k.OAuth.GetUser(r) 368 + user := k.OAuth.GetMultiAccountUser(r) 386 369 l := k.Logger.With("handler", "retry") 387 370 388 371 noticeId := "operation-error" ··· 398 381 return 399 382 } 400 383 l = l.With("domain", domain) 401 - l = l.With("user", user.Did) 384 + l = l.With("user", user.Active.Did) 402 385 403 386 // get record from db first 404 387 registrations, err := db.GetRegistrations( 405 388 k.Db, 406 - orm.FilterEq("did", user.Did), 389 + orm.FilterEq("did", user.Active.Did), 407 390 orm.FilterEq("domain", domain), 408 391 ) 409 392 if err != nil { ··· 419 402 registration := registrations[0] 420 403 421 404 // begin verification 422 - err = serververify.RunVerification(r.Context(), domain, user.Did, k.Config.Core.Dev) 405 + err = serververify.RunVerification(r.Context(), domain, user.Active.Did, k.Config.Core.Dev) 423 406 if err != nil { 424 407 l.Error("verification failed", "err", err) 425 408 ··· 437 420 return 438 421 } 439 422 440 - err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Did) 423 + err = serververify.MarkKnotVerified(k.Db, k.Enforcer, domain, user.Active.Did) 441 424 if err != nil { 442 425 l.Error("failed to mark verified", "err", err) 443 426 k.Pages.Notice(w, noticeId, err.Error()) ··· 456 439 return 457 440 } 458 441 459 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Did, domain) 442 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.KnotNSID, user.Active.Did, domain) 460 443 var exCid *string 461 444 if ex != nil { 462 445 exCid = ex.Cid ··· 465 448 // ignore the error here 466 449 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 467 450 Collection: tangled.KnotNSID, 468 - Repo: user.Did, 451 + Repo: user.Active.Did, 469 452 Rkey: domain, 470 453 Record: &lexutil.LexiconTypeDecoder{ 471 454 Val: &tangled.Knot{ ··· 494 477 // Get updated registration to show 495 478 registrations, err = db.GetRegistrations( 496 479 k.Db, 497 - orm.FilterEq("did", user.Did), 480 + orm.FilterEq("did", user.Active.Did), 498 481 orm.FilterEq("domain", domain), 499 482 ) 500 483 if err != nil { ··· 516 499 } 517 500 518 501 func (k *Knots) addMember(w http.ResponseWriter, r *http.Request) { 519 - user := k.OAuth.GetUser(r) 502 + user := k.OAuth.GetMultiAccountUser(r) 520 503 l := k.Logger.With("handler", "addMember") 521 504 522 505 domain := chi.URLParam(r, "domain") ··· 526 509 return 527 510 } 528 511 l = l.With("domain", domain) 529 - l = l.With("user", user.Did) 512 + l = l.With("user", user.Active.Did) 530 513 531 514 registrations, err := db.GetRegistrations( 532 515 k.Db, 533 - orm.FilterEq("did", user.Did), 516 + orm.FilterEq("did", user.Active.Did), 534 517 orm.FilterEq("domain", domain), 535 518 orm.FilterIsNot("registered", "null"), 536 519 ) ··· 583 566 584 567 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 585 568 Collection: tangled.KnotMemberNSID, 586 - Repo: user.Did, 569 + Repo: user.Active.Did, 587 570 Rkey: rkey, 588 571 Record: &lexutil.LexiconTypeDecoder{ 589 572 Val: &tangled.KnotMember{ ··· 618 601 } 619 602 620 603 func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) { 621 - user := k.OAuth.GetUser(r) 604 + user := k.OAuth.GetMultiAccountUser(r) 622 605 l := k.Logger.With("handler", "removeMember") 623 606 624 607 noticeId := "operation-error" ··· 634 617 return 635 618 } 636 619 l = l.With("domain", domain) 637 - l = l.With("user", user.Did) 620 + l = l.With("user", user.Active.Did) 638 621 639 622 registrations, err := db.GetRegistrations( 640 623 k.Db, 641 - orm.FilterEq("did", user.Did), 624 + orm.FilterEq("did", user.Active.Did), 642 625 orm.FilterEq("domain", domain), 643 626 orm.FilterIsNot("registered", "null"), 644 627 )
+2 -2
appview/labels/labels.go
··· 68 68 // - this handler should calculate the diff in order to create the labelop record 69 69 // - we need the diff in order to maintain a "history" of operations performed by users 70 70 func (l *Labels) PerformLabelOp(w http.ResponseWriter, r *http.Request) { 71 - user := l.oauth.GetUser(r) 71 + user := l.oauth.GetMultiAccountUser(r) 72 72 73 73 noticeId := "add-label-error" 74 74 ··· 82 82 return 83 83 } 84 84 85 - did := user.Did 85 + did := user.Active.Did 86 86 rkey := tid.TID() 87 87 performedAt := time.Now() 88 88 indexedAt := time.Now()
+6 -8
appview/middleware/middleware.go
··· 115 115 return func(next http.Handler) http.Handler { 116 116 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 117 117 // requires auth also 118 - actor := mw.oauth.GetUser(r) 118 + actor := mw.oauth.GetMultiAccountUser(r) 119 119 if actor == nil { 120 120 // we need a logged in user 121 121 log.Printf("not logged in, redirecting") ··· 128 128 return 129 129 } 130 130 131 - ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Did, group, domain) 131 + ok, err := mw.enforcer.E.HasGroupingPolicy(actor.Active.Did, group, domain) 132 132 if err != nil || !ok { 133 - // we need a logged in user 134 - log.Printf("%s does not have perms of a %s in domain %s", actor.Did, group, domain) 133 + log.Printf("%s does not have perms of a %s in domain %s", actor.Active.Did, group, domain) 135 134 http.Error(w, "Forbiden", http.StatusUnauthorized) 136 135 return 137 136 } ··· 149 148 return func(next http.Handler) http.Handler { 150 149 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 150 // requires auth also 152 - actor := mw.oauth.GetUser(r) 151 + actor := mw.oauth.GetMultiAccountUser(r) 153 152 if actor == nil { 154 153 // we need a logged in user 155 154 log.Printf("not logged in, redirecting") ··· 162 161 return 163 162 } 164 163 165 - ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 164 + ok, err := mw.enforcer.E.Enforce(actor.Active.Did, f.Knot, f.DidSlashRepo(), requiredPerm) 166 165 if err != nil || !ok { 167 - // we need a logged in user 168 - log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo()) 166 + log.Printf("%s does not have perms of a %s in repo %s", actor.Active.Did, requiredPerm, f.DidSlashRepo()) 169 167 http.Error(w, "Forbiden", http.StatusUnauthorized) 170 168 return 171 169 }
+38
appview/models/pipeline.go
··· 3 3 import ( 4 4 "fmt" 5 5 "slices" 6 + "strings" 6 7 "time" 7 8 8 9 "github.com/bluesky-social/indigo/atproto/syntax" ··· 56 57 } 57 58 58 59 return 0 60 + } 61 + 62 + // produces short summary of successes: 63 + // - "0/4" when zero successes of 4 workflows 64 + // - "4/4" when all successes of 4 workflows 65 + // - "0/0" when no workflows run in this pipeline 66 + func (p Pipeline) ShortStatusSummary() string { 67 + counts := make(map[spindle.StatusKind]int) 68 + for _, w := range p.Statuses { 69 + counts[w.Latest().Status] += 1 70 + } 71 + 72 + total := len(p.Statuses) 73 + successes := counts[spindle.StatusKindSuccess] 74 + 75 + return fmt.Sprintf("%d/%d", successes, total) 76 + } 77 + 78 + // produces a string of the form "3/4 success, 2/4 failed, 1/4 pending" 79 + func (p Pipeline) LongStatusSummary() string { 80 + counts := make(map[spindle.StatusKind]int) 81 + for _, w := range p.Statuses { 82 + counts[w.Latest().Status] += 1 83 + } 84 + 85 + total := len(p.Statuses) 86 + 87 + var result []string 88 + // finish states first, followed by start states 89 + states := append(spindle.FinishStates[:], spindle.StartStates[:]...) 90 + for _, state := range states { 91 + if count, ok := counts[state]; ok { 92 + result = append(result, fmt.Sprintf("%d/%d %s", count, total, state.String())) 93 + } 94 + } 95 + 96 + return strings.Join(result, ", ") 59 97 } 60 98 61 99 func (p Pipeline) Counts() map[string]int {
+1
appview/models/profile.go
··· 13 13 Did string 14 14 15 15 // data 16 + Avatar string // CID of the avatar blob 16 17 Description string 17 18 IncludeBluesky bool 18 19 Location string
+7 -17
appview/models/pull.go
··· 171 171 return syntax.ATURI(p.CommentAt) 172 172 } 173 173 174 - // func (p *PullComment) AsRecord() tangled.RepoPullComment { 175 - // mentions := make([]string, len(p.Mentions)) 176 - // for i, did := range p.Mentions { 177 - // mentions[i] = string(did) 178 - // } 179 - // references := make([]string, len(p.References)) 180 - // for i, uri := range p.References { 181 - // references[i] = string(uri) 182 - // } 183 - // return tangled.RepoPullComment{ 184 - // Pull: p.PullAt, 185 - // Body: p.Body, 186 - // Mentions: mentions, 187 - // References: references, 188 - // CreatedAt: p.Created.Format(time.RFC3339), 189 - // } 190 - // } 174 + func (p *Pull) TotalComments() int { 175 + total := 0 176 + for _, s := range p.Submissions { 177 + total += len(s.Comments) 178 + } 179 + return total 180 + } 191 181 192 182 func (p *Pull) LastRoundNumber() int { 193 183 return len(p.Submissions) - 1
+4 -1
appview/models/repo.go
··· 130 130 131 131 // current display mode 132 132 ShowingRendered bool // currently in rendered mode 133 - ShowingText bool // currently in text/code mode 134 133 135 134 // content type flags 136 135 ContentType BlobContentType ··· 151 150 // no view available, only raw 152 151 return !(b.HasRenderedView || b.HasTextView) 153 152 } 153 + 154 + func (b BlobView) ShowingText() bool { 155 + return !b.ShowingRendered 156 + }
+6 -6
appview/notifications/notifications.go
··· 48 48 49 49 func (n *Notifications) notificationsPage(w http.ResponseWriter, r *http.Request) { 50 50 l := n.logger.With("handler", "notificationsPage") 51 - user := n.oauth.GetUser(r) 51 + user := n.oauth.GetMultiAccountUser(r) 52 52 53 53 page := pagination.FromContext(r.Context()) 54 54 55 55 total, err := db.CountNotifications( 56 56 n.db, 57 - orm.FilterEq("recipient_did", user.Did), 57 + orm.FilterEq("recipient_did", user.Active.Did), 58 58 ) 59 59 if err != nil { 60 60 l.Error("failed to get total notifications", "err", err) ··· 65 65 notifications, err := db.GetNotificationsWithEntities( 66 66 n.db, 67 67 page, 68 - orm.FilterEq("recipient_did", user.Did), 68 + orm.FilterEq("recipient_did", user.Active.Did), 69 69 ) 70 70 if err != nil { 71 71 l.Error("failed to get notifications", "err", err) ··· 73 73 return 74 74 } 75 75 76 - err = db.MarkAllNotificationsRead(n.db, user.Did) 76 + err = db.MarkAllNotificationsRead(n.db, user.Active.Did) 77 77 if err != nil { 78 78 l.Error("failed to mark notifications as read", "err", err) 79 79 } ··· 90 90 } 91 91 92 92 func (n *Notifications) getUnreadCount(w http.ResponseWriter, r *http.Request) { 93 - user := n.oauth.GetUser(r) 93 + user := n.oauth.GetMultiAccountUser(r) 94 94 if user == nil { 95 95 http.Error(w, "Forbidden", http.StatusUnauthorized) 96 96 return ··· 98 98 99 99 count, err := db.CountNotifications( 100 100 n.db, 101 - orm.FilterEq("recipient_did", user.Did), 101 + orm.FilterEq("recipient_did", user.Active.Did), 102 102 orm.FilterEq("read", 0), 103 103 ) 104 104 if err != nil {
+191
appview/oauth/accounts.go
··· 1 + package oauth 2 + 3 + import ( 4 + "encoding/json" 5 + "errors" 6 + "net/http" 7 + "time" 8 + ) 9 + 10 + const MaxAccounts = 20 11 + 12 + var ErrMaxAccountsReached = errors.New("maximum number of linked accounts reached") 13 + 14 + type AccountInfo struct { 15 + Did string `json:"did"` 16 + Handle string `json:"handle"` 17 + SessionId string `json:"session_id"` 18 + AddedAt int64 `json:"added_at"` 19 + } 20 + 21 + type AccountRegistry struct { 22 + Accounts []AccountInfo `json:"accounts"` 23 + } 24 + 25 + type MultiAccountUser struct { 26 + Active *User 27 + Accounts []AccountInfo 28 + } 29 + 30 + func (m *MultiAccountUser) Did() string { 31 + if m.Active == nil { 32 + return "" 33 + } 34 + return m.Active.Did 35 + } 36 + 37 + func (m *MultiAccountUser) Pds() string { 38 + if m.Active == nil { 39 + return "" 40 + } 41 + return m.Active.Pds 42 + } 43 + 44 + func (o *OAuth) GetAccounts(r *http.Request) *AccountRegistry { 45 + session, err := o.SessStore.Get(r, AccountsName) 46 + if err != nil || session.IsNew { 47 + return &AccountRegistry{Accounts: []AccountInfo{}} 48 + } 49 + 50 + data, ok := session.Values["accounts"].(string) 51 + if !ok { 52 + return &AccountRegistry{Accounts: []AccountInfo{}} 53 + } 54 + 55 + var registry AccountRegistry 56 + if err := json.Unmarshal([]byte(data), &registry); err != nil { 57 + return &AccountRegistry{Accounts: []AccountInfo{}} 58 + } 59 + 60 + return &registry 61 + } 62 + 63 + func (o *OAuth) SaveAccounts(w http.ResponseWriter, r *http.Request, registry *AccountRegistry) error { 64 + session, err := o.SessStore.Get(r, AccountsName) 65 + if err != nil { 66 + return err 67 + } 68 + 69 + data, err := json.Marshal(registry) 70 + if err != nil { 71 + return err 72 + } 73 + 74 + session.Values["accounts"] = string(data) 75 + session.Options.MaxAge = 60 * 60 * 24 * 365 76 + session.Options.HttpOnly = true 77 + session.Options.Secure = !o.Config.Core.Dev 78 + session.Options.SameSite = http.SameSiteLaxMode 79 + 80 + return session.Save(r, w) 81 + } 82 + 83 + func (r *AccountRegistry) AddAccount(did, handle, sessionId string) error { 84 + for i, acc := range r.Accounts { 85 + if acc.Did == did { 86 + r.Accounts[i].SessionId = sessionId 87 + r.Accounts[i].Handle = handle 88 + return nil 89 + } 90 + } 91 + 92 + if len(r.Accounts) >= MaxAccounts { 93 + return ErrMaxAccountsReached 94 + } 95 + 96 + r.Accounts = append(r.Accounts, AccountInfo{ 97 + Did: did, 98 + Handle: handle, 99 + SessionId: sessionId, 100 + AddedAt: time.Now().Unix(), 101 + }) 102 + return nil 103 + } 104 + 105 + func (r *AccountRegistry) RemoveAccount(did string) { 106 + filtered := make([]AccountInfo, 0, len(r.Accounts)) 107 + for _, acc := range r.Accounts { 108 + if acc.Did != did { 109 + filtered = append(filtered, acc) 110 + } 111 + } 112 + r.Accounts = filtered 113 + } 114 + 115 + func (r *AccountRegistry) FindAccount(did string) *AccountInfo { 116 + for i := range r.Accounts { 117 + if r.Accounts[i].Did == did { 118 + return &r.Accounts[i] 119 + } 120 + } 121 + return nil 122 + } 123 + 124 + func (r *AccountRegistry) OtherAccounts(activeDid string) []AccountInfo { 125 + result := make([]AccountInfo, 0, len(r.Accounts)) 126 + for _, acc := range r.Accounts { 127 + if acc.Did != activeDid { 128 + result = append(result, acc) 129 + } 130 + } 131 + return result 132 + } 133 + 134 + func (o *OAuth) GetMultiAccountUser(r *http.Request) *MultiAccountUser { 135 + user := o.GetUser(r) 136 + if user == nil { 137 + return nil 138 + } 139 + 140 + registry := o.GetAccounts(r) 141 + return &MultiAccountUser{ 142 + Active: user, 143 + Accounts: registry.Accounts, 144 + } 145 + } 146 + 147 + type AuthReturnInfo struct { 148 + ReturnURL string 149 + AddAccount bool 150 + } 151 + 152 + func (o *OAuth) SetAuthReturn(w http.ResponseWriter, r *http.Request, returnURL string, addAccount bool) error { 153 + session, err := o.SessStore.Get(r, AuthReturnName) 154 + if err != nil { 155 + return err 156 + } 157 + 158 + session.Values[AuthReturnURL] = returnURL 159 + session.Values[AuthAddAccount] = addAccount 160 + session.Options.MaxAge = 60 * 30 161 + session.Options.HttpOnly = true 162 + session.Options.Secure = !o.Config.Core.Dev 163 + session.Options.SameSite = http.SameSiteLaxMode 164 + 165 + return session.Save(r, w) 166 + } 167 + 168 + func (o *OAuth) GetAuthReturn(r *http.Request) *AuthReturnInfo { 169 + session, err := o.SessStore.Get(r, AuthReturnName) 170 + if err != nil || session.IsNew { 171 + return &AuthReturnInfo{} 172 + } 173 + 174 + returnURL, _ := session.Values[AuthReturnURL].(string) 175 + addAccount, _ := session.Values[AuthAddAccount].(bool) 176 + 177 + return &AuthReturnInfo{ 178 + ReturnURL: returnURL, 179 + AddAccount: addAccount, 180 + } 181 + } 182 + 183 + func (o *OAuth) ClearAuthReturn(w http.ResponseWriter, r *http.Request) error { 184 + session, err := o.SessStore.Get(r, AuthReturnName) 185 + if err != nil { 186 + return err 187 + } 188 + 189 + session.Options.MaxAge = -1 190 + return session.Save(r, w) 191 + }
+265
appview/oauth/accounts_test.go
··· 1 + package oauth 2 + 3 + import ( 4 + "testing" 5 + ) 6 + 7 + func TestAccountRegistry_AddAccount(t *testing.T) { 8 + tests := []struct { 9 + name string 10 + initial []AccountInfo 11 + addDid string 12 + addHandle string 13 + addSessionId string 14 + wantErr error 15 + wantLen int 16 + wantSessionId string 17 + }{ 18 + { 19 + name: "add first account", 20 + initial: []AccountInfo{}, 21 + addDid: "did:plc:abc123", 22 + addHandle: "alice.bsky.social", 23 + addSessionId: "session-1", 24 + wantErr: nil, 25 + wantLen: 1, 26 + wantSessionId: "session-1", 27 + }, 28 + { 29 + name: "add second account", 30 + initial: []AccountInfo{ 31 + {Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "session-1", AddedAt: 1000}, 32 + }, 33 + addDid: "did:plc:def456", 34 + addHandle: "bob.bsky.social", 35 + addSessionId: "session-2", 36 + wantErr: nil, 37 + wantLen: 2, 38 + wantSessionId: "session-2", 39 + }, 40 + { 41 + name: "update existing account session", 42 + initial: []AccountInfo{ 43 + {Did: "did:plc:abc123", Handle: "alice.bsky.social", SessionId: "old-session", AddedAt: 1000}, 44 + }, 45 + addDid: "did:plc:abc123", 46 + addHandle: "alice.bsky.social", 47 + addSessionId: "new-session", 48 + wantErr: nil, 49 + wantLen: 1, 50 + wantSessionId: "new-session", 51 + }, 52 + } 53 + 54 + for _, tt := range tests { 55 + t.Run(tt.name, func(t *testing.T) { 56 + registry := &AccountRegistry{Accounts: tt.initial} 57 + err := registry.AddAccount(tt.addDid, tt.addHandle, tt.addSessionId) 58 + 59 + if err != tt.wantErr { 60 + t.Errorf("AddAccount() error = %v, want %v", err, tt.wantErr) 61 + } 62 + 63 + if len(registry.Accounts) != tt.wantLen { 64 + t.Errorf("AddAccount() len = %d, want %d", len(registry.Accounts), tt.wantLen) 65 + } 66 + 67 + found := registry.FindAccount(tt.addDid) 68 + if found == nil { 69 + t.Errorf("AddAccount() account not found after add") 70 + return 71 + } 72 + 73 + if found.SessionId != tt.wantSessionId { 74 + t.Errorf("AddAccount() sessionId = %s, want %s", found.SessionId, tt.wantSessionId) 75 + } 76 + }) 77 + } 78 + } 79 + 80 + func TestAccountRegistry_AddAccount_MaxLimit(t *testing.T) { 81 + registry := &AccountRegistry{Accounts: make([]AccountInfo, 0, MaxAccounts)} 82 + 83 + for i := range MaxAccounts { 84 + err := registry.AddAccount("did:plc:user"+string(rune('a'+i)), "handle", "session") 85 + if err != nil { 86 + t.Fatalf("AddAccount() unexpected error on account %d: %v", i, err) 87 + } 88 + } 89 + 90 + if len(registry.Accounts) != MaxAccounts { 91 + t.Errorf("expected %d accounts, got %d", MaxAccounts, len(registry.Accounts)) 92 + } 93 + 94 + err := registry.AddAccount("did:plc:overflow", "overflow", "session-overflow") 95 + if err != ErrMaxAccountsReached { 96 + t.Errorf("AddAccount() error = %v, want %v", err, ErrMaxAccountsReached) 97 + } 98 + 99 + if len(registry.Accounts) != MaxAccounts { 100 + t.Errorf("account added despite max limit, got %d", len(registry.Accounts)) 101 + } 102 + } 103 + 104 + func TestAccountRegistry_RemoveAccount(t *testing.T) { 105 + tests := []struct { 106 + name string 107 + initial []AccountInfo 108 + removeDid string 109 + wantLen int 110 + wantDids []string 111 + }{ 112 + { 113 + name: "remove existing account", 114 + initial: []AccountInfo{ 115 + {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 116 + {Did: "did:plc:def456", Handle: "bob", SessionId: "s2"}, 117 + }, 118 + removeDid: "did:plc:abc123", 119 + wantLen: 1, 120 + wantDids: []string{"did:plc:def456"}, 121 + }, 122 + { 123 + name: "remove non-existing account", 124 + initial: []AccountInfo{ 125 + {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 126 + }, 127 + removeDid: "did:plc:notfound", 128 + wantLen: 1, 129 + wantDids: []string{"did:plc:abc123"}, 130 + }, 131 + { 132 + name: "remove last account", 133 + initial: []AccountInfo{ 134 + {Did: "did:plc:abc123", Handle: "alice", SessionId: "s1"}, 135 + }, 136 + removeDid: "did:plc:abc123", 137 + wantLen: 0, 138 + wantDids: []string{}, 139 + }, 140 + { 141 + name: "remove from empty registry", 142 + initial: []AccountInfo{}, 143 + removeDid: "did:plc:abc123", 144 + wantLen: 0, 145 + wantDids: []string{}, 146 + }, 147 + } 148 + 149 + for _, tt := range tests { 150 + t.Run(tt.name, func(t *testing.T) { 151 + registry := &AccountRegistry{Accounts: tt.initial} 152 + registry.RemoveAccount(tt.removeDid) 153 + 154 + if len(registry.Accounts) != tt.wantLen { 155 + t.Errorf("RemoveAccount() len = %d, want %d", len(registry.Accounts), tt.wantLen) 156 + } 157 + 158 + for _, wantDid := range tt.wantDids { 159 + if registry.FindAccount(wantDid) == nil { 160 + t.Errorf("RemoveAccount() expected %s to remain", wantDid) 161 + } 162 + } 163 + 164 + if registry.FindAccount(tt.removeDid) != nil && tt.wantLen < len(tt.initial) { 165 + t.Errorf("RemoveAccount() %s should have been removed", tt.removeDid) 166 + } 167 + }) 168 + } 169 + } 170 + 171 + func TestAccountRegistry_FindAccount(t *testing.T) { 172 + registry := &AccountRegistry{ 173 + Accounts: []AccountInfo{ 174 + {Did: "did:plc:first", Handle: "first", SessionId: "s1", AddedAt: 1000}, 175 + {Did: "did:plc:second", Handle: "second", SessionId: "s2", AddedAt: 2000}, 176 + {Did: "did:plc:third", Handle: "third", SessionId: "s3", AddedAt: 3000}, 177 + }, 178 + } 179 + 180 + t.Run("find existing account", func(t *testing.T) { 181 + found := registry.FindAccount("did:plc:second") 182 + if found == nil { 183 + t.Fatal("FindAccount() returned nil for existing account") 184 + } 185 + if found.Handle != "second" { 186 + t.Errorf("FindAccount() handle = %s, want second", found.Handle) 187 + } 188 + if found.SessionId != "s2" { 189 + t.Errorf("FindAccount() sessionId = %s, want s2", found.SessionId) 190 + } 191 + }) 192 + 193 + t.Run("find non-existing account", func(t *testing.T) { 194 + found := registry.FindAccount("did:plc:notfound") 195 + if found != nil { 196 + t.Errorf("FindAccount() = %v, want nil", found) 197 + } 198 + }) 199 + 200 + t.Run("returned pointer is mutable", func(t *testing.T) { 201 + found := registry.FindAccount("did:plc:first") 202 + if found == nil { 203 + t.Fatal("FindAccount() returned nil") 204 + } 205 + found.SessionId = "modified" 206 + 207 + refetch := registry.FindAccount("did:plc:first") 208 + if refetch.SessionId != "modified" { 209 + t.Errorf("FindAccount() pointer not referencing original, got %s", refetch.SessionId) 210 + } 211 + }) 212 + } 213 + 214 + func TestAccountRegistry_OtherAccounts(t *testing.T) { 215 + registry := &AccountRegistry{ 216 + Accounts: []AccountInfo{ 217 + {Did: "did:plc:active", Handle: "active", SessionId: "s1"}, 218 + {Did: "did:plc:other1", Handle: "other1", SessionId: "s2"}, 219 + {Did: "did:plc:other2", Handle: "other2", SessionId: "s3"}, 220 + }, 221 + } 222 + 223 + others := registry.OtherAccounts("did:plc:active") 224 + 225 + if len(others) != 2 { 226 + t.Errorf("OtherAccounts() len = %d, want 2", len(others)) 227 + } 228 + 229 + for _, acc := range others { 230 + if acc.Did == "did:plc:active" { 231 + t.Errorf("OtherAccounts() should not include active account") 232 + } 233 + } 234 + 235 + hasDid := func(did string) bool { 236 + for _, acc := range others { 237 + if acc.Did == did { 238 + return true 239 + } 240 + } 241 + return false 242 + } 243 + 244 + if !hasDid("did:plc:other1") || !hasDid("did:plc:other2") { 245 + t.Errorf("OtherAccounts() missing expected accounts") 246 + } 247 + } 248 + 249 + func TestMultiAccountUser_Did(t *testing.T) { 250 + t.Run("with active user", func(t *testing.T) { 251 + user := &MultiAccountUser{ 252 + Active: &User{Did: "did:plc:test", Pds: "https://bsky.social"}, 253 + } 254 + if user.Did() != "did:plc:test" { 255 + t.Errorf("Did() = %s, want did:plc:test", user.Did()) 256 + } 257 + }) 258 + 259 + t.Run("with nil active", func(t *testing.T) { 260 + user := &MultiAccountUser{Active: nil} 261 + if user.Did() != "" { 262 + t.Errorf("Did() = %s, want empty string", user.Did()) 263 + } 264 + }) 265 + }
+4
appview/oauth/consts.go
··· 2 2 3 3 const ( 4 4 SessionName = "appview-session-v2" 5 + AccountsName = "appview-accounts-v2" 6 + AuthReturnName = "appview-auth-return" 7 + AuthReturnURL = "return_url" 8 + AuthAddAccount = "add_account" 5 9 SessionHandle = "handle" 6 10 SessionDid = "did" 7 11 SessionId = "id"
+66 -2
appview/oauth/handler.go
··· 10 10 "slices" 11 11 "time" 12 12 13 + comatproto "github.com/bluesky-social/indigo/api/atproto" 13 14 "github.com/bluesky-social/indigo/atproto/auth/oauth" 15 + lexutil "github.com/bluesky-social/indigo/lex/util" 14 16 "github.com/go-chi/chi/v5" 15 17 "github.com/posthog/posthog-go" 16 18 "tangled.org/core/api/tangled" 17 19 "tangled.org/core/appview/db" 20 + "tangled.org/core/appview/models" 18 21 "tangled.org/core/consts" 19 22 "tangled.org/core/orm" 20 23 "tangled.org/core/tid" ··· 55 58 ctx := r.Context() 56 59 l := o.Logger.With("query", r.URL.Query()) 57 60 61 + authReturn := o.GetAuthReturn(r) 62 + _ = o.ClearAuthReturn(w, r) 63 + 58 64 sessData, err := o.ClientApp.ProcessCallback(ctx, r.URL.Query()) 59 65 if err != nil { 60 66 var callbackErr *oauth.AuthRequestCallbackError ··· 70 76 71 77 if err := o.SaveSession(w, r, sessData); err != nil { 72 78 l.Error("failed to save session", "data", sessData, "err", err) 73 - http.Redirect(w, r, "/login?error=session", http.StatusFound) 79 + errorCode := "session" 80 + if errors.Is(err, ErrMaxAccountsReached) { 81 + errorCode = "max_accounts" 82 + } 83 + http.Redirect(w, r, fmt.Sprintf("/login?error=%s", errorCode), http.StatusFound) 74 84 return 75 85 } 76 86 77 87 o.Logger.Debug("session saved successfully") 88 + 78 89 go o.addToDefaultKnot(sessData.AccountDID.String()) 79 90 go o.addToDefaultSpindle(sessData.AccountDID.String()) 91 + go o.ensureTangledProfile(sessData) 80 92 81 93 if !o.Config.Core.Dev { 82 94 err = o.Posthog.Enqueue(posthog.Capture{ ··· 88 100 } 89 101 } 90 102 91 - http.Redirect(w, r, "/", http.StatusFound) 103 + redirectURL := "/" 104 + if authReturn.ReturnURL != "" { 105 + redirectURL = authReturn.ReturnURL 106 + } 107 + 108 + http.Redirect(w, r, redirectURL, http.StatusFound) 92 109 } 93 110 94 111 func (o *OAuth) addToDefaultSpindle(did string) { ··· 175 192 } 176 193 177 194 l.Debug("successfully addeds to default Knot") 195 + } 196 + 197 + func (o *OAuth) ensureTangledProfile(sessData *oauth.ClientSessionData) { 198 + ctx := context.Background() 199 + did := sessData.AccountDID.String() 200 + l := o.Logger.With("did", did) 201 + 202 + _, err := db.GetProfile(o.Db, did) 203 + if err == nil { 204 + l.Debug("profile already exists in DB") 205 + return 206 + } 207 + 208 + l.Debug("creating empty Tangled profile") 209 + 210 + sess, err := o.ClientApp.ResumeSession(ctx, sessData.AccountDID, sessData.SessionID) 211 + if err != nil { 212 + l.Error("failed to resume session for profile creation", "err", err) 213 + return 214 + } 215 + client := sess.APIClient() 216 + 217 + _, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{ 218 + Collection: tangled.ActorProfileNSID, 219 + Repo: did, 220 + Rkey: "self", 221 + Record: &lexutil.LexiconTypeDecoder{Val: &tangled.ActorProfile{}}, 222 + }) 223 + 224 + if err != nil { 225 + l.Error("failed to create empty profile on PDS", "err", err) 226 + return 227 + } 228 + 229 + tx, err := o.Db.BeginTx(ctx, nil) 230 + if err != nil { 231 + l.Error("failed to start transaction", "err", err) 232 + return 233 + } 234 + 235 + emptyProfile := &models.Profile{Did: did} 236 + if err := db.UpsertProfile(tx, emptyProfile); err != nil { 237 + l.Error("failed to create empty profile in DB", "err", err) 238 + return 239 + } 240 + 241 + l.Debug("successfully created empty Tangled profile on PDS and DB") 178 242 } 179 243 180 244 // create a session using apppasswords
+68 -6
appview/oauth/oauth.go
··· 41 41 if config.Core.Dev { 42 42 clientUri = "http://127.0.0.1:3000" 43 43 callbackUri := clientUri + "/oauth/callback" 44 - oauthConfig = oauth.NewLocalhostConfig(callbackUri, []string{"atproto", "transition:generic"}) 44 + oauthConfig = oauth.NewLocalhostConfig(callbackUri, TangledScopes) 45 45 } else { 46 46 clientUri = config.Core.AppviewHost 47 47 clientId := fmt.Sprintf("%s/oauth/client-metadata.json", clientUri) 48 48 callbackUri := clientUri + "/oauth/callback" 49 - oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, []string{"atproto", "transition:generic"}) 49 + oauthConfig = oauth.NewPublicConfig(clientId, callbackUri, TangledScopes) 50 50 } 51 51 52 52 // configure client secret ··· 98 98 } 99 99 100 100 func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, sessData *oauth.ClientSessionData) error { 101 - // first we save the did in the user session 102 101 userSession, err := o.SessStore.Get(r, SessionName) 103 102 if err != nil { 104 103 return err ··· 108 107 userSession.Values[SessionPds] = sessData.HostURL 109 108 userSession.Values[SessionId] = sessData.SessionID 110 109 userSession.Values[SessionAuthenticated] = true 111 - return userSession.Save(r, w) 110 + 111 + if err := userSession.Save(r, w); err != nil { 112 + return err 113 + } 114 + 115 + handle := "" 116 + resolved, err := o.IdResolver.ResolveIdent(r.Context(), sessData.AccountDID.String()) 117 + if err == nil && resolved.Handle.String() != "" { 118 + handle = resolved.Handle.String() 119 + } 120 + 121 + registry := o.GetAccounts(r) 122 + if err := registry.AddAccount(sessData.AccountDID.String(), handle, sessData.SessionID); err != nil { 123 + return err 124 + } 125 + return o.SaveAccounts(w, r, registry) 112 126 } 113 127 114 128 func (o *OAuth) ResumeSession(r *http.Request) (*oauth.ClientSession, error) { ··· 163 177 return errors.Join(err1, err2) 164 178 } 165 179 180 + func (o *OAuth) SwitchAccount(w http.ResponseWriter, r *http.Request, targetDid string) error { 181 + registry := o.GetAccounts(r) 182 + account := registry.FindAccount(targetDid) 183 + if account == nil { 184 + return fmt.Errorf("account not found in registry: %s", targetDid) 185 + } 186 + 187 + did, err := syntax.ParseDID(targetDid) 188 + if err != nil { 189 + return fmt.Errorf("invalid DID: %w", err) 190 + } 191 + 192 + sess, err := o.ClientApp.ResumeSession(r.Context(), did, account.SessionId) 193 + if err != nil { 194 + registry.RemoveAccount(targetDid) 195 + _ = o.SaveAccounts(w, r, registry) 196 + return fmt.Errorf("session expired for account: %w", err) 197 + } 198 + 199 + userSession, err := o.SessStore.Get(r, SessionName) 200 + if err != nil { 201 + return err 202 + } 203 + 204 + userSession.Values[SessionDid] = sess.Data.AccountDID.String() 205 + userSession.Values[SessionPds] = sess.Data.HostURL 206 + userSession.Values[SessionId] = sess.Data.SessionID 207 + userSession.Values[SessionAuthenticated] = true 208 + 209 + return userSession.Save(r, w) 210 + } 211 + 212 + func (o *OAuth) RemoveAccount(w http.ResponseWriter, r *http.Request, targetDid string) error { 213 + registry := o.GetAccounts(r) 214 + account := registry.FindAccount(targetDid) 215 + if account == nil { 216 + return nil 217 + } 218 + 219 + did, err := syntax.ParseDID(targetDid) 220 + if err == nil { 221 + _ = o.ClientApp.Logout(r.Context(), did, account.SessionId) 222 + } 223 + 224 + registry.RemoveAccount(targetDid) 225 + return o.SaveAccounts(w, r, registry) 226 + } 227 + 166 228 type User struct { 167 229 Did string 168 230 Pds string ··· 181 243 } 182 244 183 245 func (o *OAuth) GetDid(r *http.Request) string { 184 - if u := o.GetUser(r); u != nil { 185 - return u.Did 246 + if u := o.GetMultiAccountUser(r); u != nil { 247 + return u.Did() 186 248 } 187 249 188 250 return ""
+41
appview/oauth/scopes.go
··· 1 + package oauth 2 + 3 + var TangledScopes = []string{ 4 + "atproto", 5 + 6 + "repo:sh.tangled.publicKey", 7 + "repo:sh.tangled.repo", 8 + "repo:sh.tangled.repo.pull", 9 + "repo:sh.tangled.repo.pull.comment", 10 + "repo:sh.tangled.repo.artifact", 11 + "repo:sh.tangled.repo.issue", 12 + "repo:sh.tangled.repo.issue.comment", 13 + "repo:sh.tangled.repo.collaborator", 14 + "repo:sh.tangled.knot", 15 + "repo:sh.tangled.knot.member", 16 + "repo:sh.tangled.spindle", 17 + "repo:sh.tangled.spindle.member", 18 + "repo:sh.tangled.graph.follow", 19 + "repo:sh.tangled.feed.star", 20 + "repo:sh.tangled.feed.reaction", 21 + "repo:sh.tangled.label.definition", 22 + "repo:sh.tangled.label.op", 23 + "repo:sh.tangled.string", 24 + "repo:sh.tangled.actor.profile", 25 + 26 + "blob:*/*", 27 + 28 + "rpc:sh.tangled.repo.create?aud=*", 29 + "rpc:sh.tangled.repo.delete?aud=*", 30 + "rpc:sh.tangled.repo.merge?aud=*", 31 + "rpc:sh.tangled.repo.hiddenRef?aud=*", 32 + "rpc:sh.tangled.repo.deleteBranch?aud=*", 33 + "rpc:sh.tangled.repo.setDefaultBranch?aud=*", 34 + "rpc:sh.tangled.repo.forkSync?aud=*", 35 + "rpc:sh.tangled.repo.forkStatus?aud=*", 36 + "rpc:sh.tangled.repo.mergeCheck?aud=*", 37 + "rpc:sh.tangled.pipeline.cancelPipeline?aud=*", 38 + "rpc:sh.tangled.repo.addSecret?aud=*", 39 + "rpc:sh.tangled.repo.removeSecret?aud=*", 40 + "rpc:sh.tangled.repo.listSecrets?aud=*", 41 + }
+123 -15
appview/pages/funcmap.go
··· 26 26 "github.com/go-enry/go-enry/v2" 27 27 "github.com/yuin/goldmark" 28 28 emoji "github.com/yuin/goldmark-emoji" 29 - "tangled.org/core/appview/filetree" 29 + "tangled.org/core/appview/db" 30 30 "tangled.org/core/appview/models" 31 + "tangled.org/core/appview/oauth" 31 32 "tangled.org/core/appview/pages/markup" 32 33 "tangled.org/core/crypto" 33 34 ) 35 + 36 + type tab map[string]string 34 37 35 38 func (p *Pages) funcMap() template.FuncMap { 36 39 return template.FuncMap{ ··· 332 335 } 333 336 return dict, nil 334 337 }, 338 + "queryParams": func(params ...any) (url.Values, error) { 339 + if len(params)%2 != 0 { 340 + return nil, errors.New("invalid queryParams call") 341 + } 342 + vals := make(url.Values, len(params)/2) 343 + for i := 0; i < len(params); i += 2 { 344 + key, ok := params[i].(string) 345 + if !ok { 346 + return nil, errors.New("queryParams keys must be strings") 347 + } 348 + v, ok := params[i+1].(string) 349 + if !ok { 350 + return nil, errors.New("queryParams values must be strings") 351 + } 352 + vals.Add(key, v) 353 + } 354 + return vals, nil 355 + }, 335 356 "deref": func(v any) any { 336 357 val := reflect.ValueOf(v) 337 - if val.Kind() == reflect.Ptr && !val.IsNil() { 358 + if val.Kind() == reflect.Pointer && !val.IsNil() { 338 359 return val.Elem().Interface() 339 360 } 340 361 return nil ··· 348 369 return template.HTML(data) 349 370 }, 350 371 "cssContentHash": p.CssContentHash, 351 - "fileTree": filetree.FileTree, 352 372 "pathEscape": func(s string) string { 353 373 return url.PathEscape(s) 354 374 }, ··· 365 385 "fullAvatar": func(handle string) string { 366 386 return p.AvatarUrl(handle, "") 367 387 }, 368 - "langColor": enry.GetColor, 369 - "layoutSide": func() string { 370 - return "col-span-1 md:col-span-2 lg:col-span-3" 388 + "placeholderAvatar": func(size string) template.HTML { 389 + sizeClass := "size-6" 390 + iconSize := "size-4" 391 + if size == "tiny" { 392 + sizeClass = "size-6" 393 + iconSize = "size-4" 394 + } else if size == "small" { 395 + sizeClass = "size-8" 396 + iconSize = "size-5" 397 + } else { 398 + sizeClass = "size-12" 399 + iconSize = "size-8" 400 + } 401 + icon, _ := p.icon("user-round", []string{iconSize, "text-gray-400", "dark:text-gray-500"}) 402 + return template.HTML(fmt.Sprintf(`<div class="%s rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">%s</div>`, sizeClass, icon)) 371 403 }, 372 - "layoutCenter": func() string { 373 - return "col-span-1 md:col-span-8 lg:col-span-6" 404 + "profileAvatarUrl": func(profile *models.Profile, size string) string { 405 + if profile != nil { 406 + return p.AvatarUrl(profile.Did, size) 407 + } 408 + return "" 374 409 }, 410 + "langColor": enry.GetColor, 411 + "reverse": func(s any) any { 412 + if s == nil { 413 + return nil 414 + } 375 415 416 + v := reflect.ValueOf(s) 417 + 418 + if v.Kind() != reflect.Slice { 419 + return s 420 + } 421 + 422 + length := v.Len() 423 + reversed := reflect.MakeSlice(v.Type(), length, length) 424 + 425 + for i := range length { 426 + reversed.Index(i).Set(v.Index(length - 1 - i)) 427 + } 428 + 429 + return reversed.Interface() 430 + }, 376 431 "normalizeForHtmlId": func(s string) string { 377 432 normalized := strings.ReplaceAll(s, ":", "_") 378 433 normalized = strings.ReplaceAll(normalized, ".", "_") ··· 385 440 } 386 441 return fp 387 442 }, 443 + "otherAccounts": func(activeDid string, accounts []oauth.AccountInfo) []oauth.AccountInfo { 444 + result := make([]oauth.AccountInfo, 0, len(accounts)) 445 + for _, acc := range accounts { 446 + if acc.Did != activeDid { 447 + result = append(result, acc) 448 + } 449 + } 450 + return result 451 + }, 452 + // constant values used to define a template 453 + "const": func() map[string]any { 454 + return map[string]any{ 455 + "OrderedReactionKinds": models.OrderedReactionKinds, 456 + // would be great to have ordered maps right about now 457 + "UserSettingsTabs": []tab{ 458 + {"Name": "profile", "Icon": "user"}, 459 + {"Name": "keys", "Icon": "key"}, 460 + {"Name": "emails", "Icon": "mail"}, 461 + {"Name": "notifications", "Icon": "bell"}, 462 + {"Name": "knots", "Icon": "volleyball"}, 463 + {"Name": "spindles", "Icon": "spool"}, 464 + }, 465 + "RepoSettingsTabs": []tab{ 466 + {"Name": "general", "Icon": "sliders-horizontal"}, 467 + {"Name": "access", "Icon": "users"}, 468 + {"Name": "pipelines", "Icon": "layers-2"}, 469 + }, 470 + } 471 + }, 388 472 } 389 473 } 390 474 ··· 402 486 return identity.Handle.String() 403 487 } 404 488 405 - func (p *Pages) AvatarUrl(handle, size string) string { 406 - handle = strings.TrimPrefix(handle, "@") 489 + func (p *Pages) AvatarUrl(actor, size string) string { 490 + actor = strings.TrimPrefix(actor, "@") 407 491 408 - handle = p.resolveDid(handle) 492 + identity, err := p.resolver.ResolveIdent(context.Background(), actor) 493 + var did string 494 + if err != nil { 495 + did = actor 496 + } else { 497 + did = identity.DID.String() 498 + } 409 499 410 500 secret := p.avatar.SharedSecret 411 501 h := hmac.New(sha256.New, []byte(secret)) 412 - h.Write([]byte(handle)) 502 + h.Write([]byte(did)) 413 503 signature := hex.EncodeToString(h.Sum(nil)) 414 504 415 - sizeArg := "" 505 + // Get avatar CID for cache busting 506 + profile, err := db.GetProfile(p.db, did) 507 + version := "" 508 + if err == nil && profile != nil && profile.Avatar != "" { 509 + // Use first 8 chars of avatar CID as version 510 + if len(profile.Avatar) > 8 { 511 + version = profile.Avatar[:8] 512 + } else { 513 + version = profile.Avatar 514 + } 515 + } 516 + 517 + baseUrl := fmt.Sprintf("%s/%s/%s", p.avatar.Host, signature, did) 416 518 if size != "" { 417 - sizeArg = fmt.Sprintf("size=%s", size) 519 + if version != "" { 520 + return fmt.Sprintf("%s?size=%s&v=%s", baseUrl, size, version) 521 + } 522 + return fmt.Sprintf("%s?size=%s", baseUrl, size) 418 523 } 419 - return fmt.Sprintf("%s/%s/%s?%s", p.avatar.Host, signature, handle, sizeArg) 524 + if version != "" { 525 + return fmt.Sprintf("%s?v=%s", baseUrl, version) 526 + } 527 + return baseUrl 420 528 } 421 529 422 530 func (p *Pages) icon(name string, classes []string) (template.HTML, error) {
+1 -1
appview/pages/funcmap_test.go
··· 22 22 } 23 23 for _, tt := range tests { 24 24 t.Run(tt.name, func(t *testing.T) { 25 - p := NewPages(tt.config, tt.res, tt.l) 25 + p := NewPages(tt.config, tt.res, nil, tt.l) 26 26 got := p.funcMap() 27 27 // TODO: update the condition below to compare got with tt.want. 28 28 if true {
+102 -100
appview/pages/pages.go
··· 19 19 "tangled.org/core/api/tangled" 20 20 "tangled.org/core/appview/commitverify" 21 21 "tangled.org/core/appview/config" 22 + "tangled.org/core/appview/db" 22 23 "tangled.org/core/appview/models" 23 24 "tangled.org/core/appview/oauth" 24 25 "tangled.org/core/appview/pages/markup" ··· 42 43 43 44 avatar config.AvatarConfig 44 45 resolver *idresolver.Resolver 46 + db *db.DB 45 47 dev bool 46 48 embedFS fs.FS 47 49 templateDir string // Path to templates on disk for dev mode ··· 49 51 logger *slog.Logger 50 52 } 51 53 52 - func NewPages(config *config.Config, res *idresolver.Resolver, logger *slog.Logger) *Pages { 54 + func NewPages(config *config.Config, res *idresolver.Resolver, database *db.DB, logger *slog.Logger) *Pages { 53 55 // initialized with safe defaults, can be overriden per use 54 56 rctx := &markup.RenderContext{ 55 57 IsDev: config.Core.Dev, ··· 66 68 avatar: config.Avatar, 67 69 rctx: rctx, 68 70 resolver: res, 71 + db: database, 69 72 templateDir: "appview/pages", 70 73 logger: logger, 71 74 } ··· 226 229 } 227 230 228 231 type LoginParams struct { 229 - ReturnUrl string 230 - ErrorCode string 232 + ReturnUrl string 233 + ErrorCode string 234 + AddAccount bool 235 + LoggedInUser *oauth.MultiAccountUser 231 236 } 232 237 233 238 func (p *Pages) Login(w io.Writer, params LoginParams) error { ··· 247 252 } 248 253 249 254 type TermsOfServiceParams struct { 250 - LoggedInUser *oauth.User 255 + LoggedInUser *oauth.MultiAccountUser 251 256 Content template.HTML 252 257 } 253 258 ··· 275 280 } 276 281 277 282 type PrivacyPolicyParams struct { 278 - LoggedInUser *oauth.User 283 + LoggedInUser *oauth.MultiAccountUser 279 284 Content template.HTML 280 285 } 281 286 ··· 303 308 } 304 309 305 310 type BrandParams struct { 306 - LoggedInUser *oauth.User 311 + LoggedInUser *oauth.MultiAccountUser 307 312 } 308 313 309 314 func (p *Pages) Brand(w io.Writer, params BrandParams) error { ··· 311 316 } 312 317 313 318 type TimelineParams struct { 314 - LoggedInUser *oauth.User 319 + LoggedInUser *oauth.MultiAccountUser 315 320 Timeline []models.TimelineEvent 316 321 Repos []models.Repo 317 322 GfiLabel *models.LabelDefinition ··· 322 327 } 323 328 324 329 type GoodFirstIssuesParams struct { 325 - LoggedInUser *oauth.User 330 + LoggedInUser *oauth.MultiAccountUser 326 331 Issues []models.Issue 327 332 RepoGroups []*models.RepoGroup 328 333 LabelDefs map[string]*models.LabelDefinition ··· 335 340 } 336 341 337 342 type UserProfileSettingsParams struct { 338 - LoggedInUser *oauth.User 339 - Tabs []map[string]any 343 + LoggedInUser *oauth.MultiAccountUser 340 344 Tab string 341 345 } 342 346 343 347 func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { 348 + params.Tab = "profile" 344 349 return p.execute("user/settings/profile", w, params) 345 350 } 346 351 347 352 type NotificationsParams struct { 348 - LoggedInUser *oauth.User 353 + LoggedInUser *oauth.MultiAccountUser 349 354 Notifications []*models.NotificationWithEntity 350 355 UnreadCount int 351 356 Page pagination.Page ··· 373 378 } 374 379 375 380 type UserKeysSettingsParams struct { 376 - LoggedInUser *oauth.User 381 + LoggedInUser *oauth.MultiAccountUser 377 382 PubKeys []models.PublicKey 378 - Tabs []map[string]any 379 383 Tab string 380 384 } 381 385 382 386 func (p *Pages) UserKeysSettings(w io.Writer, params UserKeysSettingsParams) error { 387 + params.Tab = "keys" 383 388 return p.execute("user/settings/keys", w, params) 384 389 } 385 390 386 391 type UserEmailsSettingsParams struct { 387 - LoggedInUser *oauth.User 392 + LoggedInUser *oauth.MultiAccountUser 388 393 Emails []models.Email 389 - Tabs []map[string]any 390 394 Tab string 391 395 } 392 396 393 397 func (p *Pages) UserEmailsSettings(w io.Writer, params UserEmailsSettingsParams) error { 398 + params.Tab = "emails" 394 399 return p.execute("user/settings/emails", w, params) 395 400 } 396 401 397 402 type UserNotificationSettingsParams struct { 398 - LoggedInUser *oauth.User 403 + LoggedInUser *oauth.MultiAccountUser 399 404 Preferences *models.NotificationPreferences 400 - Tabs []map[string]any 401 405 Tab string 402 406 } 403 407 404 408 func (p *Pages) UserNotificationSettings(w io.Writer, params UserNotificationSettingsParams) error { 409 + params.Tab = "notifications" 405 410 return p.execute("user/settings/notifications", w, params) 406 411 } 407 412 ··· 415 420 } 416 421 417 422 type KnotsParams struct { 418 - LoggedInUser *oauth.User 423 + LoggedInUser *oauth.MultiAccountUser 419 424 Registrations []models.Registration 420 - Tabs []map[string]any 421 425 Tab string 422 426 } 423 427 424 428 func (p *Pages) Knots(w io.Writer, params KnotsParams) error { 429 + params.Tab = "knots" 425 430 return p.execute("knots/index", w, params) 426 431 } 427 432 428 433 type KnotParams struct { 429 - LoggedInUser *oauth.User 434 + LoggedInUser *oauth.MultiAccountUser 430 435 Registration *models.Registration 431 436 Members []string 432 437 Repos map[string][]models.Repo 433 438 IsOwner bool 434 - Tabs []map[string]any 435 439 Tab string 436 440 } 437 441 ··· 448 452 } 449 453 450 454 type SpindlesParams struct { 451 - LoggedInUser *oauth.User 455 + LoggedInUser *oauth.MultiAccountUser 452 456 Spindles []models.Spindle 453 - Tabs []map[string]any 454 457 Tab string 455 458 } 456 459 457 460 func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error { 461 + params.Tab = "spindles" 458 462 return p.execute("spindles/index", w, params) 459 463 } 460 464 461 465 type SpindleListingParams struct { 462 466 models.Spindle 463 - Tabs []map[string]any 464 - Tab string 467 + Tab string 465 468 } 466 469 467 470 func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error { ··· 469 472 } 470 473 471 474 type SpindleDashboardParams struct { 472 - LoggedInUser *oauth.User 475 + LoggedInUser *oauth.MultiAccountUser 473 476 Spindle models.Spindle 474 477 Members []string 475 478 Repos map[string][]models.Repo 476 - Tabs []map[string]any 477 479 Tab string 478 480 } 479 481 ··· 482 484 } 483 485 484 486 type NewRepoParams struct { 485 - LoggedInUser *oauth.User 487 + LoggedInUser *oauth.MultiAccountUser 486 488 Knots []string 487 489 } 488 490 ··· 491 493 } 492 494 493 495 type ForkRepoParams struct { 494 - LoggedInUser *oauth.User 496 + LoggedInUser *oauth.MultiAccountUser 495 497 Knots []string 496 498 RepoInfo repoinfo.RepoInfo 497 499 } ··· 529 531 } 530 532 531 533 type ProfileOverviewParams struct { 532 - LoggedInUser *oauth.User 534 + LoggedInUser *oauth.MultiAccountUser 533 535 Repos []models.Repo 534 536 CollaboratingRepos []models.Repo 535 537 ProfileTimeline *models.ProfileTimeline ··· 543 545 } 544 546 545 547 type ProfileReposParams struct { 546 - LoggedInUser *oauth.User 548 + LoggedInUser *oauth.MultiAccountUser 547 549 Repos []models.Repo 548 550 Card *ProfileCard 549 551 Active string ··· 555 557 } 556 558 557 559 type ProfileStarredParams struct { 558 - LoggedInUser *oauth.User 560 + LoggedInUser *oauth.MultiAccountUser 559 561 Repos []models.Repo 560 562 Card *ProfileCard 561 563 Active string ··· 567 569 } 568 570 569 571 type ProfileStringsParams struct { 570 - LoggedInUser *oauth.User 572 + LoggedInUser *oauth.MultiAccountUser 571 573 Strings []models.String 572 574 Card *ProfileCard 573 575 Active string ··· 580 582 581 583 type FollowCard struct { 582 584 UserDid string 583 - LoggedInUser *oauth.User 585 + LoggedInUser *oauth.MultiAccountUser 584 586 FollowStatus models.FollowStatus 585 587 FollowersCount int64 586 588 FollowingCount int64 ··· 588 590 } 589 591 590 592 type ProfileFollowersParams struct { 591 - LoggedInUser *oauth.User 593 + LoggedInUser *oauth.MultiAccountUser 592 594 Followers []FollowCard 593 595 Card *ProfileCard 594 596 Active string ··· 600 602 } 601 603 602 604 type ProfileFollowingParams struct { 603 - LoggedInUser *oauth.User 605 + LoggedInUser *oauth.MultiAccountUser 604 606 Following []FollowCard 605 607 Card *ProfileCard 606 608 Active string ··· 622 624 } 623 625 624 626 type EditBioParams struct { 625 - LoggedInUser *oauth.User 627 + LoggedInUser *oauth.MultiAccountUser 626 628 Profile *models.Profile 627 629 } 628 630 ··· 631 633 } 632 634 633 635 type EditPinsParams struct { 634 - LoggedInUser *oauth.User 636 + LoggedInUser *oauth.MultiAccountUser 635 637 Profile *models.Profile 636 638 AllRepos []PinnedRepo 637 639 } ··· 658 660 } 659 661 660 662 type RepoIndexParams struct { 661 - LoggedInUser *oauth.User 663 + LoggedInUser *oauth.MultiAccountUser 662 664 RepoInfo repoinfo.RepoInfo 663 665 Active string 664 666 TagMap map[string][]string ··· 707 709 } 708 710 709 711 type RepoLogParams struct { 710 - LoggedInUser *oauth.User 712 + LoggedInUser *oauth.MultiAccountUser 711 713 RepoInfo repoinfo.RepoInfo 712 714 TagMap map[string][]string 713 715 Active string ··· 724 726 } 725 727 726 728 type RepoCommitParams struct { 727 - LoggedInUser *oauth.User 729 + LoggedInUser *oauth.MultiAccountUser 728 730 RepoInfo repoinfo.RepoInfo 729 731 Active string 730 732 EmailToDid map[string]string ··· 743 745 } 744 746 745 747 type RepoTreeParams struct { 746 - LoggedInUser *oauth.User 748 + LoggedInUser *oauth.MultiAccountUser 747 749 RepoInfo repoinfo.RepoInfo 748 750 Active string 749 751 BreadCrumbs [][]string ··· 798 800 } 799 801 800 802 type RepoBranchesParams struct { 801 - LoggedInUser *oauth.User 803 + LoggedInUser *oauth.MultiAccountUser 802 804 RepoInfo repoinfo.RepoInfo 803 805 Active string 804 806 types.RepoBranchesResponse ··· 810 812 } 811 813 812 814 type RepoTagsParams struct { 813 - LoggedInUser *oauth.User 815 + LoggedInUser *oauth.MultiAccountUser 814 816 RepoInfo repoinfo.RepoInfo 815 817 Active string 816 818 types.RepoTagsResponse ··· 824 826 } 825 827 826 828 type RepoArtifactParams struct { 827 - LoggedInUser *oauth.User 829 + LoggedInUser *oauth.MultiAccountUser 828 830 RepoInfo repoinfo.RepoInfo 829 831 Artifact models.Artifact 830 832 } ··· 834 836 } 835 837 836 838 type RepoBlobParams struct { 837 - LoggedInUser *oauth.User 839 + LoggedInUser *oauth.MultiAccountUser 838 840 RepoInfo repoinfo.RepoInfo 839 841 Active string 840 842 BreadCrumbs [][]string ··· 858 860 } 859 861 860 862 type RepoSettingsParams struct { 861 - LoggedInUser *oauth.User 863 + LoggedInUser *oauth.MultiAccountUser 862 864 RepoInfo repoinfo.RepoInfo 863 865 Collaborators []Collaborator 864 866 Active string ··· 877 879 } 878 880 879 881 type RepoGeneralSettingsParams struct { 880 - LoggedInUser *oauth.User 882 + LoggedInUser *oauth.MultiAccountUser 881 883 RepoInfo repoinfo.RepoInfo 882 884 Labels []models.LabelDefinition 883 885 DefaultLabels []models.LabelDefinition 884 886 SubscribedLabels map[string]struct{} 885 887 ShouldSubscribeAll bool 886 888 Active string 887 - Tabs []map[string]any 888 889 Tab string 889 890 Branches []types.Branch 890 891 } 891 892 892 893 func (p *Pages) RepoGeneralSettings(w io.Writer, params RepoGeneralSettingsParams) error { 893 894 params.Active = "settings" 895 + params.Tab = "general" 894 896 return p.executeRepo("repo/settings/general", w, params) 895 897 } 896 898 897 899 type RepoAccessSettingsParams struct { 898 - LoggedInUser *oauth.User 900 + LoggedInUser *oauth.MultiAccountUser 899 901 RepoInfo repoinfo.RepoInfo 900 902 Active string 901 - Tabs []map[string]any 902 903 Tab string 903 904 Collaborators []Collaborator 904 905 } 905 906 906 907 func (p *Pages) RepoAccessSettings(w io.Writer, params RepoAccessSettingsParams) error { 907 908 params.Active = "settings" 909 + params.Tab = "access" 908 910 return p.executeRepo("repo/settings/access", w, params) 909 911 } 910 912 911 913 type RepoPipelineSettingsParams struct { 912 - LoggedInUser *oauth.User 914 + LoggedInUser *oauth.MultiAccountUser 913 915 RepoInfo repoinfo.RepoInfo 914 916 Active string 915 - Tabs []map[string]any 916 917 Tab string 917 918 Spindles []string 918 919 CurrentSpindle string ··· 921 922 922 923 func (p *Pages) RepoPipelineSettings(w io.Writer, params RepoPipelineSettingsParams) error { 923 924 params.Active = "settings" 925 + params.Tab = "pipelines" 924 926 return p.executeRepo("repo/settings/pipelines", w, params) 925 927 } 926 928 927 929 type RepoIssuesParams struct { 928 - LoggedInUser *oauth.User 930 + LoggedInUser *oauth.MultiAccountUser 929 931 RepoInfo repoinfo.RepoInfo 930 932 Active string 931 933 Issues []models.Issue ··· 942 944 } 943 945 944 946 type RepoSingleIssueParams struct { 945 - LoggedInUser *oauth.User 947 + LoggedInUser *oauth.MultiAccountUser 946 948 RepoInfo repoinfo.RepoInfo 947 949 Active string 948 950 Issue *models.Issue ··· 950 952 Backlinks []models.RichReferenceLink 951 953 LabelDefs map[string]*models.LabelDefinition 952 954 953 - OrderedReactionKinds []models.ReactionKind 954 - Reactions map[models.ReactionKind]models.ReactionDisplayData 955 - UserReacted map[models.ReactionKind]bool 955 + Reactions map[models.ReactionKind]models.ReactionDisplayData 956 + UserReacted map[models.ReactionKind]bool 956 957 } 957 958 958 959 func (p *Pages) RepoSingleIssue(w io.Writer, params RepoSingleIssueParams) error { ··· 961 962 } 962 963 963 964 type EditIssueParams struct { 964 - LoggedInUser *oauth.User 965 + LoggedInUser *oauth.MultiAccountUser 965 966 RepoInfo repoinfo.RepoInfo 966 967 Issue *models.Issue 967 968 Action string ··· 985 986 } 986 987 987 988 type RepoNewIssueParams struct { 988 - LoggedInUser *oauth.User 989 + LoggedInUser *oauth.MultiAccountUser 989 990 RepoInfo repoinfo.RepoInfo 990 991 Issue *models.Issue // existing issue if any -- passed when editing 991 992 Active string ··· 999 1000 } 1000 1001 1001 1002 type EditIssueCommentParams struct { 1002 - LoggedInUser *oauth.User 1003 + LoggedInUser *oauth.MultiAccountUser 1003 1004 RepoInfo repoinfo.RepoInfo 1004 1005 Issue *models.Issue 1005 1006 Comment *models.IssueComment ··· 1010 1011 } 1011 1012 1012 1013 type ReplyIssueCommentPlaceholderParams struct { 1013 - LoggedInUser *oauth.User 1014 + LoggedInUser *oauth.MultiAccountUser 1014 1015 RepoInfo repoinfo.RepoInfo 1015 1016 Issue *models.Issue 1016 1017 Comment *models.IssueComment ··· 1021 1022 } 1022 1023 1023 1024 type ReplyIssueCommentParams struct { 1024 - LoggedInUser *oauth.User 1025 + LoggedInUser *oauth.MultiAccountUser 1025 1026 RepoInfo repoinfo.RepoInfo 1026 1027 Issue *models.Issue 1027 1028 Comment *models.IssueComment ··· 1032 1033 } 1033 1034 1034 1035 type IssueCommentBodyParams struct { 1035 - LoggedInUser *oauth.User 1036 + LoggedInUser *oauth.MultiAccountUser 1036 1037 RepoInfo repoinfo.RepoInfo 1037 1038 Issue *models.Issue 1038 1039 Comment *models.IssueComment ··· 1043 1044 } 1044 1045 1045 1046 type RepoNewPullParams struct { 1046 - LoggedInUser *oauth.User 1047 + LoggedInUser *oauth.MultiAccountUser 1047 1048 RepoInfo repoinfo.RepoInfo 1048 1049 Branches []types.Branch 1049 1050 Strategy string ··· 1060 1061 } 1061 1062 1062 1063 type RepoPullsParams struct { 1063 - LoggedInUser *oauth.User 1064 + LoggedInUser *oauth.MultiAccountUser 1064 1065 RepoInfo repoinfo.RepoInfo 1065 1066 Pulls []*models.Pull 1066 1067 Active string ··· 1097 1098 } 1098 1099 1099 1100 type RepoSinglePullParams struct { 1100 - LoggedInUser *oauth.User 1101 + LoggedInUser *oauth.MultiAccountUser 1101 1102 RepoInfo repoinfo.RepoInfo 1102 1103 Active string 1103 1104 Pull *models.Pull ··· 1108 1109 MergeCheck types.MergeCheckResponse 1109 1110 ResubmitCheck ResubmitResult 1110 1111 Pipelines map[string]models.Pipeline 1112 + Diff types.DiffRenderer 1113 + DiffOpts types.DiffOpts 1114 + ActiveRound int 1115 + IsInterdiff bool 1111 1116 1112 - OrderedReactionKinds []models.ReactionKind 1113 - Reactions map[models.ReactionKind]models.ReactionDisplayData 1114 - UserReacted map[models.ReactionKind]bool 1117 + Reactions map[models.ReactionKind]models.ReactionDisplayData 1118 + UserReacted map[models.ReactionKind]bool 1115 1119 1116 1120 LabelDefs map[string]*models.LabelDefinition 1117 1121 } ··· 1122 1126 } 1123 1127 1124 1128 type RepoPullPatchParams struct { 1125 - LoggedInUser *oauth.User 1126 - RepoInfo repoinfo.RepoInfo 1127 - Pull *models.Pull 1128 - Stack models.Stack 1129 - Diff *types.NiceDiff 1130 - Round int 1131 - Submission *models.PullSubmission 1132 - OrderedReactionKinds []models.ReactionKind 1133 - DiffOpts types.DiffOpts 1129 + LoggedInUser *oauth.MultiAccountUser 1130 + RepoInfo repoinfo.RepoInfo 1131 + Pull *models.Pull 1132 + Stack models.Stack 1133 + Diff *types.NiceDiff 1134 + Round int 1135 + Submission *models.PullSubmission 1136 + DiffOpts types.DiffOpts 1134 1137 } 1135 1138 1136 1139 // this name is a mouthful ··· 1139 1142 } 1140 1143 1141 1144 type RepoPullInterdiffParams struct { 1142 - LoggedInUser *oauth.User 1143 - RepoInfo repoinfo.RepoInfo 1144 - Pull *models.Pull 1145 - Round int 1146 - Interdiff *patchutil.InterdiffResult 1147 - OrderedReactionKinds []models.ReactionKind 1148 - DiffOpts types.DiffOpts 1145 + LoggedInUser *oauth.MultiAccountUser 1146 + RepoInfo repoinfo.RepoInfo 1147 + Pull *models.Pull 1148 + Round int 1149 + Interdiff *patchutil.InterdiffResult 1150 + DiffOpts types.DiffOpts 1149 1151 } 1150 1152 1151 1153 // this name is a mouthful ··· 1192 1194 } 1193 1195 1194 1196 type PullResubmitParams struct { 1195 - LoggedInUser *oauth.User 1197 + LoggedInUser *oauth.MultiAccountUser 1196 1198 RepoInfo repoinfo.RepoInfo 1197 1199 Pull *models.Pull 1198 1200 SubmissionId int ··· 1203 1205 } 1204 1206 1205 1207 type PullActionsParams struct { 1206 - LoggedInUser *oauth.User 1208 + LoggedInUser *oauth.MultiAccountUser 1207 1209 RepoInfo repoinfo.RepoInfo 1208 1210 Pull *models.Pull 1209 1211 RoundNumber int ··· 1218 1220 } 1219 1221 1220 1222 type PullNewCommentParams struct { 1221 - LoggedInUser *oauth.User 1223 + LoggedInUser *oauth.MultiAccountUser 1222 1224 RepoInfo repoinfo.RepoInfo 1223 1225 Pull *models.Pull 1224 1226 RoundNumber int ··· 1229 1231 } 1230 1232 1231 1233 type RepoCompareParams struct { 1232 - LoggedInUser *oauth.User 1234 + LoggedInUser *oauth.MultiAccountUser 1233 1235 RepoInfo repoinfo.RepoInfo 1234 1236 Forks []models.Repo 1235 1237 Branches []types.Branch ··· 1248 1250 } 1249 1251 1250 1252 type RepoCompareNewParams struct { 1251 - LoggedInUser *oauth.User 1253 + LoggedInUser *oauth.MultiAccountUser 1252 1254 RepoInfo repoinfo.RepoInfo 1253 1255 Forks []models.Repo 1254 1256 Branches []types.Branch ··· 1265 1267 } 1266 1268 1267 1269 type RepoCompareAllowPullParams struct { 1268 - LoggedInUser *oauth.User 1270 + LoggedInUser *oauth.MultiAccountUser 1269 1271 RepoInfo repoinfo.RepoInfo 1270 1272 Base string 1271 1273 Head string ··· 1285 1287 } 1286 1288 1287 1289 type LabelPanelParams struct { 1288 - LoggedInUser *oauth.User 1290 + LoggedInUser *oauth.MultiAccountUser 1289 1291 RepoInfo repoinfo.RepoInfo 1290 1292 Defs map[string]*models.LabelDefinition 1291 1293 Subject string ··· 1297 1299 } 1298 1300 1299 1301 type EditLabelPanelParams struct { 1300 - LoggedInUser *oauth.User 1302 + LoggedInUser *oauth.MultiAccountUser 1301 1303 RepoInfo repoinfo.RepoInfo 1302 1304 Defs map[string]*models.LabelDefinition 1303 1305 Subject string ··· 1309 1311 } 1310 1312 1311 1313 type PipelinesParams struct { 1312 - LoggedInUser *oauth.User 1314 + LoggedInUser *oauth.MultiAccountUser 1313 1315 RepoInfo repoinfo.RepoInfo 1314 1316 Pipelines []models.Pipeline 1315 1317 Active string ··· 1352 1354 } 1353 1355 1354 1356 type WorkflowParams struct { 1355 - LoggedInUser *oauth.User 1357 + LoggedInUser *oauth.MultiAccountUser 1356 1358 RepoInfo repoinfo.RepoInfo 1357 1359 Pipeline models.Pipeline 1358 1360 Workflow string ··· 1366 1368 } 1367 1369 1368 1370 type PutStringParams struct { 1369 - LoggedInUser *oauth.User 1371 + LoggedInUser *oauth.MultiAccountUser 1370 1372 Action string 1371 1373 1372 1374 // this is supplied in the case of editing an existing string ··· 1378 1380 } 1379 1381 1380 1382 type StringsDashboardParams struct { 1381 - LoggedInUser *oauth.User 1383 + LoggedInUser *oauth.MultiAccountUser 1382 1384 Card ProfileCard 1383 1385 Strings []models.String 1384 1386 } ··· 1388 1390 } 1389 1391 1390 1392 type StringTimelineParams struct { 1391 - LoggedInUser *oauth.User 1393 + LoggedInUser *oauth.MultiAccountUser 1392 1394 Strings []models.String 1393 1395 } 1394 1396 ··· 1397 1399 } 1398 1400 1399 1401 type SingleStringParams struct { 1400 - LoggedInUser *oauth.User 1402 + LoggedInUser *oauth.MultiAccountUser 1401 1403 ShowRendered bool 1402 1404 RenderToggle bool 1403 1405 RenderedContents template.HTML
+1 -1
appview/pages/templates/banner.html
··· 30 30 <div class="mx-6"> 31 31 These services may not be fully accessible until upgraded. 32 32 <a class="underline text-red-800 dark:text-red-200" 33 - href="https://docs.tangled.org/migrating-knots-spindles.html#migrating-knots-spindles"> 33 + href="https://docs.tangled.org/migrating-knots-and-spindles.html"> 34 34 Click to read the upgrade guide</a>. 35 35 </div> 36 36 </details>
+2 -2
appview/pages/templates/fragments/pagination.html
··· 1 1 {{ define "fragments/pagination" }} 2 - {{/* Params: Page (pagination.Page), TotalCount (int), BasePath (string), QueryParams (string) */}} 2 + {{/* Params: Page (pagination.Page), TotalCount (int), BasePath (string), QueryParams (url.Values) */}} 3 3 {{ $page := .Page }} 4 4 {{ $totalCount := .TotalCount }} 5 5 {{ $basePath := .BasePath }} 6 - {{ $queryParams := .QueryParams }} 6 + {{ $queryParams := safeUrl .QueryParams.Encode }} 7 7 8 8 {{ $prev := $page.Previous.Offset }} 9 9 {{ $next := $page.Next.Offset }}
+1 -1
appview/pages/templates/labels/fragments/label.html
··· 24 24 {{ $rhs = printf "%s" $v }} 25 25 {{ end }} 26 26 27 - {{ $chipClasses := "w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm" }} 27 + {{ $chipClasses := "w-fit flex items-center gap-2 font-normal normal-case rounded py-1 px-2 border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-sm text-inherit" }} 28 28 29 29 {{ if $isDid }} 30 30 <a href="/{{ $resolvedVal }}" class="{{ $chipClasses }} no-underline hover:underline">
+56 -11
appview/pages/templates/layouts/fragments/topbar.html
··· 45 45 {{ define "profileDropdown" }} 46 46 <details class="relative inline-block text-left nav-dropdown"> 47 47 <summary class="cursor-pointer list-none flex items-center gap-1"> 48 - {{ $user := .Did }} 48 + {{ $user := .Active.Did }} 49 49 <img 50 50 src="{{ tinyAvatar $user }}" 51 51 alt="" ··· 53 53 /> 54 54 <span class="hidden md:inline">{{ $user | resolve | truncateAt30 }}</span> 55 55 </summary> 56 - <div class="absolute flex flex-col right-0 mt-4 p-4 rounded w-48 bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700"> 57 - <a href="/{{ $user }}">profile</a> 58 - <a href="/{{ $user }}?tab=repos">repositories</a> 59 - <a href="/{{ $user }}?tab=strings">strings</a> 60 - <a href="/settings">settings</a> 61 - <a href="#" 62 - hx-post="/logout" 63 - hx-swap="none" 64 - class="text-red-400 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 65 - logout 56 + <div class="absolute right-0 mt-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg z-50 text-sm" style="width: 14rem;"> 57 + {{ $active := .Active.Did }} 58 + {{ $linkStyle := "flex items-center gap-3 px-4 py-2 hover:no-underline hover:bg-gray-50 hover:dark:bg-gray-700/50" }} 59 + 60 + {{ $others := .Accounts | otherAccounts $active }} 61 + {{ if $others }} 62 + <div class="text-sm text-gray-500 dark:text-gray-400 px-3 py-1 pt-2">switch account</div> 63 + {{ range $others }} 64 + <button 65 + type="button" 66 + hx-post="/account/switch" 67 + hx-vals='{"did": "{{ .Did }}"}' 68 + hx-swap="none" 69 + class="{{$linkStyle}} w-full text-left pl-3" 70 + > 71 + <img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full size-6 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 72 + <span class="truncate flex-1">{{ .Did | resolve }}</span> 73 + </button> 74 + {{ end }} 75 + {{ end }} 76 + 77 + <a href="/login?mode=add_account" class="{{$linkStyle}} pl-3"> 78 + <div class="size-6 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center"> 79 + {{ i "plus" "size-3" }} 80 + </div> 81 + 82 + <div class="text-left flex-1 min-w-0 block truncate"> 83 + add account 84 + </div> 66 85 </a> 86 + 87 + <div class="border-t border-gray-200 dark:border-gray-700"> 88 + <a href="/{{ $active }}" class="{{$linkStyle}}"> 89 + {{ i "user" "size-4" }} 90 + profile 91 + </a> 92 + <a href="/{{ $active }}?tab=repos" class="{{$linkStyle}}"> 93 + {{ i "book-marked" "size-4" }} 94 + repositories 95 + </a> 96 + <a href="/{{ $active }}?tab=strings" class="{{$linkStyle}}"> 97 + {{ i "line-squiggle" "size-4" }} 98 + strings 99 + </a> 100 + <a href="/settings" class="{{$linkStyle}}"> 101 + {{ i "cog" "size-4" }} 102 + settings 103 + </a> 104 + <a href="#" 105 + hx-post="/logout" 106 + hx-swap="none" 107 + class="{{$linkStyle}} text-red-400 hover:text-red-400 hover:bg-red-100 dark:hover:bg-red-700/20 pb-2"> 108 + {{ i "log-out" "size-4" }} 109 + logout 110 + </a> 111 + </div> 67 112 </div> 68 113 </details> 69 114
+1 -1
appview/pages/templates/layouts/profilebase.html
··· 2 2 3 3 {{ define "extrameta" }} 4 4 {{ $handle := resolve .Card.UserDid }} 5 - {{ $avatarUrl := fullAvatar $handle }} 5 + {{ $avatarUrl := profileAvatarUrl .Card.Profile "" }} 6 6 <meta property="og:title" content="{{ $handle }}" /> 7 7 <meta property="og:type" content="profile" /> 8 8 <meta property="og:url" content="https://tangled.org/{{ $handle }}?tab={{ .Active }}" />
+1 -1
appview/pages/templates/layouts/repobase.html
··· 1 1 {{ define "title" }}{{ .RepoInfo.FullName }}{{ end }} 2 2 3 3 {{ define "content" }} 4 - <section id="repo-header" class="mb-4 p-2 dark:text-white"> 4 + <section id="repo-header" class="mb-2 py-2 px-4 dark:text-white"> 5 5 <div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between"> 6 6 <!-- left items --> 7 7 <div class="flex flex-col gap-2">
+1 -1
appview/pages/templates/repo/blob.html
··· 35 35 36 36 {{ if .BlobView.ShowingText }} 37 37 <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 38 - <span>{{ .Lines }} lines</span> 38 + <span>{{ .BlobView.Lines }} lines</span> 39 39 {{ end }} 40 40 41 41 {{ if .BlobView.SizeHint }}
+5 -19
appview/pages/templates/repo/commit.html
··· 100 100 {{ if $did }} 101 101 {{ template "user/fragments/picHandleLink" $did }} 102 102 {{ else }} 103 - <a href="mailto:{{ $email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $name }}</a> 103 + <span class="flex items-center gap-1"> 104 + {{ placeholderAvatar "tiny" }} 105 + <a href="mailto:{{ $email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $name }}</a> 106 + </span> 104 107 {{ end }} 105 108 {{ end }} 106 109 ··· 116 119 {{ block "content" . }}{{ end }} 117 120 {{ end }} 118 121 119 - {{ block "contentAfterLayout" . }} 120 - <div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-4"> 121 - <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 122 - {{ block "contentAfterLeft" . }} {{ end }} 123 - </div> 124 - <main class="col-span-1 md:col-span-10"> 125 - {{ block "contentAfter" . }}{{ end }} 126 - </main> 127 - </div> 128 - {{ end }} 122 + {{ block "contentAfter" . }}{{ end }} 129 123 </div> 130 124 {{ end }} 131 125 ··· 139 133 {{ template "repo/fragments/diff" (list .Diff .DiffOpts) }} 140 134 {{end}} 141 135 142 - {{ define "contentAfterLeft" }} 143 - <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 144 - {{ template "repo/fragments/diffOpts" .DiffOpts }} 145 - </div> 146 - <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 147 - {{ template "repo/fragments/diffChangedFiles" .Diff }} 148 - </div> 149 - {{end}}
+1 -19
appview/pages/templates/repo/compare/compare.html
··· 22 22 {{ block "content" . }}{{ end }} 23 23 {{ end }} 24 24 25 - {{ block "contentAfterLayout" . }} 26 - <div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-4"> 27 - <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 28 - {{ block "contentAfterLeft" . }} {{ end }} 29 - </div> 30 - <main class="col-span-1 md:col-span-10"> 31 - {{ block "contentAfter" . }}{{ end }} 32 - </main> 33 - </div> 34 - {{ end }} 25 + {{ block "contentAfter" . }}{{ end }} 35 26 </div> 36 27 {{ end }} 37 28 ··· 44 35 {{ define "contentAfter" }} 45 36 {{ template "repo/fragments/diff" (list .Diff .DiffOpts) }} 46 37 {{end}} 47 - 48 - {{ define "contentAfterLeft" }} 49 - <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 50 - {{ template "repo/fragments/diffOpts" .DiffOpts }} 51 - </div> 52 - <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 53 - {{ template "repo/fragments/diffChangedFiles" .Diff }} 54 - </div> 55 - {{end}}
+81 -96
appview/pages/templates/repo/fragments/cloneDropdown.html
··· 1 1 {{ define "repo/fragments/cloneDropdown" }} 2 - {{ $knot := .RepoInfo.Knot }} 3 - {{ if eq $knot "knot1.tangled.sh" }} 4 - {{ $knot = "tangled.org" }} 5 - {{ end }} 2 + {{ $knot := .RepoInfo.Knot }} 3 + {{ if eq $knot "knot1.tangled.sh" }} 4 + {{ $knot = "tangled.org" }} 5 + {{ end }} 6 6 7 - <details id="clone-dropdown" class="relative inline-block text-left group"> 8 - <summary class="btn-create cursor-pointer list-none flex items-center gap-2"> 9 - {{ i "download" "w-4 h-4" }} 10 - <span class="hidden md:inline">code</span> 11 - <span class="group-open:hidden"> 12 - {{ i "chevron-down" "w-4 h-4" }} 13 - </span> 14 - <span class="hidden group-open:flex"> 15 - {{ i "chevron-up" "w-4 h-4" }} 16 - </span> 17 - </summary> 7 + <button 8 + popovertarget="clone-dropdown" 9 + popovertargetaction="toggle" 10 + class="btn-create cursor-pointer list-none flex items-center gap-2 px-4"> 11 + {{ i "download" "w-4 h-4" }} 12 + <span class="hidden md:inline">code</span> 13 + </button> 14 + <div 15 + popover 16 + id="clone-dropdown" 17 + class=" 18 + bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 19 + dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50 20 + w-96 p-4 rounded drop-shadow overflow-visible"> 21 + <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-5">Clone this repository</h3> 18 22 19 - <div class="absolute right-0 mt-2 w-96 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 drop-shadow-sm dark:text-white z-[9999]"> 20 - <div class="p-4"> 21 - <div class="mb-3"> 22 - <h3 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">Clone this repository</h3> 23 - </div> 23 + <!-- HTTPS Clone --> 24 + <div class="mb-3"> 25 + <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">HTTPS</label> 26 + <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 27 + <code 28 + class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 29 + onclick="window.getSelection().selectAllChildren(this)" 30 + data-url="https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}" 31 + >https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code> 32 + <button 33 + onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 34 + class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 35 + title="Copy to clipboard" 36 + > 37 + {{ i "copy" "w-4 h-4" }} 38 + </button> 39 + </div> 40 + </div> 24 41 25 - <!-- HTTPS Clone --> 26 - <div class="mb-3"> 27 - <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">HTTPS</label> 28 - <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 29 - <code 30 - class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 31 - onclick="window.getSelection().selectAllChildren(this)" 32 - data-url="https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}" 33 - >https://tangled.org/{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code> 34 - <button 35 - onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 36 - class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 37 - title="Copy to clipboard" 38 - > 39 - {{ i "copy" "w-4 h-4" }} 40 - </button> 41 - </div> 42 - </div> 42 + <!-- SSH Clone --> 43 + <div class="mb-3"> 44 + {{ $repoOwnerHandle := resolve .RepoInfo.OwnerDid }} 45 + <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label> 46 + <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 47 + <code 48 + class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 49 + onclick="window.getSelection().selectAllChildren(this)" 50 + data-url="git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}" 51 + >git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}</code> 52 + <button 53 + onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 54 + class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 55 + title="Copy to clipboard" 56 + > 57 + {{ i "copy" "w-4 h-4" }} 58 + </button> 59 + </div> 60 + </div> 43 61 44 - <!-- SSH Clone --> 45 - <div class="mb-3"> 46 - {{ $repoOwnerHandle := resolve .RepoInfo.OwnerDid }} 47 - <label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label> 48 - <div class="flex items-center border border-gray-300 dark:border-gray-600 rounded"> 49 - <code 50 - class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto" 51 - onclick="window.getSelection().selectAllChildren(this)" 52 - data-url="git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}" 53 - >git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}</code> 54 - <button 55 - onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))" 56 - class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600" 57 - title="Copy to clipboard" 58 - > 59 - {{ i "copy" "w-4 h-4" }} 60 - </button> 61 - </div> 62 - </div> 63 - 64 - <!-- Note for self-hosted --> 65 - <p class="text-xs text-gray-500 dark:text-gray-400"> 66 - For self-hosted knots, clone URLs may differ based on your setup. 67 - </p> 68 - 69 - <!-- Download Archive --> 70 - <div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700"> 71 - <a 72 - href="/{{ .RepoInfo.FullName }}/archive/{{ .Ref | urlquery }}" 73 - class="flex items-center gap-2 px-3 py-2 text-sm" 74 - > 75 - {{ i "download" "w-4 h-4" }} 76 - Download tar.gz 77 - </a> 78 - </div> 79 - 80 - </div> 81 - </div> 82 - </details> 62 + <!-- Note for self-hosted --> 63 + <p class="text-xs text-gray-500 dark:text-gray-400"> 64 + For self-hosted knots, clone URLs may differ based on your setup. 65 + </p> 83 66 84 - <script> 85 - function copyToClipboard(button, text) { 86 - navigator.clipboard.writeText(text).then(() => { 87 - const originalContent = button.innerHTML; 88 - button.innerHTML = `{{ i "check" "w-4 h-4" }}`; 89 - setTimeout(() => { 90 - button.innerHTML = originalContent; 91 - }, 2000); 92 - }); 93 - } 67 + <!-- Download Archive --> 68 + <div class="pt-2 mt-2 border-t border-gray-200 dark:border-gray-700"> 69 + <a 70 + href="/{{ .RepoInfo.FullName }}/archive/{{ .Ref | urlquery }}" 71 + class="flex items-center gap-2 px-3 py-2 text-sm" 72 + > 73 + {{ i "download" "w-4 h-4" }} 74 + Download tar.gz 75 + </a> 76 + </div> 77 + </div> 94 78 95 - // Close clone dropdown when clicking outside 96 - document.addEventListener('click', function(event) { 97 - const cloneDropdown = document.getElementById('clone-dropdown'); 98 - if (cloneDropdown && cloneDropdown.hasAttribute('open')) { 99 - if (!cloneDropdown.contains(event.target)) { 100 - cloneDropdown.removeAttribute('open'); 101 - } 102 - } 79 + <script> 80 + function copyToClipboard(button, text) { 81 + navigator.clipboard.writeText(text).then(() => { 82 + const originalContent = button.innerHTML; 83 + button.innerHTML = `{{ i "check" "w-4 h-4" }}`; 84 + setTimeout(() => { 85 + button.innerHTML = originalContent; 86 + }, 2000); 103 87 }); 104 - </script> 88 + } 89 + </script> 105 90 {{ end }}
+198 -43
appview/pages/templates/repo/fragments/diff.html
··· 1 1 {{ define "repo/fragments/diff" }} 2 + <style> 3 + #filesToggle:checked ~ div label[for="filesToggle"] .show-text { display: none; } 4 + #filesToggle:checked ~ div label[for="filesToggle"] .hide-text { display: inline; } 5 + #filesToggle:not(:checked) ~ div label[for="filesToggle"] .hide-text { display: none; } 6 + #filesToggle:checked ~ div div#files { width: fit-content; max-width: 15vw; margin-right: 1rem; } 7 + #filesToggle:not(:checked) ~ div div#files { width: 0; display: none; margin-right: 0; } 8 + </style> 9 + 10 + {{ template "diffTopbar" . }} 11 + {{ block "diffLayout" . }} {{ end }} 12 + {{ end }} 13 + 14 + {{ define "diffTopbar" }} 2 15 {{ $diff := index . 0 }} 3 16 {{ $opts := index . 1 }} 17 + {{ $root := "" }} 18 + {{ if gt (len .) 2 }} 19 + {{ $root = index . 2 }} 20 + {{ end }} 21 + 22 + {{ block "filesCheckbox" $ }} {{ end }} 23 + {{ block "subsCheckbox" $ }} {{ end }} 24 + 25 + <!-- top bar --> 26 + <div class="sticky top-0 z-30 bg-slate-100 dark:bg-gray-900 flex items-center gap-2 col-span-full h-12 p-2 {{ if $root }}mt-4{{ end }}"> 27 + <!-- left panel toggle --> 28 + {{ template "filesToggle" . }} 29 + 30 + <!-- stats --> 31 + {{ $stat := $diff.Stats }} 32 + {{ $count := len $diff.ChangedFiles }} 33 + {{ template "repo/fragments/diffStatPill" $stat }} 34 + <span class="text-xs text-gray-600 dark:text-gray-400 hidden md:inline-flex">{{ $count }} changed file{{ if ne $count 1 }}s{{ end }}</span> 35 + 36 + {{ if $root }} 37 + {{ if $root.IsInterdiff }} 38 + <!-- interdiff indicator --> 39 + <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 40 + <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Interdiff</span> 41 + <a 42 + href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ sub $root.ActiveRound 1 }}" 43 + class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 44 + > 45 + #{{ sub $root.ActiveRound 1 }} 46 + </a> 47 + <span class="text-gray-400 text-xs">โ†’</span> 48 + <a 49 + href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $root.ActiveRound }}" 50 + class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs hover:bg-gray-50 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600" 51 + > 52 + #{{ $root.ActiveRound }} 53 + </a> 54 + </div> 55 + {{ else if ne $root.ActiveRound nil }} 56 + <!-- diff round indicator --> 57 + <div class="flex items-center gap-2 before:content-['|'] before:text-gray-300 dark:before:text-gray-600 before:mr-2"> 58 + <span class="text-xs text-gray-600 dark:text-gray-400 uppercase tracking-wide">Diff</span> 59 + <span class="px-2 py-0.5 bg-white dark:bg-gray-700 rounded font-mono text-xs border border-gray-300 dark:border-gray-600"> 60 + <span class="hidden md:inline">round </span>#{{ $root.ActiveRound }} 61 + </span> 62 + </div> 63 + {{ end }} 64 + {{ end }} 4 65 5 - {{ $commit := $diff.Commit }} 6 - {{ $diff := $diff.Diff }} 7 - {{ $isSplit := $opts.Split }} 8 - {{ $this := $commit.This }} 9 - {{ $parent := $commit.Parent }} 10 - {{ $last := sub (len $diff) 1 }} 66 + <!-- spacer --> 67 + <div class="flex-grow"></div> 68 + 69 + <!-- collapse diffs --> 70 + {{ template "collapseToggle" }} 71 + 72 + <!-- diff options --> 73 + {{ template "repo/fragments/diffOpts" $opts }} 11 74 75 + <!-- right panel toggle --> 76 + {{ block "subsToggle" $ }} {{ end }} 77 + </div> 78 + 79 + {{ end }} 80 + 81 + {{ define "diffLayout" }} 82 + {{ $diff := index . 0 }} 83 + {{ $opts := index . 1 }} 84 + 85 + <div class="flex col-span-full flex-grow"> 86 + <!-- left panel --> 87 + <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto pb-12"> 88 + <section class="overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 89 + {{ template "repo/fragments/fileTree" $diff.FileTree }} 90 + </section> 91 + </div> 92 + 93 + <!-- main content --> 94 + <div class="flex-1 min-w-0 sticky top-12 pb-12"> 95 + {{ template "diffFiles" (list $diff $opts) }} 96 + </div> 97 + 98 + </div> 99 + {{ end }} 100 + 101 + {{ define "diffFiles" }} 102 + {{ $diff := index . 0 }} 103 + {{ $opts := index . 1 }} 104 + {{ $files := $diff.ChangedFiles }} 105 + {{ $isSplit := $opts.Split }} 12 106 <div class="flex flex-col gap-4"> 13 - {{ if eq (len $diff) 0 }} 107 + {{ if eq (len $files) 0 }} 14 108 <div class="text-center text-gray-500 dark:text-gray-400 py-8"> 15 109 <p>No differences found between the selected revisions.</p> 16 110 </div> 17 111 {{ else }} 18 - {{ range $idx, $hunk := $diff }} 19 - {{ with $hunk }} 20 - <details open id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> 21 - <summary class="list-none cursor-pointer sticky top-0"> 22 - <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 23 - <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 24 - <span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span> 25 - <span class="hidden group-open:inline">{{ i "chevron-down" "w-4 h-4" }}</span> 26 - {{ template "repo/fragments/diffStatPill" .Stats }} 112 + {{ range $idx, $file := $files }} 113 + {{ template "diffFile" (list $idx $file $isSplit) }} 114 + {{ end }} 115 + {{ end }} 116 + </div> 117 + {{ end }} 27 118 28 - <div class="flex gap-2 items-center overflow-x-auto"> 29 - {{ if .IsDelete }} 30 - {{ .Name.Old }} 31 - {{ else if (or .IsCopy .IsRename) }} 32 - {{ .Name.Old }} {{ i "arrow-right" "w-4 h-4" }} {{ .Name.New }} 33 - {{ else }} 34 - {{ .Name.New }} 35 - {{ end }} 36 - </div> 37 - </div> 38 - </div> 39 - </summary> 119 + {{ define "diffFile" }} 120 + {{ $idx := index . 0 }} 121 + {{ $file := index . 1 }} 122 + {{ $isSplit := index . 2 }} 123 + {{ with $file }} 124 + <details open id="file-{{ .Id }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> 125 + <summary class="list-none cursor-pointer sticky top-12 group-open:border-b border-gray-200 dark:border-gray-700"> 126 + <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 127 + <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 128 + <span class="group-open:hidden inline">{{ i "chevron-right" "w-4 h-4" }}</span> 129 + <span class="hidden group-open:inline">{{ i "chevron-down" "w-4 h-4" }}</span> 130 + {{ template "repo/fragments/diffStatPill" .Stats }} 40 131 41 - <div class="transition-all duration-700 ease-in-out"> 42 - {{ if .IsBinary }} 43 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 44 - This is a binary file and will not be displayed. 45 - </p> 46 - {{ else }} 47 - {{ if $isSplit }} 48 - {{- template "repo/fragments/splitDiff" .Split -}} 132 + <div class="flex gap-2 items-center overflow-x-auto"> 133 + {{ $n := .Names }} 134 + {{ if and $n.New $n.Old (ne $n.New $n.Old)}} 135 + {{ $n.Old }} {{ i "arrow-right" "w-4 h-4" }} {{ $n.New }} 136 + {{ else if $n.New }} 137 + {{ $n.New }} 49 138 {{ else }} 50 - {{- template "repo/fragments/unifiedDiff" . -}} 139 + {{ $n.Old }} 51 140 {{ end }} 52 - {{- end -}} 141 + </div> 53 142 </div> 54 - </details> 55 - {{ end }} 56 - {{ end }} 57 - {{ end }} 58 - </div> 143 + </div> 144 + </summary> 145 + 146 + <div class="transition-all duration-700 ease-in-out"> 147 + {{ $reason := .CanRender }} 148 + {{ if $reason }} 149 + <p class="text-center text-gray-400 dark:text-gray-500 p-4">{{ $reason }}</p> 150 + {{ else }} 151 + {{ if $isSplit }} 152 + {{- template "repo/fragments/splitDiff" .Split -}} 153 + {{ else }} 154 + {{- template "repo/fragments/unifiedDiff" . -}} 155 + {{ end }} 156 + {{- end -}} 157 + </div> 158 + </details> 159 + {{ end }} 160 + {{ end }} 161 + 162 + {{ define "filesCheckbox" }} 163 + <input type="checkbox" id="filesToggle" class="peer/files hidden" checked/> 164 + {{ end }} 165 + 166 + {{ define "filesToggle" }} 167 + <label title="Toggle filetree panel" for="filesToggle" class="hidden md:inline-flex items-center justify-center rounded cursor-pointer text-normal font-normal normalcase"> 168 + <span class="show-text">{{ i "panel-left-open" "size-4" }}</span> 169 + <span class="hide-text">{{ i "panel-left-close" "size-4" }}</span> 170 + </label> 171 + {{ end }} 172 + 173 + {{ define "collapseToggle" }} 174 + <label 175 + title="Expand/Collapse diffs" 176 + for="collapseToggle" 177 + class="btn font-normal normal-case p-2" 178 + > 179 + <input type="checkbox" id="collapseToggle" class="peer/collapse hidden" checked/> 180 + <span class="peer-checked/collapse:hidden inline-flex items-center gap-2"> 181 + {{ i "fold-vertical" "w-4 h-4" }} 182 + <span class="hidden md:inline">expand all</span> 183 + </span> 184 + <span class="peer-checked/collapse:inline-flex hidden flex items-center gap-2"> 185 + {{ i "unfold-vertical" "w-4 h-4" }} 186 + <span class="hidden md:inline">collapse all</span> 187 + </span> 188 + </label> 189 + <script> 190 + document.addEventListener('DOMContentLoaded', function() { 191 + const checkbox = document.getElementById('collapseToggle'); 192 + const details = document.querySelectorAll('details[id^="file-"]'); 193 + 194 + checkbox.addEventListener('change', function() { 195 + details.forEach(detail => { 196 + detail.open = checkbox.checked; 197 + }); 198 + }); 199 + 200 + details.forEach(detail => { 201 + detail.addEventListener('toggle', function() { 202 + const allOpen = Array.from(details).every(d => d.open); 203 + const allClosed = Array.from(details).every(d => !d.open); 204 + 205 + if (allOpen) { 206 + checkbox.checked = true; 207 + } else if (allClosed) { 208 + checkbox.checked = false; 209 + } 210 + }); 211 + }); 212 + }); 213 + </script> 59 214 {{ end }}
-13
appview/pages/templates/repo/fragments/diffChangedFiles.html
··· 1 - {{ define "repo/fragments/diffChangedFiles" }} 2 - {{ $stat := .Stat }} 3 - {{ $fileTree := fileTree .ChangedFiles }} 4 - <section class="overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 5 - <div class="diff-stat"> 6 - <div class="flex gap-2 items-center"> 7 - <strong class="text-sm uppercase dark:text-gray-200">Changed files</strong> 8 - {{ template "repo/fragments/diffStatPill" $stat }} 9 - </div> 10 - {{ template "repo/fragments/fileTree" $fileTree }} 11 - </div> 12 - </section> 13 - {{ end }}
+22 -25
appview/pages/templates/repo/fragments/diffOpts.html
··· 1 1 {{ define "repo/fragments/diffOpts" }} 2 - <section class="flex flex-col gap-2 overflow-x-auto text-sm px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 3 - <strong class="text-sm uppercase dark:text-gray-200">options</strong> 4 - {{ $active := "unified" }} 5 - {{ if .Split }} 6 - {{ $active = "split" }} 7 - {{ end }} 2 + {{ $active := "unified" }} 3 + {{ if .Split }} 4 + {{ $active = "split" }} 5 + {{ end }} 8 6 9 - {{ $unified := 10 - (dict 11 - "Key" "unified" 12 - "Value" "unified" 13 - "Icon" "square-split-vertical" 14 - "Meta" "") }} 15 - {{ $split := 16 - (dict 17 - "Key" "split" 18 - "Value" "split" 19 - "Icon" "square-split-horizontal" 20 - "Meta" "") }} 21 - {{ $values := list $unified $split }} 7 + {{ $unified := 8 + (dict 9 + "Key" "unified" 10 + "Value" "unified" 11 + "Icon" "square-split-vertical" 12 + "Meta" "") }} 13 + {{ $split := 14 + (dict 15 + "Key" "split" 16 + "Value" "split" 17 + "Icon" "square-split-horizontal" 18 + "Meta" "") }} 19 + {{ $values := list $unified $split }} 22 20 23 - {{ template "fragments/tabSelector" 24 - (dict 25 - "Name" "diff" 26 - "Values" $values 27 - "Active" $active) }} 28 - </section> 21 + {{ template "fragments/tabSelector" 22 + (dict 23 + "Name" "diff" 24 + "Values" $values 25 + "Active" $active) }} 29 26 {{ end }} 30 27
-67
appview/pages/templates/repo/fragments/interdiff.html
··· 1 - {{ define "repo/fragments/interdiff" }} 2 - {{ $repo := index . 0 }} 3 - {{ $x := index . 1 }} 4 - {{ $opts := index . 2 }} 5 - {{ $fileTree := fileTree $x.AffectedFiles }} 6 - {{ $diff := $x.Files }} 7 - {{ $last := sub (len $diff) 1 }} 8 - {{ $isSplit := $opts.Split }} 9 - 10 - <div class="flex flex-col gap-4"> 11 - {{ range $idx, $hunk := $diff }} 12 - {{ with $hunk }} 13 - <details {{ if not (.Status.IsOnlyInOne) }}open{{end}} id="file-{{ .Name }}" class="border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> 14 - <summary class="list-none cursor-pointer sticky top-0"> 15 - <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 16 - <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 17 - <div class="flex gap-1 items-center" style="direction: ltr;"> 18 - {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 19 - {{ if .Status.IsOk }} 20 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">CHANGED</span> 21 - {{ else if .Status.IsUnchanged }} 22 - <span class="bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300 {{ $markerstyle }}">UNCHANGED</span> 23 - {{ else if .Status.IsOnlyInOne }} 24 - <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">REVERTED</span> 25 - {{ else if .Status.IsOnlyInTwo }} 26 - <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">NEW</span> 27 - {{ else if .Status.IsRebased }} 28 - <span class="bg-amber-100 text-amber-700 dark:bg-amber-800/50 dark:text-amber-400 {{ $markerstyle }}">REBASED</span> 29 - {{ else }} 30 - <span class="bg-red-100 text-red-700 dark:bg-red-800/50 dark:text-red-400 {{ $markerstyle }}">ERROR</span> 31 - {{ end }} 32 - </div> 33 - 34 - <div class="flex gap-2 items-center overflow-x-auto" style="direction: rtl;">{{ .Name }}</div> 35 - </div> 36 - 37 - </div> 38 - </summary> 39 - 40 - <div class="transition-all duration-700 ease-in-out"> 41 - {{ if .Status.IsUnchanged }} 42 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 43 - This file has not been changed. 44 - </p> 45 - {{ else if .Status.IsRebased }} 46 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 47 - This patch was likely rebased, as context lines do not match. 48 - </p> 49 - {{ else if .Status.IsError }} 50 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 51 - Failed to calculate interdiff for this file. 52 - </p> 53 - {{ else }} 54 - {{ if $isSplit }} 55 - {{- template "repo/fragments/splitDiff" .Split -}} 56 - {{ else }} 57 - {{- template "repo/fragments/unifiedDiff" . -}} 58 - {{ end }} 59 - {{- end -}} 60 - </div> 61 - 62 - </details> 63 - {{ end }} 64 - {{ end }} 65 - </div> 66 - {{ end }} 67 -
-11
appview/pages/templates/repo/fragments/interdiffFiles.html
··· 1 - {{ define "repo/fragments/interdiffFiles" }} 2 - {{ $fileTree := fileTree .AffectedFiles }} 3 - <section class="px-6 py-2 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm min-h-full text-sm"> 4 - <div class="diff-stat"> 5 - <div class="flex gap-2 items-center"> 6 - <strong class="text-sm uppercase dark:text-gray-200">files</strong> 7 - </div> 8 - {{ template "repo/fragments/fileTree" $fileTree }} 9 - </div> 10 - </section> 11 - {{ end }}
+50
appview/pages/templates/repo/fragments/reactions.html
··· 1 + {{ define "repo/fragments/reactions" }} 2 + <div class="flex flex-wrap items-center gap-2"> 3 + {{- $reactions := .Reactions -}} 4 + {{- $userReacted := .UserReacted -}} 5 + {{- $threadAt := .ThreadAt -}} 6 + 7 + {{ template "reactionsPopup" }} 8 + {{ range $kind := const.OrderedReactionKinds }} 9 + {{ $reactionData := index $reactions $kind }} 10 + {{ template "repo/fragments/reaction" 11 + (dict 12 + "Kind" $kind 13 + "Count" $reactionData.Count 14 + "IsReacted" (index $userReacted $kind) 15 + "ThreadAt" $threadAt 16 + "Users" $reactionData.Users) }} 17 + {{ end }} 18 + </div> 19 + {{ end }} 20 + 21 + {{ define "reactionsPopup" }} 22 + <details 23 + id="reactionsPopUp" 24 + class="relative inline-block" 25 + > 26 + <summary 27 + class="flex justify-center items-center min-w-8 min-h-8 rounded border border-gray-200 dark:border-gray-700 28 + hover:bg-gray-50 29 + hover:border-gray-300 30 + dark:hover:bg-gray-700 31 + dark:hover:border-gray-600 32 + cursor-pointer list-none" 33 + > 34 + {{ i "smile" "size-4" }} 35 + </summary> 36 + <div 37 + class="absolute flex left-0 z-10 mt-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg" 38 + > 39 + {{ range $kind := const.OrderedReactionKinds }} 40 + <button 41 + id="reactBtn-{{ $kind }}" 42 + class="size-12 hover:bg-gray-100 dark:hover:bg-gray-700" 43 + hx-on:click="this.parentElement.parentElement.removeAttribute('open')" 44 + > 45 + {{ $kind }} 46 + </button> 47 + {{ end }} 48 + </div> 49 + </details> 50 + {{ end }}
-30
appview/pages/templates/repo/fragments/reactionsPopUp.html
··· 1 - {{ define "repo/fragments/reactionsPopUp" }} 2 - <details 3 - id="reactionsPopUp" 4 - class="relative inline-block" 5 - > 6 - <summary 7 - class="flex justify-center items-center min-w-8 min-h-8 rounded border border-gray-200 dark:border-gray-700 8 - hover:bg-gray-50 9 - hover:border-gray-300 10 - dark:hover:bg-gray-700 11 - dark:hover:border-gray-600 12 - cursor-pointer list-none" 13 - > 14 - {{ i "smile" "size-4" }} 15 - </summary> 16 - <div 17 - class="absolute flex left-0 z-10 mt-4 rounded bg-white dark:bg-gray-800 dark:text-white border border-gray-200 dark:border-gray-700 shadow-lg" 18 - > 19 - {{ range $kind := . }} 20 - <button 21 - id="reactBtn-{{ $kind }}" 22 - class="size-12 hover:bg-gray-100 dark:hover:bg-gray-700" 23 - hx-on:click="this.parentElement.parentElement.removeAttribute('open')" 24 - > 25 - {{ $kind }} 26 - </button> 27 - {{ end }} 28 - </div> 29 - </details> 30 - {{ end }}
+3 -3
appview/pages/templates/repo/fragments/splitDiff.html
··· 1 1 {{ define "repo/fragments/splitDiff" }} 2 2 {{ $name := .Id }} 3 - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 3 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 group-target/line:bg-yellow-200/30 group-target/line:dark:bg-yellow-600/30" -}} 4 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline group-target/line:text-black group-target/line:dark:text-white" -}} 5 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 - {{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 6 + {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200/50 target:dark:bg-yellow-700/50 scroll-mt-48 group/line" -}} 7 7 {{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} 8 8 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} 9 9 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}}
+3 -3
appview/pages/templates/repo/fragments/unifiedDiff.html
··· 3 3 <div class="overflow-x-auto font-mono leading-normal"><div class="overflow-x-auto"><div class="inline-flex flex-col min-w-full">{{- range .TextFragments -}}<span class="block bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</span> 4 4 {{- $oldStart := .OldPosition -}} 5 5 {{- $newStart := .NewPosition -}} 6 - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} 7 - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 6 + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 group-target/line:bg-yellow-200/30 group-target/line:dark:bg-yellow-600/30" -}} 7 + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline group-target/line:text-black group-target/line:dark:text-white" -}} 8 8 {{- $lineNrSepStyle1 := "" -}} 9 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 - {{- $containerStyle := "inline-flex w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 10 + {{- $containerStyle := "inline-flex w-full items-center target:bg-yellow-200/30 target:dark:bg-yellow-700/30 scroll-mt-48 group/line" -}} 11 11 {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} 12 12 {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} 13 13 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}}
+10 -11
appview/pages/templates/repo/index.html
··· 14 14 {{ end }} 15 15 <div class="flex items-center justify-between pb-5"> 16 16 {{ block "branchSelector" . }}{{ end }} 17 - <div class="flex md:hidden items-center gap-3"> 18 - <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold"> 17 + <div class="flex items-center gap-3"> 18 + <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex md:hidden items-center text-sm gap-1 font-bold"> 19 19 {{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }} 20 20 </a> 21 - <a href="/{{ .RepoInfo.FullName }}/branches" class="inline-flex items-center text-sm gap-1 font-bold"> 21 + <a href="/{{ .RepoInfo.FullName }}/branches" class="inline-flex md:hidden items-center text-sm gap-1 font-bold"> 22 22 {{ i "git-branch" "w-4" "h-4" }} {{ len .Branches }} 23 23 </a> 24 - <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex items-center text-sm gap-1 font-bold"> 24 + <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex md:hidden items-center text-sm gap-1 font-bold"> 25 25 {{ i "tags" "w-4" "h-4" }} {{ len .Tags }} 26 26 </a> 27 27 {{ template "repo/fragments/cloneDropdown" . }} ··· 109 109 {{ i "git-compare" "w-4 h-4" }} 110 110 </a> 111 111 </div> 112 - </div> 113 - 114 - <!-- Clone dropdown in top right --> 115 - <div class="hidden md:flex items-center "> 116 - {{ template "repo/fragments/cloneDropdown" . }} 117 112 </div> 118 113 </div> 119 114 {{ end }} ··· 259 254 {{ define "attribution" }} 260 255 {{ $commit := index . 0 }} 261 256 {{ $map := index . 1 }} 262 - <span class="flex items-center"> 257 + <span class="flex items-center gap-1"> 263 258 {{ $author := index $map $commit.Author.Email }} 264 259 {{ $coauthors := $commit.CoAuthors }} 265 260 {{ $all := list }} ··· 274 269 {{ end }} 275 270 {{ end }} 276 271 277 - {{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }} 272 + {{ if $author }} 273 + {{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }} 274 + {{ else }} 275 + {{ placeholderAvatar "tiny" }} 276 + {{ end }} 278 277 <a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}" 279 278 class="no-underline hover:underline"> 280 279 {{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
+35 -22
appview/pages/templates/repo/issues/fragments/commentList.html
··· 1 1 {{ define "repo/issues/fragments/commentList" }} 2 - <div class="flex flex-col gap-8"> 2 + <div class="flex flex-col gap-4"> 3 3 {{ range $item := .CommentList }} 4 4 {{ template "commentListing" (list $ .) }} 5 5 {{ end }} ··· 19 19 <div class="rounded border border-gray-200 dark:border-gray-700 w-full overflow-hidden shadow-sm bg-gray-50 dark:bg-gray-800/50"> 20 20 {{ template "topLevelComment" $params }} 21 21 22 - <div class="relative ml-4 border-l-2 border-gray-200 dark:border-gray-700"> 22 + <div class="relative ml-10 border-l-2 border-gray-200 dark:border-gray-700"> 23 23 {{ range $index, $reply := $comment.Replies }} 24 - <div class="relative "> 25 - <!-- Horizontal connector --> 26 - <div class="absolute left-0 top-6 w-4 h-1 bg-gray-200 dark:bg-gray-700"></div> 27 - 28 - <div class="pl-2"> 29 - {{ 30 - template "replyComment" 31 - (dict 32 - "RepoInfo" $root.RepoInfo 33 - "LoggedInUser" $root.LoggedInUser 34 - "Issue" $root.Issue 35 - "Comment" $reply) 36 - }} 37 - </div> 24 + <div class="-ml-4"> 25 + {{ 26 + template "replyComment" 27 + (dict 28 + "RepoInfo" $root.RepoInfo 29 + "LoggedInUser" $root.LoggedInUser 30 + "Issue" $root.Issue 31 + "Comment" $reply) 32 + }} 38 33 </div> 39 34 {{ end }} 40 35 </div> ··· 44 39 {{ end }} 45 40 46 41 {{ define "topLevelComment" }} 47 - <div class="rounded px-6 py-4 bg-white dark:bg-gray-800"> 48 - {{ template "repo/issues/fragments/issueCommentHeader" . }} 49 - {{ template "repo/issues/fragments/issueCommentBody" . }} 42 + <div class="rounded px-6 py-4 bg-white dark:bg-gray-800 flex gap-2 "> 43 + <div class="flex-shrink-0"> 44 + <img 45 + src="{{ tinyAvatar .Comment.Did }}" 46 + alt="" 47 + class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 48 + /> 49 + </div> 50 + <div class="flex-1 min-w-0"> 51 + {{ template "repo/issues/fragments/issueCommentHeader" . }} 52 + {{ template "repo/issues/fragments/issueCommentBody" . }} 53 + </div> 50 54 </div> 51 55 {{ end }} 52 56 53 57 {{ define "replyComment" }} 54 - <div class="p-4 w-full mx-auto overflow-hidden"> 55 - {{ template "repo/issues/fragments/issueCommentHeader" . }} 56 - {{ template "repo/issues/fragments/issueCommentBody" . }} 58 + <div class="py-4 pr-4 w-full mx-auto overflow-hidden flex gap-2 "> 59 + <div class="flex-shrink-0"> 60 + <img 61 + src="{{ tinyAvatar .Comment.Did }}" 62 + alt="" 63 + class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 64 + /> 65 + </div> 66 + <div class="flex-1 min-w-0"> 67 + {{ template "repo/issues/fragments/issueCommentHeader" . }} 68 + {{ template "repo/issues/fragments/issueCommentBody" . }} 69 + </div> 57 70 </div> 58 71 {{ end }}
-63
appview/pages/templates/repo/issues/fragments/globalIssueListing.html
··· 1 - {{ define "repo/issues/fragments/globalIssueListing" }} 2 - <div class="flex flex-col gap-2"> 3 - {{ range .Issues }} 4 - <div class="rounded drop-shadow-sm bg-white px-6 py-4 dark:bg-gray-800 dark:border-gray-700"> 5 - <div class="pb-2 mb-3"> 6 - <div class="flex items-center gap-3 mb-2"> 7 - <a 8 - href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}" 9 - class="text-blue-600 dark:text-blue-400 font-medium hover:underline text-sm" 10 - > 11 - {{ resolve .Repo.Did }}/{{ .Repo.Name }} 12 - </a> 13 - </div> 14 - <a 15 - href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" 16 - class="no-underline hover:underline" 17 - > 18 - {{ .Title | description }} 19 - <span class="text-gray-500">#{{ .IssueId }}</span> 20 - </a> 21 - </div> 22 - <div class="text-sm text-gray-500 dark:text-gray-400 flex flex-wrap items-center gap-1"> 23 - {{ $bgColor := "bg-gray-800 dark:bg-gray-700" }} 24 - {{ $icon := "ban" }} 25 - {{ $state := "closed" }} 26 - {{ if .Open }} 27 - {{ $bgColor = "bg-green-600 dark:bg-green-700" }} 28 - {{ $icon = "circle-dot" }} 29 - {{ $state = "open" }} 30 - {{ end }} 31 - 32 - <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm"> 33 - {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 34 - <span class="text-white dark:text-white">{{ $state }}</span> 35 - </span> 36 - 37 - <span class="ml-1"> 38 - {{ template "user/fragments/picHandleLink" .Did }} 39 - </span> 40 - 41 - <span class="before:content-['ยท']"> 42 - {{ template "repo/fragments/time" .Created }} 43 - </span> 44 - 45 - <span class="before:content-['ยท']"> 46 - {{ $s := "s" }} 47 - {{ if eq (len .Comments) 1 }} 48 - {{ $s = "" }} 49 - {{ end }} 50 - <a href="/{{ resolve .Repo.Did }}/{{ .Repo.Name }}/issues/{{ .IssueId }}" class="text-gray-500 dark:text-gray-400">{{ len .Comments }} comment{{$s}}</a> 51 - </span> 52 - 53 - {{ $state := .Labels }} 54 - {{ range $k, $d := $.LabelDefs }} 55 - {{ range $v, $s := $state.GetValSet $d.AtUri.String }} 56 - {{ template "labels/fragments/label" (dict "def" $d "val" $v "withPrefix" true) }} 57 - {{ end }} 58 - {{ end }} 59 - </div> 60 - </div> 61 - {{ end }} 62 - </div> 63 - {{ end }}
+3 -1
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
··· 1 1 {{ define "repo/issues/fragments/issueCommentHeader" }} 2 2 <div class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-400 "> 3 - {{ template "user/fragments/picHandleLink" .Comment.Did }} 3 + {{ $handle := resolve .Comment.Did }} 4 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 4 5 {{ template "hats" $ }} 6 + <span class="before:content-['ยท']"></span> 5 7 {{ template "timestamp" . }} 6 8 {{ $isCommentOwner := and .LoggedInUser (eq .LoggedInUser.Did .Comment.Did) }} 7 9 {{ if and $isCommentOwner (not .Comment.Deleted) }}
+2 -2
appview/pages/templates/repo/issues/fragments/issueListing.html
··· 21 21 {{ $state = "open" }} 22 22 {{ end }} 23 23 24 - <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm"> 24 + <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }}"> 25 25 {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 26 - <span class="text-white dark:text-white">{{ $state }}</span> 26 + <span class="text-white dark:text-white text-sm">{{ $state }}</span> 27 27 </span> 28 28 29 29 <span class="ml-1">
+1 -1
appview/pages/templates/repo/issues/fragments/newComment.html
··· 12 12 <textarea 13 13 id="comment-textarea" 14 14 name="body" 15 - class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 15 + class="w-full p-2 rounded" 16 16 placeholder="Add to the discussion. Markdown is supported." 17 17 onkeyup="updateCommentForm()" 18 18 rows="5"
+1 -1
appview/pages/templates/repo/issues/fragments/putIssue.html
··· 18 18 <textarea 19 19 name="body" 20 20 id="body" 21 - rows="6" 21 + rows="15" 22 22 class="w-full resize-y" 23 23 placeholder="Describe your issue. Markdown is supported." 24 24 >{{ if .Issue }}{{ .Issue.Body }}{{ end }}</textarea>
+3 -3
appview/pages/templates/repo/issues/fragments/replyIssueCommentPlaceholder.html
··· 1 1 {{ define "repo/issues/fragments/replyIssueCommentPlaceholder" }} 2 - <div class="p-2 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 2 + <div class="py-2 px-6 border-t flex gap-2 items-center border-gray-300 dark:border-gray-700"> 3 3 {{ if .LoggedInUser }} 4 4 <img 5 5 src="{{ tinyAvatar .LoggedInUser.Did }}" 6 6 alt="" 7 - class="rounded-full h-6 w-6 mr-1 border border-gray-300 dark:border-gray-700" 7 + class="rounded-full size-8 mr-1 border-2 border-gray-300 dark:border-gray-700" 8 8 /> 9 9 {{ end }} 10 10 <input 11 - class="w-full py-2 border-none focus:outline-none" 11 + class="w-full p-0 border-none focus:outline-none bg-transparent" 12 12 placeholder="Leave a reply..." 13 13 hx-get="/{{ .RepoInfo.FullName }}/issues/{{ .Issue.IssueId }}/comment/{{ .Comment.Id }}/reply" 14 14 hx-trigger="focus"
+10 -26
appview/pages/templates/repo/issues/issue.html
··· 35 35 {{ if .Issue.Body }} 36 36 <article id="body" class="mt-4 prose dark:prose-invert">{{ .Issue.Body | markdown }}</article> 37 37 {{ end }} 38 - <div class="flex flex-wrap gap-2 items-stretch mt-4"> 39 - {{ template "issueReactions" . }} 38 + <div class="mt-4"> 39 + {{ template "repo/fragments/reactions" 40 + (dict "Reactions" .Reactions 41 + "UserReacted" .UserReacted 42 + "ThreadAt" .Issue.AtUri) }} 40 43 </div> 41 44 </section> 42 45 {{ end }} ··· 58 61 {{ $icon = "circle-dot" }} 59 62 {{ end }} 60 63 <div class="inline-flex items-center gap-2"> 61 - <div id="state" 62 - class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}"> 63 - {{ i $icon "w-4 h-4 mr-1.5 text-white" }} 64 - <span class="text-white">{{ .Issue.State }}</span> 65 - </div> 64 + <span class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }}"> 65 + {{ i $icon "w-3 h-3 mr-1.5 text-white dark:text-white" }} 66 + <span class="text-white dark:text-white text-sm">{{ .Issue.State }}</span> 67 + </span> 68 + 66 69 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 67 70 opened by 68 71 {{ template "user/fragments/picHandleLink" .Issue.Did }} ··· 106 109 {{ i "loader-circle" "size-3 animate-spin hidden group-[.htmx-request]:inline" }} 107 110 </a> 108 111 {{ end }} 109 - 110 - {{ define "issueReactions" }} 111 - <div class="flex items-center gap-2"> 112 - {{ template "repo/fragments/reactionsPopUp" .OrderedReactionKinds }} 113 - {{ range $kind := .OrderedReactionKinds }} 114 - {{ $reactionData := index $.Reactions $kind }} 115 - {{ 116 - template "repo/fragments/reaction" 117 - (dict 118 - "Kind" $kind 119 - "Count" $reactionData.Count 120 - "IsReacted" (index $.UserReacted $kind) 121 - "ThreadAt" $.Issue.AtUri 122 - "Users" $reactionData.Users) 123 - }} 124 - {{ end }} 125 - </div> 126 - {{ end }} 127 - 128 112 129 113 {{ define "repoAfter" }} 130 114 <div class="flex flex-col gap-4 mt-4">
+1 -1
appview/pages/templates/repo/issues/issues.html
··· 80 80 "Page" .Page 81 81 "TotalCount" .IssueCount 82 82 "BasePath" (printf "/%s/issues" .RepoInfo.FullName) 83 - "QueryParams" (printf "state=%s&q=%s" $state .FilterQuery) 83 + "QueryParams" (queryParams "state" $state "q" .FilterQuery) 84 84 ) }} 85 85 {{ end }} 86 86 {{ end }}
+5 -1
appview/pages/templates/repo/log.html
··· 186 186 {{ end }} 187 187 {{ end }} 188 188 189 - {{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }} 189 + {{ if $author }} 190 + {{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }} 191 + {{ else }} 192 + {{ placeholderAvatar "tiny" }} 193 + {{ end }} 190 194 <a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}" 191 195 class="no-underline hover:underline"> 192 196 {{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
+60 -69
appview/pages/templates/repo/pipelines/fragments/pipelineSymbol.html
··· 1 1 {{ define "repo/pipelines/fragments/pipelineSymbol" }} 2 - <div class="cursor-pointer"> 3 - {{ $c := .Counts }} 4 - {{ $statuses := .Statuses }} 5 - {{ $total := len $statuses }} 6 - {{ $success := index $c "success" }} 7 - {{ $fail := index $c "failed" }} 8 - {{ $timeout := index $c "timeout" }} 9 - {{ $empty := eq $total 0 }} 10 - {{ $allPass := eq $success $total }} 11 - {{ $allFail := eq $fail $total }} 12 - {{ $allTimeout := eq $timeout $total }} 13 - 14 - {{ if $empty }} 15 - <div class="flex gap-1 items-center"> 16 - {{ i "hourglass" "size-4 text-gray-600 dark:text-gray-400 " }} 17 - <span>0/{{ $total }}</span> 18 - </div> 19 - {{ else if $allPass }} 20 - <div class="flex gap-1 items-center"> 21 - {{ i "check" "size-4 text-green-600" }} 22 - <span>{{ $total }}/{{ $total }}</span> 23 - </div> 24 - {{ else if $allFail }} 25 - <div class="flex gap-1 items-center"> 26 - {{ i "x" "size-4 text-red-500" }} 27 - <span>0/{{ $total }}</span> 28 - </div> 29 - {{ else if $allTimeout }} 30 - <div class="flex gap-1 items-center"> 31 - {{ i "clock-alert" "size-4 text-orange-500" }} 32 - <span>0/{{ $total }}</span> 33 - </div> 2 + <div class="cursor-pointer flex gap-2 items-center"> 3 + {{ template "symbol" .Pipeline }} 4 + {{ if .ShortSummary }} 5 + {{ .Pipeline.ShortStatusSummary }} 34 6 {{ else }} 35 - {{ $radius := f64 8 }} 36 - {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} 37 - {{ $offset := 0.0 }} 38 - <div class="flex gap-1 items-center"> 39 - <svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20"> 40 - <circle cx="10" cy="10" r="{{ $radius }}" fill="none" stroke="#f3f4f633" stroke-width="2"/> 7 + {{ .Pipeline.LongStatusSummary }} 8 + {{ end }} 9 + </div> 10 + {{ end }} 41 11 42 - {{ range $kind, $count := $c }} 43 - {{ $color := "" }} 44 - {{ if or (eq $kind "pending") (eq $kind "running") }} 45 - {{ $color = "#eab308" }} {{/* amber-500 */}} 46 - {{ else if eq $kind "success" }} 47 - {{ $color = "#10b981" }} {{/* green-500 */}} 48 - {{ else if eq $kind "cancelled" }} 49 - {{ $color = "#6b7280" }} {{/* gray-500 */}} 50 - {{ else if eq $kind "timeout" }} 51 - {{ $color = "#fb923c" }} {{/* orange-400 */}} 52 - {{ else }} 53 - {{ $color = "#ef4444" }} {{/* red-500 for failed or unknown */}} 54 - {{ end }} 12 + {{ define "symbol" }} 13 + {{ $c := .Counts }} 14 + {{ $statuses := .Statuses }} 15 + {{ $total := len $statuses }} 16 + {{ $success := index $c "success" }} 17 + {{ $fail := index $c "failed" }} 18 + {{ $timeout := index $c "timeout" }} 19 + {{ $empty := eq $total 0 }} 20 + {{ $allPass := eq $success $total }} 21 + {{ $allFail := eq $fail $total }} 22 + {{ $allTimeout := eq $timeout $total }} 55 23 56 - {{ $percent := divf64 (f64 $count) (f64 $total) }} 57 - {{ $length := mulf64 $percent $circumference }} 58 - 59 - <circle 60 - cx="10" cy="10" r="{{ $radius }}" 61 - fill="none" 62 - stroke="{{ $color }}" 63 - stroke-width="2" 64 - stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}" 65 - stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}" 66 - /> 67 - {{ $offset = addf64 $offset $length }} 68 - {{ end }} 69 - </svg> 70 - <span>{{ $success }}/{{ $total }}</span> 71 - </div> 72 - {{ end }} 73 - </div> 24 + {{ if $empty }} 25 + {{ i "hourglass" "size-4 text-gray-600 dark:text-gray-400 " }} 26 + {{ else if $allPass }} 27 + {{ i "check" "size-4 text-green-600 dark:text-green-500" }} 28 + {{ else if $allFail }} 29 + {{ i "x" "size-4 text-red-600 dark:text-red-500" }} 30 + {{ else if $allTimeout }} 31 + {{ i "clock-alert" "size-4 text-orange-500" }} 32 + {{ else }} 33 + {{ $radius := f64 8 }} 34 + {{ $circumference := mulf64 2.0 (mulf64 3.1416 $radius) }} 35 + {{ $offset := 0.0 }} 36 + <svg class="w-4 h-4 transform -rotate-90" viewBox="0 0 20 20"> 37 + <circle cx="10" cy="10" r="{{ $radius }}" fill="none" class="stroke-gray-200 dark:stroke-gray-700" stroke-width="2"/> 38 + {{ range $kind, $count := $c }} 39 + {{ $colorClass := "" }} 40 + {{ if or (eq $kind "pending") (eq $kind "running") }} 41 + {{ $colorClass = "stroke-yellow-600 dark:stroke-yellow-500" }} 42 + {{ else if eq $kind "success" }} 43 + {{ $colorClass = "stroke-green-600 dark:stroke-green-500" }} 44 + {{ else if eq $kind "cancelled" }} 45 + {{ $colorClass = "stroke-gray-600 dark:stroke-gray-500" }} 46 + {{ else if eq $kind "timeout" }} 47 + {{ $colorClass = "stroke-orange-600 dark:stroke-orange-500" }} 48 + {{ else }} 49 + {{ $colorClass = "stroke-red-600 dark:stroke-red-500" }} 50 + {{ end }} 51 + {{ $percent := divf64 (f64 $count) (f64 $total) }} 52 + {{ $length := mulf64 $percent $circumference }} 53 + <circle 54 + cx="10" cy="10" r="{{ $radius }}" 55 + fill="none" 56 + class="{{ $colorClass }}" 57 + stroke-width="2" 58 + stroke-dasharray="{{ printf "%.2f %.2f" $length (subf64 $circumference $length) }}" 59 + stroke-dashoffset="{{ printf "%.2f" (negf64 $offset) }}" 60 + /> 61 + {{ $offset = addf64 $offset $length }} 62 + {{ end }} 63 + </svg> 64 + {{ end }} 74 65 {{ end }}
+1 -1
appview/pages/templates/repo/pipelines/fragments/pipelineSymbolLong.html
··· 4 4 <div class="relative inline-block"> 5 5 <details class="relative"> 6 6 <summary class="cursor-pointer list-none"> 7 - {{ template "repo/pipelines/fragments/pipelineSymbol" .Pipeline }} 7 + {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 8 8 </summary> 9 9 {{ template "repo/pipelines/fragments/tooltip" $ }} 10 10 </details>
+17 -17
appview/pages/templates/repo/pulls/fragments/pullActions.html
··· 22 22 {{ $isLastRound := eq $roundNumber $lastIdx }} 23 23 {{ $isSameRepoBranch := .Pull.IsBranchBased }} 24 24 {{ $isUpToDate := .ResubmitCheck.No }} 25 - <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative"> 25 + <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2 relative p-2"> 26 26 <button 27 27 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 28 28 hx-target="#actions-{{$roundNumber}}" 29 29 hx-swap="outerHtml" 30 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group"> 31 - {{ i "message-square-plus" "w-4 h-4" }} 32 - <span>comment</span> 30 + class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group"> 31 + {{ i "message-square-plus" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 33 32 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 33 + comment 34 34 </button> 35 35 {{ if .BranchDeleteStatus }} 36 36 <button 37 37 hx-delete="/{{ .BranchDeleteStatus.Repo.Did }}/{{ .BranchDeleteStatus.Repo.Name }}/branches" 38 38 hx-vals='{"branch": "{{ .BranchDeleteStatus.Branch }}" }' 39 39 hx-swap="none" 40 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 40 + class="btn-flat p-2 flex items-center gap-2 no-underline hover:no-underline group text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 41 41 {{ i "git-branch" "w-4 h-4" }} 42 42 <span>delete branch</span> 43 43 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} ··· 52 52 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 53 53 hx-swap="none" 54 54 hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 55 - class="btn p-2 flex items-center gap-2 group" {{ $disabled }}> 56 - {{ i "git-merge" "w-4 h-4" }} 57 - <span>merge{{if $stackCount}} {{$stackCount}}{{end}}</span> 55 + class="btn-flat p-2 flex items-center gap-2 group" {{ $disabled }}> 56 + {{ i "git-merge" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 58 57 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 58 + merge{{if $stackCount}} {{$stackCount}}{{end}} 59 59 </button> 60 60 {{ end }} 61 61 ··· 74 74 {{ end }} 75 75 76 76 hx-disabled-elt="#resubmitBtn" 77 - class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 77 + class="btn-flat p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 78 78 79 79 {{ if $disabled }} 80 80 title="Update this branch to resubmit this pull request" ··· 82 82 title="Resubmit this pull request" 83 83 {{ end }} 84 84 > 85 - {{ i "rotate-ccw" "w-4 h-4" }} 86 - <span>resubmit</span> 85 + {{ i "rotate-ccw" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 87 86 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 87 + resubmit 88 88 </button> 89 89 {{ end }} 90 90 ··· 92 92 <button 93 93 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 94 94 hx-swap="none" 95 - class="btn p-2 flex items-center gap-2 group"> 96 - {{ i "ban" "w-4 h-4" }} 97 - <span>close</span> 95 + class="btn-flat p-2 flex items-center gap-2 group"> 96 + {{ i "ban" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 98 97 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 98 + close 99 99 </button> 100 100 {{ end }} 101 101 ··· 103 103 <button 104 104 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 105 105 hx-swap="none" 106 - class="btn p-2 flex items-center gap-2 group"> 107 - {{ i "refresh-ccw-dot" "w-4 h-4" }} 108 - <span>reopen</span> 106 + class="btn-flat p-2 flex items-center gap-2 group"> 107 + {{ i "refresh-ccw-dot" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 109 108 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 109 + reopen 110 110 </button> 111 111 {{ end }} 112 112 </div>
+11 -23
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 1 1 {{ define "repo/pulls/fragments/pullHeader" }} 2 - <header class="pb-4"> 2 + <header class="pb-2"> 3 3 <h1 class="text-2xl dark:text-white"> 4 4 {{ .Pull.Title | description }} 5 5 <span class="text-gray-500 dark:text-gray-400">#{{ .Pull.PullId }}</span> ··· 17 17 {{ $icon = "git-merge" }} 18 18 {{ end }} 19 19 20 - <section class="mt-2"> 20 + <section> 21 21 <div class="flex items-center gap-2"> 22 - <div 23 - id="state" 24 - class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}" 22 + <span 23 + class="inline-flex items-center rounded px-2 py-[5px] {{ $bgColor }} text-sm" 25 24 > 26 - {{ i $icon "w-4 h-4 mr-1.5 text-white" }} 25 + {{ i $icon "w-3 h-3 mr-1.5 text-white" }} 27 26 <span class="text-white">{{ .Pull.State.String }}</span> 28 - </div> 27 + </span> 29 28 <span class="text-gray-500 dark:text-gray-400 text-sm flex flex-wrap items-center gap-1"> 30 29 opened by 31 30 {{ template "user/fragments/picHandleLink" .Pull.OwnerDid }} ··· 64 63 </article> 65 64 {{ end }} 66 65 67 - {{ with .OrderedReactionKinds }} 68 - <div class="flex items-center gap-2 mt-2"> 69 - {{ template "repo/fragments/reactionsPopUp" . }} 70 - {{ range $kind := . }} 71 - {{ $reactionData := index $.Reactions $kind }} 72 - {{ 73 - template "repo/fragments/reaction" 74 - (dict 75 - "Kind" $kind 76 - "Count" $reactionData.Count 77 - "IsReacted" (index $.UserReacted $kind) 78 - "ThreadAt" $.Pull.AtUri 79 - "Users" $reactionData.Users) 80 - }} 81 - {{ end }} 66 + <div class="mt-2"> 67 + {{ template "repo/fragments/reactions" 68 + (dict "Reactions" .Reactions 69 + "UserReacted" .UserReacted 70 + "ThreadAt" .Pull.AtUri) }} 82 71 </div> 83 - {{ end }} 84 72 </section> 85 73 86 74
+40 -25
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 1 1 {{ define "repo/pulls/fragments/pullNewComment" }} 2 2 <div 3 3 id="pull-comment-card-{{ .RoundNumber }}" 4 - class="bg-white dark:bg-gray-800 rounded drop-shadow-sm p-4 relative w-full flex flex-col gap-2"> 5 - <div class="text-sm text-gray-500 dark:text-gray-400"> 6 - {{ resolve .LoggedInUser.Did }} 7 - </div> 4 + class="w-full flex flex-col gap-2"> 5 + {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 8 6 <form 9 7 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/comment" 10 - hx-indicator="#create-comment-spinner" 11 8 hx-swap="none" 12 - class="w-full flex flex-wrap gap-2" 9 + hx-on::after-request="if(event.detail.successful) this.reset()" 10 + hx-disabled-elt="#reply-{{ .RoundNumber }}" 11 + class="w-full flex flex-wrap gap-2 group" 13 12 > 14 13 <textarea 15 14 name="body" 16 - class="w-full p-2 rounded border border-gray-200" 15 + class="w-full p-2 rounded border" 16 + rows=8 17 17 placeholder="Add to the discussion..."></textarea 18 18 > 19 - <button type="submit" class="btn flex items-center gap-2"> 20 - {{ i "message-square" "w-4 h-4" }} 21 - <span>comment</span> 22 - <span id="create-comment-spinner" class="group"> 23 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 24 - </span> 25 - </button> 26 - <button 27 - type="button" 28 - class="btn flex items-center gap-2 group" 29 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions" 30 - hx-swap="outerHTML" 31 - hx-target="#pull-comment-card-{{ .RoundNumber }}" 32 - > 33 - {{ i "x" "w-4 h-4" }} 34 - <span>cancel</span> 35 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 36 - </button> 19 + {{ template "replyActions" . }} 37 20 <div id="pull-comment"></div> 38 21 </form> 39 22 </div> 40 23 {{ end }} 24 + 25 + {{ define "replyActions" }} 26 + <div class="flex flex-wrap items-stretch justify-end gap-2 text-gray-500 dark:text-gray-400 text-sm w-full"> 27 + {{ template "cancel" . }} 28 + {{ template "reply" . }} 29 + </div> 30 + {{ end }} 31 + 32 + {{ define "cancel" }} 33 + <button 34 + type="button" 35 + class="btn text-red-500 dark:text-red-400 flex gap-2 items-center group" 36 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ .RoundNumber }}/actions" 37 + hx-swap="outerHTML" 38 + hx-target="#actions-{{.RoundNumber}}" 39 + > 40 + {{ i "x" "w-4 h-4" }} 41 + <span>cancel</span> 42 + </button> 43 + {{ end }} 44 + 45 + {{ define "reply" }} 46 + <button 47 + type="submit" 48 + id="reply-{{ .RoundNumber }}" 49 + class="btn-create flex items-center gap-2"> 50 + {{ i "reply" "w-4 h-4 inline group-[.htmx-request]:hidden" }} 51 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 52 + reply 53 + </button> 54 + {{ end }} 55 +
+2 -3
appview/pages/templates/repo/pulls/fragments/summarizedPullHeader.html
··· 15 15 16 16 <div class="flex-shrink-0 flex items-center gap-2"> 17 17 {{ $latestRound := .LastRoundNumber }} 18 - {{ $lastSubmission := index .Submissions $latestRound }} 19 - {{ $commentCount := len $lastSubmission.Comments }} 18 + {{ $commentCount := .TotalComments }} 20 19 {{ if and $pipeline $pipeline.Id }} 21 - {{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }} 20 + {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 22 21 <span class="before:content-['ยท'] before:select-none text-gray-500 dark:text-gray-400"></span> 23 22 {{ end }} 24 23 <span>
+2 -21
appview/pages/templates/repo/pulls/interdiff.html
··· 25 25 {{ template "repo/pulls/fragments/pullHeader" . }} 26 26 </header> 27 27 </section> 28 - 29 28 {{ end }} 30 29 31 30 {{ define "mainLayout" }} ··· 34 33 {{ block "content" . }}{{ end }} 35 34 {{ end }} 36 35 37 - {{ block "contentAfterLayout" . }} 38 - <div class="flex-grow grid grid-cols-1 md:grid-cols-12 gap-4"> 39 - <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 40 - {{ block "contentAfterLeft" . }} {{ end }} 41 - </div> 42 - <main class="col-span-1 md:col-span-10"> 43 - {{ block "contentAfter" . }}{{ end }} 44 - </main> 45 - </div> 46 - {{ end }} 36 + {{ block "contentAfter" . }}{{ end }} 47 37 </div> 48 38 {{ end }} 49 39 50 40 {{ define "contentAfter" }} 51 - {{ template "repo/fragments/interdiff" (list .RepoInfo.FullName .Interdiff .DiffOpts) }} 52 - {{end}} 53 - 54 - {{ define "contentAfterLeft" }} 55 - <div class="flex flex-col gap-4 col-span-1 md:col-span-2"> 56 - {{ template "repo/fragments/diffOpts" .DiffOpts }} 57 - </div> 58 - <div class="sticky top-0 flex-grow max-h-screen overflow-y-auto"> 59 - {{ template "repo/fragments/interdiffFiles" .Interdiff }} 60 - </div> 41 + {{ template "repo/fragments/diff" (list .Interdiff .DiffOpts) }} 61 42 {{end}}
+509 -232
appview/pages/templates/repo/pulls/pull.html
··· 6 6 {{ template "repo/pulls/fragments/og" (dict "RepoInfo" .RepoInfo "Pull" .Pull) }} 7 7 {{ end }} 8 8 9 + {{ define "mainLayout" }} 10 + <div class="px-1 flex-grow flex flex-col gap-4"> 11 + <div class="max-w-full md:max-w-screen-lg mx-auto"> 12 + {{ block "contentLayout" . }} 13 + {{ block "content" . }}{{ end }} 14 + {{ end }} 15 + </div> 16 + {{ block "contentAfterLayout" . }} 17 + <main> 18 + {{ block "contentAfter" . }}{{ end }} 19 + </main> 20 + {{ end }} 21 + </div> 22 + <script> 23 + (function() { 24 + const details = document.getElementById('bottomSheet'); 25 + const backdrop = document.getElementById('bottomSheetBackdrop'); 26 + const isDesktop = () => window.matchMedia('(min-width: 768px)').matches; 27 + 28 + // function to update backdrop 29 + const updateBackdrop = () => { 30 + if (backdrop) { 31 + if (details.open && !isDesktop()) { 32 + backdrop.classList.remove('opacity-0', 'pointer-events-none'); 33 + backdrop.classList.add('opacity-100', 'pointer-events-auto'); 34 + document.body.style.overflow = 'hidden'; 35 + } else { 36 + backdrop.classList.remove('opacity-100', 'pointer-events-auto'); 37 + backdrop.classList.add('opacity-0', 'pointer-events-none'); 38 + document.body.style.overflow = ''; 39 + } 40 + } 41 + }; 42 + 43 + // close on mobile initially 44 + if (!isDesktop()) { 45 + details.open = false; 46 + } 47 + updateBackdrop(); // initialize backdrop 48 + 49 + // prevent closing on desktop 50 + details.addEventListener('toggle', function(e) { 51 + if (isDesktop() && !this.open) { 52 + this.open = true; 53 + } 54 + updateBackdrop(); 55 + }); 56 + 57 + const mediaQuery = window.matchMedia('(min-width: 768px)'); 58 + mediaQuery.addEventListener('change', function(e) { 59 + if (e.matches) { 60 + // switched to desktop - keep open 61 + details.open = true; 62 + } else { 63 + // switched to mobile - close 64 + details.open = false; 65 + } 66 + updateBackdrop(); 67 + }); 68 + 69 + // close when clicking backdrop 70 + if (backdrop) { 71 + backdrop.addEventListener('click', () => { 72 + if (!isDesktop()) { 73 + details.open = false; 74 + } 75 + }); 76 + } 77 + })(); 78 + </script> 79 + {{ end }} 80 + 9 81 {{ define "repoContentLayout" }} 10 82 <div class="grid grid-cols-1 md:grid-cols-10 gap-4 w-full"> 11 - <div class="col-span-1 md:col-span-8"> 12 - <section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white"> 13 - {{ block "repoContent" . }}{{ end }} 14 - </section> 15 - {{ block "repoAfter" . }}{{ end }} 16 - </div> 17 - <div class="col-span-1 md:col-span-2 flex flex-col gap-6"> 83 + <section class="bg-white col-span-1 md:col-span-8 dark:bg-gray-800 p-6 rounded relative w-full mx-auto dark:text-white h-full flex-shrink"> 84 + {{ block "repoContent" . }}{{ end }} 85 + </section> 86 + <div class="flex flex-col gap-6 col-span-1 md:col-span-2"> 18 87 {{ template "repo/fragments/labelPanel" 19 88 (dict "RepoInfo" $.RepoInfo 20 89 "Defs" $.LabelDefs ··· 29 98 </div> 30 99 {{ end }} 31 100 101 + {{ define "contentAfter" }} 102 + {{ template "repo/fragments/diff" (list .Diff .DiffOpts $) }} 103 + {{ end }} 104 + 32 105 {{ define "repoContent" }} 33 106 {{ template "repo/pulls/fragments/pullHeader" . }} 34 - 35 107 {{ if .Pull.IsStacked }} 36 108 <div class="mt-8"> 37 109 {{ template "repo/pulls/fragments/pullStack" . }} ··· 39 111 {{ end }} 40 112 {{ end }} 41 113 42 - {{ define "repoAfter" }} 43 - <section id="submissions" class="mt-4"> 44 - <div class="flex flex-col gap-4"> 45 - {{ block "submissions" . }} {{ end }} 114 + {{ define "diffLayout" }} 115 + {{ $diff := index . 0 }} 116 + {{ $opts := index . 1 }} 117 + {{ $root := index . 2 }} 118 + 119 + <div class="flex col-span-full"> 120 + <!-- left panel --> 121 + <div id="files" class="w-0 hidden md:block overflow-hidden sticky top-12 max-h-screen overflow-y-auto pb-12"> 122 + <section class="overflow-x-auto text-sm px-6 py-2 border-b border-x border-gray-200 dark:border-gray-700 w-full mx-auto min-h-full rounded-b rounded-t-none bg-white dark:bg-gray-800 drop-shadow-sm"> 123 + {{ template "repo/fragments/fileTree" $diff.FileTree }} 124 + </section> 125 + </div> 126 + 127 + <!-- main content --> 128 + <div class="flex-1 min-w-0 sticky top-12 pb-12"> 129 + {{ template "diffFiles" (list $diff $opts) }} 130 + </div> 131 + 132 + <!-- right panel --> 133 + {{ template "subsPanel" $ }} 134 + </div> 135 + {{ end }} 136 + 137 + {{ define "subsPanel" }} 138 + {{ $root := index . 2 }} 139 + {{ $pull := $root.Pull }} 140 + <!-- backdrop overlay - only visible on mobile when open --> 141 + <div id="bottomSheetBackdrop" class="fixed inset-0 bg-black/50 md:hidden opacity-0 pointer-events-none transition-opacity duration-300 z-40"></div> 142 + <!-- right panel - bottom sheet on mobile, side panel on desktop --> 143 + <div id="subs" class="fixed bottom-0 left-0 right-0 z-50 w-full md:static md:z-auto md:max-h-screen md:sticky md:top-12 overflow-hidden"> 144 + <details open id="bottomSheet" class="rounded-t-2xl md:rounded-t drop-shadow-lg md:drop-shadow-none group/panel"> 145 + <summary class=" 146 + flex gap-4 items-center justify-between 147 + rounded-t-2xl md:rounded-t cursor-pointer list-none p-4 md:h-12 148 + text-white md:text-black md:dark:text-white 149 + bg-green-600 dark:bg-green-700 150 + md:bg-white md:dark:bg-gray-800 151 + drop-shadow-sm 152 + border-t md:border-x md:border-t-0 border-gray-200 dark:border-gray-700"> 153 + <h2 class="">History</h2> 154 + {{ template "subsPanelSummary" $ }} 155 + </summary> 156 + <div class="max-h-[85vh] md:max-h-[calc(100vh-3rem-3rem)] w-full flex flex-col-reverse gap-4 overflow-y-auto bg-slate-100 dark:bg-gray-900 md:bg-transparent"> 157 + {{ template "submissions" $root }} 46 158 </div> 47 - </section> 159 + </details> 160 + </div> 161 + {{ end }} 162 + 163 + {{ define "subsPanelSummary" }} 164 + {{ $root := index . 2 }} 165 + {{ $pull := $root.Pull }} 166 + {{ $rounds := len $pull.Submissions }} 167 + {{ $comments := $pull.TotalComments }} 168 + <div class="flex items-center gap-2 text-sm"> 169 + <span> 170 + {{ $rounds }} round{{ if ne $rounds 1 }}s{{ end }} 171 + </span> 172 + <span class="select-none before:content-['\00B7']"></span> 173 + <span> 174 + {{ $comments }} comment{{ if ne $comments 1 }}s{{ end }} 175 + </span> 176 + 177 + <span class="md:hidden inline"> 178 + <span class="inline group-open:hidden">{{ i "chevron-up" "size-4" }}</span> 179 + <span class="hidden group-open:inline">{{ i "chevron-down" "size-4" }}</span> 180 + </span> 181 + </div> 182 + {{ end }} 48 183 49 - <div id="pull-close"></div> 50 - <div id="pull-reopen"></div> 184 + {{ define "subsCheckbox" }} 185 + <input type="checkbox" id="subsToggle" class="peer/subs hidden" checked/> 186 + {{ end }} 187 + 188 + {{ define "subsToggle" }} 189 + <style> 190 + /* Mobile: full width */ 191 + #subsToggle:checked ~ div div#subs { 192 + width: 100%; 193 + margin-left: 0; 194 + } 195 + #subsToggle:checked ~ div label[for="subsToggle"] .show-toggle { display: none; } 196 + #subsToggle:checked ~ div label[for="subsToggle"] .hide-toggle { display: flex; } 197 + #subsToggle:not(:checked) ~ div label[for="subsToggle"] .hide-toggle { display: none; } 198 + 199 + /* Desktop: 25vw with left margin */ 200 + @media (min-width: 768px) { 201 + #subsToggle:checked ~ div div#subs { 202 + width: 25vw; 203 + margin-left: 1rem; 204 + } 205 + /* Unchecked state */ 206 + #subsToggle:not(:checked) ~ div div#subs { 207 + width: 0; 208 + display: none; 209 + margin-left: 0; 210 + } 211 + } 212 + </style> 213 + <label title="Toggle review panel" for="subsToggle" class="hidden md:flex items-center justify-end rounded cursor-pointer"> 214 + <span class="show-toggle">{{ i "message-square-more" "size-4" }}</span> 215 + <span class="hide-toggle w-[25vw] flex justify-end">{{ i "message-square" "size-4" }}</span> 216 + </label> 51 217 {{ end }} 218 + 52 219 53 220 {{ define "submissions" }} 54 221 {{ $lastIdx := sub (len .Pull.Submissions) 1 }} 55 - {{ $targetBranch := .Pull.TargetBranch }} 56 - {{ $repoName := .RepoInfo.FullName }} 57 - {{ range $idx, $item := .Pull.Submissions }} 58 - {{ with $item }} 59 - <details {{ if eq $idx $lastIdx }}open{{ end }}> 60 - <summary id="round-#{{ .RoundNumber }}" class="list-none cursor-pointer"> 61 - <div class="flex flex-wrap gap-2 items-stretch"> 62 - <!-- round number --> 63 - <div class="rounded bg-white dark:bg-gray-800 drop-shadow-sm px-3 py-2 dark:text-white"> 64 - <span class="flex items-center">{{ i "hash" "w-4 h-4" }}{{ .RoundNumber }}</span> 65 - </div> 66 - <!-- round summary --> 67 - <div class="flex-1 rounded drop-shadow-sm bg-white dark:bg-gray-800 p-2 text-gray-500 dark:text-gray-400"> 68 - <span class="gap-1 flex items-center"> 69 - {{ $owner := resolve $.Pull.OwnerDid }} 70 - {{ $re := "re" }} 71 - {{ if eq .RoundNumber 0 }} 72 - {{ $re = "" }} 73 - {{ end }} 74 - <span class="hidden md:inline">{{$re}}submitted</span> 75 - by {{ template "user/fragments/picHandleLink" $.Pull.OwnerDid }} 76 - <span class="select-none before:content-['\00B7']"></span> 77 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ .RoundNumber }}">{{ template "repo/fragments/shortTime" .Created }}</a> 78 - <span class="select-none before:content-['ยท']"></span> 79 - {{ $s := "s" }} 80 - {{ if eq (len .Comments) 1 }} 81 - {{ $s = "" }} 82 - {{ end }} 83 - {{ len .Comments }} comment{{$s}} 84 - </span> 85 - </div> 222 + {{ if not .LoggedInUser }} 223 + {{ template "loginPrompt" $ }} 224 + {{ end }} 225 + {{ range $ridx, $item := reverse .Pull.Submissions }} 226 + {{ $idx := sub $lastIdx $ridx }} 227 + {{ template "submission" (list $item $idx $lastIdx $) }} 228 + {{ end }} 229 + {{ end }} 86 230 87 - <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 88 - hx-boost="true" 89 - href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}"> 90 - {{ i "file-diff" "w-4 h-4" }} 91 - <span class="hidden md:inline">diff</span> 92 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 93 - </a> 94 - {{ if ne $idx 0 }} 95 - <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 96 - hx-boost="true" 97 - href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff"> 98 - {{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }} 99 - <span class="hidden md:inline">interdiff</span> 100 - {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 101 - </a> 102 - {{ end }} 103 - <span id="interdiff-error-{{.RoundNumber}}"></span> 104 - </div> 105 - </summary> 231 + {{ define "submission" }} 232 + {{ $item := index . 0 }} 233 + {{ $idx := index . 1 }} 234 + {{ $lastIdx := index . 2 }} 235 + {{ $root := index . 3 }} 236 + {{ $round := $item.RoundNumber }} 237 + <div class=" 238 + w-full shadow-sm bg-gray-50 dark:bg-gray-900 border-2 border-t-0 239 + {{ if eq $round 0 }}rounded-b{{ else }}rounded{{ end }} 240 + {{ if eq $round $root.ActiveRound }} 241 + border-blue-200 dark:border-blue-700 242 + {{ else }} 243 + border-gray-200 dark:border-gray-700 244 + {{ end }} 245 + "> 246 + {{ template "submissionHeader" $ }} 247 + {{ template "submissionComments" $ }} 248 + </div> 249 + {{ end }} 106 250 107 - {{ if .IsFormatPatch }} 108 - {{ $patches := .AsFormatPatch }} 109 - {{ $round := .RoundNumber }} 110 - <details class="group py-2 md:ml-[3.5rem] text-gray-500 dark:text-gray-400 flex flex-col gap-2 relative text-sm"> 111 - <summary class="py-1 list-none cursor-pointer hover:text-gray-500 hover:dark:text-gray-400"> 112 - {{ $s := "s" }} 113 - {{ if eq (len $patches) 1 }} 114 - {{ $s = "" }} 115 - {{ end }} 116 - <div class="group-open:hidden flex items-center gap-2 ml-2"> 117 - {{ i "chevrons-up-down" "w-4 h-4" }} expand {{ len $patches }} commit{{$s}} 118 - </div> 119 - <div class="hidden group-open:flex items-center gap-2 ml-2"> 120 - {{ i "chevrons-down-up" "w-4 h-4" }} hide {{ len $patches }} commit{{$s}} 121 - </div> 122 - </summary> 123 - {{ range $patches }} 124 - <div id="commit-{{.SHA}}" class="py-1 px-2 relative w-full md:max-w-3/5 md:w-fit flex flex-col"> 125 - <div class="flex items-center gap-2"> 126 - {{ i "git-commit-horizontal" "w-4 h-4" }} 127 - <div class="text-sm text-gray-500 dark:text-gray-400"> 128 - <!-- attempt to resolve $fullRepo: this is possible only on non-deleted forks and branches --> 129 - {{ $fullRepo := "" }} 130 - {{ if and $.Pull.IsForkBased $.Pull.PullSource.Repo }} 131 - {{ $fullRepo = printf "%s/%s" $owner $.Pull.PullSource.Repo.Name }} 132 - {{ else if $.Pull.IsBranchBased }} 133 - {{ $fullRepo = $.RepoInfo.FullName }} 134 - {{ end }} 251 + {{ define "submissionHeader" }} 252 + {{ $item := index . 0 }} 253 + {{ $lastIdx := index . 2 }} 254 + {{ $root := index . 3 }} 255 + {{ $round := $item.RoundNumber }} 256 + <div class=" 257 + {{ if eq $round 0 }}rounded-b{{ else }}rounded{{ end }} 258 + px-6 py-4 pr-2 pt-2 259 + bg-white dark:bg-gray-800 260 + {{ if eq $round $root.ActiveRound }} 261 + border-t-2 border-blue-200 dark:border-blue-700 262 + {{ else }} 263 + border-b-2 border-gray-200 dark:border-gray-700 264 + {{ end }} 265 + flex gap-2 sticky top-0 z-20"> 266 + <!-- left column: just profile picture --> 267 + <div class="flex-shrink-0 pt-2"> 268 + <img 269 + src="{{ tinyAvatar $root.Pull.OwnerDid }}" 270 + alt="" 271 + class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900" 272 + /> 273 + </div> 274 + <!-- right column --> 275 + <div class="flex-1 min-w-0 flex flex-col gap-1"> 276 + {{ template "submissionInfo" $ }} 277 + {{ template "submissionCommits" $ }} 278 + {{ template "submissionPipeline" $ }} 279 + {{ if eq $lastIdx $round }} 280 + {{ block "mergeCheck" $root }} {{ end }} 281 + {{ end }} 282 + </div> 283 + </div> 284 + {{ end }} 135 285 136 - <!-- if $fullRepo was resolved, link to it, otherwise just span without a link --> 137 - {{ if $fullRepo }} 138 - <a href="/{{ $fullRepo }}/commit/{{ .SHA }}" class="font-mono text-gray-500 dark:text-gray-400">{{ slice .SHA 0 8 }}</a> 139 - {{ else }} 140 - <span class="font-mono">{{ slice .SHA 0 8 }}</span> 141 - {{ end }} 142 - </div> 143 - <div class="flex items-center"> 144 - <span>{{ .Title | description }}</span> 145 - {{ if gt (len .Body) 0 }} 146 - <button 147 - class="py-1/2 px-1 mx-2 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 148 - hx-on:click="document.getElementById('body-{{$round}}-{{.SHA}}').classList.toggle('hidden')" 149 - > 150 - {{ i "ellipsis" "w-3 h-3" }} 151 - </button> 152 - {{ end }} 153 - </div> 154 - </div> 155 - {{ if gt (len .Body) 0 }} 156 - <p id="body-{{$round}}-{{.SHA}}" class="hidden mt-1 text-sm pb-2"> 157 - {{ nl2br .Body }} 158 - </p> 159 - {{ end }} 160 - </div> 161 - {{ end }} 162 - </details> 163 - {{ end }} 286 + {{ define "submissionInfo" }} 287 + {{ $item := index . 0 }} 288 + {{ $idx := index . 1 }} 289 + {{ $root := index . 3 }} 290 + {{ $round := $item.RoundNumber }} 291 + <div class="flex gap-2 items-center justify-between mb-1"> 292 + <span class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 pt-2"> 293 + {{ $handle := resolve $root.Pull.OwnerDid }} 294 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 295 + submitted 296 + <span class="px-2 py-0.5 text-black dark:text-white bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded font-mono text-xs border"> 297 + #{{ $round }} 298 + </span> 299 + <span class="select-none before:content-['\00B7']"></span> 300 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500" href="#round-#{{ $round }}"> 301 + {{ template "repo/fragments/shortTime" $item.Created }} 302 + </a> 303 + </span> 304 + <div class="flex gap-2 items-center"> 305 + {{ if ne $root.ActiveRound $round }} 306 + <a class="btn-flat flex items-center gap-2 no-underline hover:no-underline text-sm" 307 + href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $round }}?{{ safeUrl $root.DiffOpts.Encode }}#round-#{{ $round }}"> 308 + {{ i "diff" "w-4 h-4" }} 309 + diff 310 + </a> 311 + {{ end }} 312 + {{ if ne $idx 0 }} 313 + <a class="btn-flat flex items-center gap-2 no-underline hover:no-underline text-sm" 314 + href="/{{ $root.RepoInfo.FullName }}/pulls/{{ $root.Pull.PullId }}/round/{{ $round }}/interdiff?{{ safeUrl $root.DiffOpts.Encode }}"> 315 + {{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }} 316 + interdiff 317 + </a> 318 + {{ end }} 319 + </div> 320 + </div> 321 + {{ end }} 164 322 323 + {{ define "submissionCommits" }} 324 + {{ $item := index . 0 }} 325 + {{ $root := index . 3 }} 326 + {{ $round := $item.RoundNumber }} 327 + {{ $patches := $item.AsFormatPatch }} 328 + {{ if $patches }} 329 + <details class="group/commit"> 330 + <summary class="list-none cursor-pointer flex items-center gap-2"> 331 + <span>{{ i "git-commit-horizontal" "w-4 h-4" }}</span> 332 + {{ len $patches }} commit{{ if ne (len $patches) 1 }}s{{ end }} 333 + <div class="text-sm text-gray-500 dark:text-gray-400"> 334 + <span class="group-open/commit:hidden inline">expand</span> 335 + <span class="hidden group-open/commit:inline">collapse</span> 336 + </div> 337 + </summary> 338 + {{ range $patches }} 339 + {{ template "submissionCommit" (list . $item $root) }} 340 + {{ end }} 341 + </details> 342 + {{ end }} 343 + {{ end }} 165 344 166 - <div class="md:pl-[3.5rem] flex flex-col gap-2 mt-2 relative"> 167 - {{ range $cidx, $c := .Comments }} 168 - <div id="comment-{{$c.ID}}" class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-2 px-4 relative w-full"> 169 - {{ if gt $cidx 0 }} 170 - <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 171 - {{ end }} 172 - <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 173 - {{ template "user/fragments/picHandleLink" $c.OwnerDid }} 174 - <span class="before:content-['ยท']"></span> 175 - <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}">{{ template "repo/fragments/time" $c.Created }}</a> 176 - </div> 177 - <div class="prose dark:prose-invert"> 178 - {{ $c.Body | markdown }} 179 - </div> 180 - </div> 345 + {{ define "submissionCommit" }} 346 + {{ $patch := index . 0 }} 347 + {{ $item := index . 1 }} 348 + {{ $root := index . 2 }} 349 + {{ $round := $item.RoundNumber }} 350 + {{ with $patch }} 351 + <div id="commit-{{.SHA}}" class="py-1 relative w-full md:max-w-3/5 md:w-fit flex flex-col text-gray-600 dark:text-gray-300"> 352 + <div class="flex items-baseline gap-2"> 353 + <div class="text-xs"> 354 + <!-- attempt to resolve $fullRepo: this is possible only on non-deleted forks and branches --> 355 + {{ $fullRepo := "" }} 356 + {{ if and $root.Pull.IsForkBased $root.Pull.PullSource.Repo }} 357 + {{ $fullRepo = printf "%s/%s" $root.Pull.OwnerDid $root.Pull.PullSource.Repo.Name }} 358 + {{ else if $root.Pull.IsBranchBased }} 359 + {{ $fullRepo = $root.RepoInfo.FullName }} 181 360 {{ end }} 182 361 183 - {{ block "pipelineStatus" (list $ .) }} {{ end }} 362 + <!-- if $fullRepo was resolved, link to it, otherwise just span without a link --> 363 + {{ if $fullRepo }} 364 + <a href="/{{ $fullRepo }}/commit/{{ .SHA }}" class="font-mono text-gray-600 dark:text-gray-300">{{ slice .SHA 0 8 }}</a> 365 + {{ else }} 366 + <span class="font-mono">{{ slice .SHA 0 8 }}</span> 367 + {{ end }} 368 + </div> 184 369 185 - {{ if eq $lastIdx .RoundNumber }} 186 - {{ block "mergeStatus" $ }} {{ end }} 187 - {{ block "resubmitStatus" $ }} {{ end }} 370 + <div> 371 + <span>{{ .Title | description }}</span> 372 + {{ if gt (len .Body) 0 }} 373 + <button 374 + class="py-1/2 px-1 mx-2 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 375 + hx-on:click="document.getElementById('body-{{$round}}-{{.SHA}}').classList.toggle('hidden')" 376 + > 377 + {{ i "ellipsis" "w-3 h-3" }} 378 + </button> 188 379 {{ end }} 189 - 190 - {{ if $.LoggedInUser }} 191 - {{ template "repo/pulls/fragments/pullActions" 192 - (dict 193 - "LoggedInUser" $.LoggedInUser 194 - "Pull" $.Pull 195 - "RepoInfo" $.RepoInfo 196 - "RoundNumber" .RoundNumber 197 - "MergeCheck" $.MergeCheck 198 - "ResubmitCheck" $.ResubmitCheck 199 - "BranchDeleteStatus" $.BranchDeleteStatus 200 - "Stack" $.Stack) }} 201 - {{ else }} 202 - <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm p-2 relative flex gap-2 items-center w-fit"> 203 - <a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2"> 204 - sign up 205 - </a> 206 - <span class="text-gray-500 dark:text-gray-400">or</span> 207 - <a href="/login" class="underline">login</a> 208 - to add to the discussion 209 - </div> 380 + {{ if gt (len .Body) 0 }} 381 + <p id="body-{{$round}}-{{.SHA}}" class="hidden mt-1 pb-2">{{ nl2br .Body }}</p> 210 382 {{ end }} 211 383 </div> 384 + </div> 385 + </div> 386 + {{ end }} 387 + {{ end }} 388 + 389 + {{ define "mergeCheck" }} 390 + {{ $isOpen := .Pull.State.IsOpen }} 391 + {{ if and $isOpen .MergeCheck .MergeCheck.Error }} 392 + <div class="flex items-center gap-2"> 393 + {{ i "triangle-alert" "w-4 h-4 text-red-600 dark:text-red-500" }} 394 + {{ .MergeCheck.Error }} 395 + </div> 396 + {{ else if and $isOpen .MergeCheck .MergeCheck.IsConflicted }} 397 + <details class="group/conflict"> 398 + <summary class="flex items-center justify-between cursor-pointer list-none"> 399 + <div class="flex items-center gap-2 "> 400 + {{ i "triangle-alert" "text-red-600 dark:text-red-500 w-4 h-4" }} 401 + <span class="font-medium">merge conflicts detected</span> 402 + <div class="text-sm text-gray-500 dark:text-gray-400"> 403 + <span class="group-open/conflict:hidden inline">expand</span> 404 + <span class="hidden group-open/conflict:inline">collapse</span> 405 + </div> 406 + </div> 407 + </summary> 408 + {{ if gt (len .MergeCheck.Conflicts) 0 }} 409 + <ul class="space-y-1 mt-2 overflow-x-auto"> 410 + {{ range .MergeCheck.Conflicts }} 411 + {{ if .Filename }} 412 + <li class="flex items-center whitespace-nowrap"> 413 + {{ i "file-warning" "inline-flex w-4 h-4 mr-1.5 text-red-600 dark:text-red-500 flex-shrink-0" }} 414 + <span class="font-mono">{{ .Filename }}</span> 415 + </li> 416 + {{ else if .Reason }} 417 + <li class="flex items-center whitespace-nowrap"> 418 + {{ i "file-warning" "w-4 h-4 mr-1.5 text-red-600 dark:text-red-500 " }} 419 + <span>{{.Reason}}</span> 420 + </li> 421 + {{ end }} 422 + {{ end }} 423 + </ul> 424 + {{ end }} 212 425 </details> 213 - {{ end }} 426 + {{ else if and $isOpen .MergeCheck }} 427 + <div class="flex items-center gap-2"> 428 + {{ i "check" "w-4 h-4 text-green-600 dark:text-green-500" }} 429 + <span>no conflicts, ready to merge</span> 430 + </div> 214 431 {{ end }} 215 432 {{ end }} 216 433 217 434 {{ define "mergeStatus" }} 218 435 {{ if .Pull.State.IsClosed }} 219 - <div class="bg-gray-50 dark:bg-gray-700 border border-black dark:border-gray-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 436 + <div class="bg-gray-50 dark:bg-gray-700 border border-black dark:border-gray-500 rounded drop-shadow-sm px-6 py-2 relative"> 220 437 <div class="flex items-center gap-2 text-black dark:text-white"> 221 438 {{ i "ban" "w-4 h-4" }} 222 439 <span class="font-medium">closed without merging</span ··· 224 441 </div> 225 442 </div> 226 443 {{ else if .Pull.State.IsMerged }} 227 - <div class="bg-purple-50 dark:bg-purple-900 border border-purple-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 444 + <div class="bg-purple-50 dark:bg-purple-900 border border-purple-500 rounded drop-shadow-sm px-6 py-2 relative"> 228 445 <div class="flex items-center gap-2 text-purple-500 dark:text-purple-300"> 229 446 {{ i "git-merge" "w-4 h-4" }} 230 447 <span class="font-medium">pull request successfully merged</span ··· 232 449 </div> 233 450 </div> 234 451 {{ else if .Pull.State.IsDeleted }} 235 - <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 452 + <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative"> 236 453 <div class="flex items-center gap-2 text-red-500 dark:text-red-300"> 237 454 {{ i "git-pull-request-closed" "w-4 h-4" }} 238 455 <span class="font-medium">This pull has been deleted (possibly by jj abandon or jj squash)</span> 239 456 </div> 240 457 </div> 241 - {{ else if and .MergeCheck .MergeCheck.Error }} 242 - <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 243 - <div class="flex items-center gap-2 text-red-500 dark:text-red-300"> 244 - {{ i "triangle-alert" "w-4 h-4" }} 245 - <span class="font-medium">{{ .MergeCheck.Error }}</span> 246 - </div> 247 - </div> 248 - {{ else if and .MergeCheck .MergeCheck.IsConflicted }} 249 - <div class="bg-red-50 dark:bg-red-900 border border-red-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 250 - <div class="flex flex-col gap-2 text-red-500 dark:text-red-300"> 251 - <div class="flex items-center gap-2"> 252 - {{ i "triangle-alert" "w-4 h-4" }} 253 - <span class="font-medium">merge conflicts detected</span> 254 - </div> 255 - {{ if gt (len .MergeCheck.Conflicts) 0 }} 256 - <ul class="space-y-1"> 257 - {{ range .MergeCheck.Conflicts }} 258 - {{ if .Filename }} 259 - <li class="flex items-center"> 260 - {{ i "file-warning" "w-4 h-4 mr-1.5 text-red-500 dark:text-red-300" }} 261 - <span class="font-mono">{{ .Filename }}</span> 262 - </li> 263 - {{ else if .Reason }} 264 - <li class="flex items-center"> 265 - {{ i "file-warning" "w-4 h-4 mr-1.5 text-red-500 dark:text-red-300" }} 266 - <span>{{.Reason}}</span> 267 - </li> 268 - {{ end }} 269 - {{ end }} 270 - </ul> 271 - {{ end }} 272 - </div> 273 - </div> 274 - {{ else if .MergeCheck }} 275 - <div class="bg-green-50 dark:bg-green-900 border border-green-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 276 - <div class="flex items-center gap-2 text-green-500 dark:text-green-300"> 277 - {{ i "circle-check-big" "w-4 h-4" }} 278 - <span class="font-medium">no conflicts, ready to merge</span> 279 - </div> 280 - </div> 281 458 {{ end }} 282 459 {{ end }} 283 460 284 461 {{ define "resubmitStatus" }} 285 462 {{ if .ResubmitCheck.Yes }} 286 - <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 463 + <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative"> 287 464 <div class="flex items-center gap-2 text-amber-500 dark:text-amber-300"> 288 465 {{ i "triangle-alert" "w-4 h-4" }} 289 466 <span class="font-medium">this branch has been updated, consider resubmitting</span> ··· 292 469 {{ end }} 293 470 {{ end }} 294 471 295 - {{ define "pipelineStatus" }} 296 - {{ $root := index . 0 }} 297 - {{ $submission := index . 1 }} 298 - {{ $pipeline := index $root.Pipelines $submission.SourceRev }} 472 + {{ define "submissionPipeline" }} 473 + {{ $item := index . 0 }} 474 + {{ $root := index . 3 }} 475 + {{ $pipeline := index $root.Pipelines $item.SourceRev }} 299 476 {{ with $pipeline }} 300 477 {{ $id := .Id }} 301 478 {{ if .Statuses }} 302 - <div class="max-w-80 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700"> 303 - {{ range $name, $all := .Statuses }} 304 - <a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 305 - <div 306 - class="flex gap-2 items-center justify-between p-2"> 307 - {{ $lastStatus := $all.Latest }} 308 - {{ $kind := $lastStatus.Status.String }} 479 + <details class="group/pipeline"> 480 + <summary class="cursor-pointer list-none flex items-center gap-2"> 481 + {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" false) }} 482 + <div class="text-sm text-gray-500 dark:text-gray-400"> 483 + <span class="group-open/pipeline:hidden inline">expand</span> 484 + <span class="hidden group-open/pipeline:inline">collapse</span> 485 + </div> 486 + </summary> 487 + <div class="my-2 grid grid-cols-1 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700"> 488 + {{ range $name, $all := .Statuses }} 489 + <a href="/{{ $root.RepoInfo.FullName }}/pipelines/{{ $id }}/workflow/{{ $name }}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 490 + <div 491 + class="flex gap-2 items-center justify-between p-2"> 492 + {{ $lastStatus := $all.Latest }} 493 + {{ $kind := $lastStatus.Status.String }} 309 494 310 - <div id="left" class="flex items-center gap-2 flex-shrink-0"> 311 - {{ template "repo/pipelines/fragments/workflowSymbol" $all }} 312 - {{ $name }} 313 - </div> 314 - <div id="right" class="flex items-center gap-2 flex-shrink-0"> 315 - <span class="font-bold">{{ $kind }}</span> 316 - {{ if .TimeTaken }} 317 - {{ template "repo/fragments/duration" .TimeTaken }} 318 - {{ else }} 319 - {{ template "repo/fragments/shortTimeAgo" $lastStatus.Created }} 320 - {{ end }} 321 - </div> 495 + <div id="left" class="flex items-center gap-2 flex-shrink-0"> 496 + {{ template "repo/pipelines/fragments/workflowSymbol" $all }} 497 + {{ $name }} 498 + </div> 499 + <div id="right" class="flex items-center gap-2 flex-shrink-0"> 500 + <span class="font-bold">{{ $kind }}</span> 501 + {{ if .TimeTaken }} 502 + {{ template "repo/fragments/duration" .TimeTaken }} 503 + {{ else }} 504 + {{ template "repo/fragments/shortTimeAgo" $lastStatus.Created }} 505 + {{ end }} 506 + </div> 507 + </div> 508 + </a> 509 + {{ end }} 322 510 </div> 511 + </details> 512 + {{ end }} 513 + {{ end }} 514 + {{ end }} 515 + 516 + {{ define "submissionComments" }} 517 + {{ $item := index . 0 }} 518 + {{ $idx := index . 1 }} 519 + {{ $lastIdx := index . 2 }} 520 + {{ $root := index . 3 }} 521 + {{ $round := $item.RoundNumber }} 522 + {{ $c := len $item.Comments }} 523 + <details class="relative ml-10 group/comments" {{ if or (eq $c 0) (eq $root.ActiveRound $round) }}open{{ end }}> 524 + <summary class="cursor-pointer list-none"> 525 + <div class="hidden group-open/comments:block absolute -left-8 top-0 bottom-0 w-16 transition-colors flex items-center justify-center group/border z-4"> 526 + <div class="absolute left-1/2 -translate-x-1/2 top-0 bottom-0 w-0.5 group-open/comments:bg-gray-200 dark:group-open/comments:bg-gray-700 group-hover/border:bg-gray-400 dark:group-hover/border:bg-gray-500 transition-colors"> </div> 527 + </div> 528 + <div class="group-open/comments:hidden block relative group/summary py-4"> 529 + <div class="absolute -left-8 top-0 bottom-0 w-16 transition-colors flex items-center justify-center z-4"> 530 + <div class="absolute left-1/2 -translate-x-1/2 h-1/3 top-0 bottom-0 w-0.5 bg-gray-200 dark:bg-gray-700 group-hover/summary:bg-gray-400 dark:group-hover/summary:bg-gray-500 transition-colors"></div> 531 + </div> 532 + <span class="text-gray-500 dark:text-gray-400 text-sm group-hover/summary:text-gray-600 dark:group-hover/summary:text-gray-300 transition-colors flex items-center gap-2 -ml-2 relative"> 533 + {{ i "circle-plus" "size-4 z-5" }} 534 + expand {{ $c }} comment{{ if ne $c 1 }}s{{ end }} 535 + </span> 536 + </div> 537 + </summary> 538 + <div> 539 + {{ range $item.Comments }} 540 + {{ template "submissionComment" . }} 541 + {{ end }} 542 + </div> 543 + 544 + <div class="relative -ml-10"> 545 + {{ if eq $lastIdx $item.RoundNumber }} 546 + {{ block "mergeStatus" $root }} {{ end }} 547 + {{ block "resubmitStatus" $root }} {{ end }} 548 + {{ end }} 549 + </div> 550 + <div class="relative -ml-10 bg-gray-50 dark:bg-gray-900"> 551 + {{ if $root.LoggedInUser }} 552 + {{ template "repo/pulls/fragments/pullActions" 553 + (dict 554 + "LoggedInUser" $root.LoggedInUser 555 + "Pull" $root.Pull 556 + "RepoInfo" $root.RepoInfo 557 + "RoundNumber" $item.RoundNumber 558 + "MergeCheck" $root.MergeCheck 559 + "ResubmitCheck" $root.ResubmitCheck 560 + "BranchDeleteStatus" $root.BranchDeleteStatus 561 + "Stack" $root.Stack) }} 562 + {{ end }} 563 + </div> 564 + </details> 565 + {{ end }} 566 + 567 + {{ define "submissionComment" }} 568 + <div id="comment-{{.ID}}" class="flex gap-2 -ml-4 py-4 w-full mx-auto"> 569 + <!-- left column: profile picture --> 570 + <div class="flex-shrink-0 h-fit relative"> 571 + <img 572 + src="{{ tinyAvatar .OwnerDid }}" 573 + alt="" 574 + class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-5" 575 + /> 576 + </div> 577 + <!-- right column: name and body in two rows --> 578 + <div class="flex-1 min-w-0"> 579 + <!-- Row 1: Author and timestamp --> 580 + <div class="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-1"> 581 + {{ $handle := resolve .OwnerDid }} 582 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="/{{ $handle }}">{{ $handle }}</a> 583 + <span class="before:content-['ยท']"></span> 584 + <a class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-300" href="#comment-{{.ID}}"> 585 + {{ template "repo/fragments/shortTime" .Created }} 323 586 </a> 324 - {{ end }} 325 587 </div> 326 - {{ end }} 327 - {{ end }} 588 + <!-- Row 2: Body text --> 589 + <div class="prose dark:prose-invert mt-1"> 590 + {{ .Body | markdown }} 591 + </div> 592 + </div> 593 + </div> 594 + {{ end }} 595 + 596 + {{ define "loginPrompt" }} 597 + <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm p-2 relative flex gap-2 items-center"> 598 + <a href="/signup" class="btn-create py-0 hover:no-underline hover:text-white flex items-center gap-2"> 599 + sign up 600 + </a> 601 + <span class="text-gray-500 dark:text-gray-400">or</span> 602 + <a href="/login" class="underline">login</a> 603 + to add to the discussion 604 + </div> 328 605 {{ end }}
+4 -13
appview/pages/templates/repo/pulls/pulls.html
··· 112 112 {{ template "repo/fragments/time" .Created }} 113 113 </span> 114 114 115 - 116 - {{ $latestRound := .LastRoundNumber }} 117 - {{ $lastSubmission := index .Submissions $latestRound }} 118 - 119 115 <span class="before:content-['ยท']"> 120 - {{ $commentCount := len $lastSubmission.Comments }} 121 - {{ $s := "s" }} 122 - {{ if eq $commentCount 1 }} 123 - {{ $s = "" }} 124 - {{ end }} 125 - 126 - {{ len $lastSubmission.Comments}} comment{{$s}} 116 + {{ $commentCount := .TotalComments }} 117 + {{ $commentCount }} comment{{ if ne $commentCount 1 }}s{{ end }} 127 118 </span> 128 119 129 120 <span class="before:content-['ยท']"> ··· 136 127 {{ $pipeline := index $.Pipelines .LatestSha }} 137 128 {{ if and $pipeline $pipeline.Id }} 138 129 <span class="before:content-['ยท']"></span> 139 - {{ template "repo/pipelines/fragments/pipelineSymbol" $pipeline }} 130 + {{ template "repo/pipelines/fragments/pipelineSymbol" (dict "Pipeline" $pipeline "ShortSummary" true) }} 140 131 {{ end }} 141 132 142 133 {{ $state := .Labels }} ··· 175 166 "Page" .Page 176 167 "TotalCount" .PullCount 177 168 "BasePath" (printf "/%s/pulls" .RepoInfo.FullName) 178 - "QueryParams" (printf "state=%s&q=%s" .FilteringBy.String .FilterQuery) 169 + "QueryParams" (queryParams "state" .FilteringBy.String "q" .FilterQuery) 179 170 ) }} 180 171 {{ end }} 181 172 {{ end }}
+1 -2
appview/pages/templates/repo/settings/fragments/sidebar.html
··· 1 1 {{ define "repo/settings/fragments/sidebar" }} 2 2 {{ $active := .Tab }} 3 - {{ $tabs := .Tabs }} 4 3 <div class="sticky top-2 grid grid-cols-1 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 shadow-inner"> 5 4 {{ $activeTab := "bg-white dark:bg-gray-700 drop-shadow-sm" }} 6 5 {{ $inactiveTab := "bg-gray-100 dark:bg-gray-800" }} 7 - {{ range $tabs }} 6 + {{ range const.RepoSettingsTabs }} 8 7 <a href="/{{ $.RepoInfo.FullName }}/settings?tab={{.Name}}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 9 8 <div class="flex gap-3 items-center p-2 {{ if eq .Name $active }} {{ $activeTab }} {{ else }} {{ $inactiveTab }} {{ end }}"> 10 9 {{ i .Icon "size-4" }}
+24 -16
appview/pages/templates/strings/string.html
··· 10 10 11 11 {{ define "content" }} 12 12 {{ $ownerId := resolve .Owner.DID.String }} 13 - <section id="string-header" class="mb-4 py-2 px-6 dark:text-white"> 14 - <div class="text-lg flex items-center justify-between"> 15 - <div> 16 - <a href="/strings/{{ $ownerId }}">{{ $ownerId }}</a> 17 - <span class="select-none">/</span> 18 - <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 13 + <section id="string-header" class="mb-2 py-2 px-4 dark:text-white"> 14 + <div class="text-lg flex flex-col sm:flex-row items-start gap-4 justify-between"> 15 + <!-- left items --> 16 + <div class="flex flex-col gap-2"> 17 + <!-- string owner / string name --> 18 + <div class="flex items-center gap-2 flex-wrap"> 19 + {{ template "user/fragments/picHandleLink" .Owner.DID.String }} 20 + <span class="select-none">/</span> 21 + <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 22 + </div> 23 + 24 + <span class="flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-gray-600 dark:text-gray-300"> 25 + {{ if .String.Description }} 26 + {{ .String.Description }} 27 + {{ else }} 28 + <span class="italic">this string has no description</span> 29 + {{ end }} 30 + </span> 19 31 </div> 20 - <div class="flex gap-2 items-stretch text-base"> 32 + 33 + <div class="w-full sm:w-fit grid grid-cols-3 gap-2 z-auto"> 21 34 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 - <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 35 + <a class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 23 36 hx-boost="true" 24 37 href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 25 - {{ i "pencil" "size-4" }} 38 + {{ i "pencil" "w-4 h-4" }} 26 39 <span class="hidden md:inline">edit</span> 27 40 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 28 41 </a> 29 42 <button 30 - class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group p-2" 43 + class="btn text-sm text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 flex items-center gap-2 group" 31 44 title="Delete string" 32 45 hx-delete="/strings/{{ .String.Did }}/{{ .String.Rkey }}/" 33 46 hx-swap="none" 34 47 hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 35 48 > 36 - {{ i "trash-2" "size-4" }} 49 + {{ i "trash-2" "w-4 h-4" }} 37 50 <span class="hidden md:inline">delete</span> 38 51 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 39 52 </button> ··· 44 57 "StarCount" .StarCount) }} 45 58 </div> 46 59 </div> 47 - <span> 48 - {{ with .String.Description }} 49 - {{ . }} 50 - {{ end }} 51 - </span> 52 60 </section> 53 61 <section class="bg-white dark:bg-gray-800 px-6 py-4 rounded relative w-full dark:text-white"> 54 62 <div class="flex flex-col md:flex-row md:justify-between md:items-center text-gray-500 dark:text-gray-400 text-sm md:text-base pb-2 mb-3 text-base border-b border-gray-200 dark:border-gray-700">
+53
appview/pages/templates/user/fragments/editAvatar.html
··· 1 + {{ define "user/fragments/editAvatar" }} 2 + <form 3 + hx-post="/profile/avatar" 4 + hx-encoding="multipart/form-data" 5 + hx-indicator="#spinner" 6 + hx-swap="none" 7 + class="flex flex-col gap-2"> 8 + <label for="avatar-file" class="uppercase p-0"> 9 + Upload or Remove Avatar 10 + </label> 11 + <p class="text-sm text-gray-500 dark:text-gray-400">Upload a new image (PNG or JPEG, max 1MB) or remove your current avatar.</p> 12 + <input 13 + type="file" 14 + id="avatar-file" 15 + name="avatar" 16 + accept="image/png,image/jpeg" 17 + required 18 + class="block w-full text-sm text-gray-500 dark:text-gray-400 19 + file:mr-4 file:py-2 file:px-4 20 + file:rounded file:border-0 21 + file:text-sm file:font-semibold 22 + file:bg-gray-100 file:text-gray-700 23 + dark:file:bg-gray-700 dark:file:text-gray-300 24 + hover:file:bg-gray-200 dark:hover:file:bg-gray-600" /> 25 + <div id="avatar-error" class="text-red-500 dark:text-red-400 text-sm min-h-5"></div> 26 + <div class="flex flex-col gap-2 pt-2"> 27 + <button type="submit" class="btn w-full flex items-center justify-center gap-2"> 28 + <span class="inline-flex gap-2 items-center">{{ i "upload" "size-4" }} upload</span> 29 + <span id="spinner" class="group"> 30 + {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 31 + </span> 32 + </button> 33 + <button 34 + type="button" 35 + hx-delete="/profile/avatar" 36 + hx-confirm="Are you sure you want to remove your profile picture?" 37 + hx-swap="none" 38 + class="btn w-full flex items-center justify-center gap-2"> 39 + {{ i "trash-2" "size-4" }} 40 + remove avatar 41 + </button> 42 + <button 43 + id="cancel-avatar-btn" 44 + type="button" 45 + popovertarget="avatar-upload-modal" 46 + popovertargetaction="hide" 47 + class="btn w-full flex items-center justify-center gap-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"> 48 + {{ i "x" "size-4" }} 49 + cancel 50 + </button> 51 + </div> 52 + </form> 53 + {{ end }}
+39 -22
appview/pages/templates/user/fragments/profileCard.html
··· 3 3 <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 4 4 <div id="avatar" class="col-span-1 flex justify-center items-center"> 5 5 <div class="w-3/4 aspect-square relative"> 6 - <img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" /> 6 + <img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ profileAvatarUrl .Profile "" }}" /> 7 + {{ if eq .FollowStatus.String "IsSelf" }} 8 + <button 9 + class="absolute bottom-2 right-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors" 10 + popovertarget="avatar-upload-modal" 11 + popovertargetaction="toggle" 12 + title="Upload avatar"> 13 + {{ i "camera" "w-4 h-4" }} 14 + </button> 15 + {{ end }} 7 16 </div> 8 17 </div> 18 + <div 19 + id="avatar-upload-modal" 20 + popover 21 + class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50"> 22 + {{ template "user/fragments/editAvatar" . }} 23 + </div> 9 24 <div class="col-span-2"> 10 25 <div class="flex items-center flex-row flex-nowrap gap-2"> 11 26 <p title="{{ $userIdent }}" ··· 13 28 {{ $userIdent }} 14 29 </p> 15 30 {{ with .Profile }} 16 - {{ if .Pronouns }} 17 - <p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p> 18 - {{ end }} 31 + {{ if .Pronouns }} 32 + <p class="text-gray-500 dark:text-gray-400">{{ .Pronouns }}</p> 33 + {{ end }} 19 34 {{ end }} 20 35 </div> 21 36 ··· 29 44 {{ with .Profile }} 30 45 31 46 {{ if .Description }} 32 - <p class="text-base pb-4 md:pb-2">{{ .Description }}</p> 47 + <p class="text-base pb-4 md:pb-2">{{ .Description }}</p> 33 48 {{ end }} 34 49 35 50 <div class="hidden md:block"> ··· 45 60 {{ end }} 46 61 {{ if .IncludeBluesky }} 47 62 <div class="flex items-center gap-2"> 48 - <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" 49 - }}</span> 63 + <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span> 50 64 <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ $userIdent }}</a> 51 65 </div> 52 66 {{ end }} 53 67 {{ range $link := .Links }} 54 - {{ if $link }} 55 - <div class="flex items-center gap-2"> 56 - <span class="flex-shrink-0">{{ i "link" "size-4" }}</span> 57 - <a href="{{ $link }}">{{ $link }}</a> 58 - </div> 59 - {{ end }} 68 + {{ if $link }} 69 + <div class="flex items-center gap-2"> 70 + <span class="flex-shrink-0">{{ i "link" "size-4" }}</span> 71 + <a href="{{ $link }}">{{ $link }}</a> 72 + </div> 73 + {{ end }} 60 74 {{ end }} 61 75 {{ if not $profile.IsStatsEmpty }} 62 76 <div class="flex items-center justify-evenly gap-2 py-2"> 63 77 {{ range $stat := .Stats }} 64 - {{ if $stat.Kind }} 65 - <div class="flex flex-col items-center gap-2"> 66 - <span class="text-xl font-bold">{{ $stat.Value }}</span> 67 - <span>{{ $stat.Kind.String }}</span> 68 - </div> 69 - {{ end }} 78 + {{ if $stat.Kind }} 79 + <div class="flex flex-col items-center gap-2"> 80 + <span class="text-xl font-bold">{{ $stat.Value }}</span> 81 + <span>{{ $stat.Kind.String }}</span> 82 + </div> 83 + {{ end }} 70 84 {{ end }} 71 85 </div> 72 86 {{ end }} ··· 75 89 76 90 <div class="flex mt-2 items-center gap-2"> 77 91 {{ if ne .FollowStatus.String "IsSelf" }} 78 - {{ template "user/fragments/follow" . }} 92 + {{ template "user/fragments/follow" . }} 79 93 {{ else }} 80 - <button id="editBtn" class="btn w-full flex items-center gap-2 group" hx-target="#profile-bio" 81 - hx-get="/profile/edit-bio" hx-swap="innerHTML"> 94 + <button id="editBtn" 95 + class="btn w-full flex items-center gap-2 group" 96 + hx-target="#profile-bio" 97 + hx-get="/profile/edit-bio" 98 + hx-swap="innerHTML"> 82 99 {{ i "pencil" "w-4 h-4" }} 83 100 edit 84 101 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
+53
appview/pages/templates/user/login.html
··· 20 20 <h2 class="text-center text-xl italic dark:text-white"> 21 21 tightly-knit social coding. 22 22 </h2> 23 + 24 + {{ if .AddAccount }} 25 + <div class="flex gap-2 my-4 bg-blue-50 dark:bg-blue-900/30 border border-blue-300 dark:border-sky-800 rounded px-3 py-2 text-blue-600 dark:text-blue-300"> 26 + <span class="py-1">{{ i "user-plus" "w-4 h-4" }}</span> 27 + <div> 28 + <h5 class="font-medium">Add another account</h5> 29 + <p class="text-sm">Sign in with a different account to add it to your account list.</p> 30 + </div> 31 + </div> 32 + {{ end }} 33 + 34 + {{ if and .LoggedInUser .LoggedInUser.Accounts }} 35 + {{ $accounts := .LoggedInUser.Accounts }} 36 + {{ if $accounts }} 37 + <div class="my-4 border border-gray-200 dark:border-gray-700 rounded overflow-hidden"> 38 + <div class="px-3 py-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> 39 + <span class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium">Saved accounts</span> 40 + </div> 41 + <div class="divide-y divide-gray-200 dark:divide-gray-700"> 42 + {{ range $accounts }} 43 + <div class="flex items-center justify-between px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"> 44 + <button 45 + type="button" 46 + hx-post="/account/switch" 47 + hx-vals='{"did": "{{ .Did }}"}' 48 + hx-swap="none" 49 + class="flex items-center gap-2 flex-1 text-left min-w-0" 50 + > 51 + <img src="{{ tinyAvatar .Did }}" alt="" class="rounded-full h-8 w-8 flex-shrink-0 border border-gray-300 dark:border-gray-700" /> 52 + <div class="flex flex-col min-w-0"> 53 + <span class="text-sm font-medium dark:text-white truncate">{{ .Did | resolve | truncateAt30 }}</span> 54 + <span class="text-xs text-gray-500 dark:text-gray-400">Click to switch</span> 55 + </div> 56 + </button> 57 + <button 58 + type="button" 59 + hx-delete="/account/{{ .Did }}" 60 + hx-swap="none" 61 + class="p-1 text-gray-400 hover:text-red-500 dark:hover:text-red-400 flex-shrink-0" 62 + title="Remove account" 63 + > 64 + {{ i "x" "w-4 h-4" }} 65 + </button> 66 + </div> 67 + {{ end }} 68 + </div> 69 + </div> 70 + {{ end }} 71 + {{ end }} 72 + 23 73 <form 24 74 class="mt-4" 25 75 hx-post="/login" ··· 46 96 </span> 47 97 </div> 48 98 <input type="hidden" name="return_url" value="{{ .ReturnUrl }}"> 99 + <input type="hidden" name="add_account" value="{{ if .AddAccount }}true{{ end }}"> 49 100 50 101 <button 51 102 class="btn w-full my-2 mt-6 text-base " ··· 66 117 You have not authorized the app. 67 118 {{ else if eq .ErrorCode "session" }} 68 119 Server failed to create user session. 120 + {{ else if eq .ErrorCode "max_accounts" }} 121 + You have reached the maximum of 20 linked accounts. Please remove an account before adding a new one. 69 122 {{ else }} 70 123 Internal Server error. 71 124 {{ end }}
+4 -2
appview/pages/templates/user/settings/emails.html
··· 62 62 hx-swap="none" 63 63 class="flex flex-col gap-2" 64 64 > 65 - <p class="uppercase p-0">ADD EMAIL</p> 65 + <label for="email-address" class="uppercase p-0"> 66 + add email 67 + </label> 66 68 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p> 67 69 <input 68 70 type="email" ··· 91 93 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div> 92 94 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div> 93 95 </form> 94 - {{ end }} 96 + {{ end }}
+2 -3
appview/pages/templates/user/settings/fragments/sidebar.html
··· 1 1 {{ define "user/settings/fragments/sidebar" }} 2 2 {{ $active := .Tab }} 3 - {{ $tabs := .Tabs }} 4 3 <div class="sticky top-2 grid grid-cols-1 rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 shadow-inner"> 5 4 {{ $activeTab := "bg-white dark:bg-gray-700 drop-shadow-sm" }} 6 5 {{ $inactiveTab := "bg-gray-100 dark:bg-gray-800" }} 7 - {{ range $tabs }} 6 + {{ range const.UserSettingsTabs }} 8 7 <a href="/settings/{{.Name}}" class="no-underline hover:no-underline hover:bg-gray-100/25 hover:dark:bg-gray-700/25"> 9 8 <div class="flex gap-3 items-center p-2 {{ if eq .Name $active }} {{ $activeTab }} {{ else }} {{ $inactiveTab }} {{ end }}"> 10 9 {{ i .Icon "size-4" }} ··· 13 12 </a> 14 13 {{ end }} 15 14 </div> 16 - {{ end }} 15 + {{ end }}
+4 -2
appview/pages/templates/user/settings/keys.html
··· 21 21 <div class="col-span-1 md:col-span-2"> 22 22 <h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2> 23 23 <p class="text-gray-500 dark:text-gray-400"> 24 - SSH public keys added here will be broadcasted to knots that you are a member of, 24 + SSH public keys added here will be broadcasted to knots that you are a member of, 25 25 allowing you to push to repositories there. 26 26 </p> 27 27 </div> ··· 63 63 hx-swap="none" 64 64 class="flex flex-col gap-2" 65 65 > 66 - <p class="uppercase p-0">ADD SSH KEY</p> 66 + <label for="key-name" class="uppercase p-0"> 67 + add ssh key 68 + </label> 67 69 <p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p> 68 70 <input 69 71 type="text"
+2 -2
appview/pipelines/pipelines.go
··· 77 77 } 78 78 79 79 func (p *Pipelines) Index(w http.ResponseWriter, r *http.Request) { 80 - user := p.oauth.GetUser(r) 80 + user := p.oauth.GetMultiAccountUser(r) 81 81 l := p.logger.With("handler", "Index") 82 82 83 83 f, err := p.repoResolver.Resolve(r) ··· 106 106 } 107 107 108 108 func (p *Pipelines) Workflow(w http.ResponseWriter, r *http.Request) { 109 - user := p.oauth.GetUser(r) 109 + user := p.oauth.GetMultiAccountUser(r) 110 110 l := p.logger.With("handler", "Workflow") 111 111 112 112 f, err := p.repoResolver.Resolve(r)
+3 -3
appview/pulls/opengraph.go
··· 18 18 "tangled.org/core/types" 19 19 ) 20 20 21 - func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffStat, filesChanged int) (*ogcard.Card, error) { 21 + func (s *Pulls) drawPullSummaryCard(pull *models.Pull, repo *models.Repo, commentCount int, diffStats types.DiffFileStat, filesChanged int) (*ogcard.Card, error) { 22 22 width, height := ogcard.DefaultSize() 23 23 mainCard, err := ogcard.NewCard(width, height) 24 24 if err != nil { ··· 199 199 currentX += commentTextWidth + 40 200 200 201 201 // Draw files changed 202 - err = statusStatsArea.DrawLucideIcon("static/icons/file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 202 + err = statusStatsArea.DrawLucideIcon("file-diff", currentX, statsY+iconBaselineOffset-iconSize/2+5, iconSize, iconColor) 203 203 if err != nil { 204 204 log.Printf("failed to draw file diff icon: %v", err) 205 205 } ··· 284 284 commentCount := len(comments) 285 285 286 286 // Calculate diff stats from latest submission using patchutil 287 - var diffStats types.DiffStat 287 + var diffStats types.DiffFileStat 288 288 filesChanged := 0 289 289 if len(pull.Submissions) > 0 { 290 290 latestSubmission := pull.Submissions[len(pull.Submissions)-1]
+111 -147
appview/pulls/pulls.go
··· 97 97 func (s *Pulls) PullActions(w http.ResponseWriter, r *http.Request) { 98 98 switch r.Method { 99 99 case http.MethodGet: 100 - user := s.oauth.GetUser(r) 100 + user := s.oauth.GetMultiAccountUser(r) 101 101 f, err := s.repoResolver.Resolve(r) 102 102 if err != nil { 103 103 log.Println("failed to get repo and knot", err) ··· 128 128 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 129 129 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 130 130 resubmitResult := pages.Unknown 131 - if user.Did == pull.OwnerDid { 131 + if user.Active.Did == pull.OwnerDid { 132 132 resubmitResult = s.resubmitCheck(r, f, pull, stack) 133 133 } 134 134 ··· 146 146 } 147 147 } 148 148 149 - func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 150 - user := s.oauth.GetUser(r) 149 + func (s *Pulls) repoPullHelper(w http.ResponseWriter, r *http.Request, interdiff bool) { 150 + user := s.oauth.GetMultiAccountUser(r) 151 151 f, err := s.repoResolver.Resolve(r) 152 152 if err != nil { 153 153 log.Println("failed to get repo and knot", err) ··· 168 168 return 169 169 } 170 170 171 + roundId := chi.URLParam(r, "round") 172 + roundIdInt := pull.LastRoundNumber() 173 + if r, err := strconv.Atoi(roundId); err == nil { 174 + roundIdInt = r 175 + } 176 + if roundIdInt >= len(pull.Submissions) { 177 + http.Error(w, "bad round id", http.StatusBadRequest) 178 + log.Println("failed to parse round id", err) 179 + return 180 + } 181 + 182 + var diffOpts types.DiffOpts 183 + if d := r.URL.Query().Get("diff"); d == "split" { 184 + diffOpts.Split = true 185 + } 186 + 171 187 // can be nil if this pull is not stacked 172 188 stack, _ := r.Context().Value("stack").(models.Stack) 173 189 abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull) ··· 175 191 mergeCheckResponse := s.mergeCheck(r, f, pull, stack) 176 192 branchDeleteStatus := s.branchDeleteStatus(r, f, pull) 177 193 resubmitResult := pages.Unknown 178 - if user != nil && user.Did == pull.OwnerDid { 194 + if user != nil && user.Active != nil && user.Active.Did == pull.OwnerDid { 179 195 resubmitResult = s.resubmitCheck(r, f, pull, stack) 180 196 } 181 197 ··· 212 228 reactionMap, err := db.GetReactionMap(s.db, 20, pull.AtUri()) 213 229 if err != nil { 214 230 log.Println("failed to get pull reactions") 215 - s.pages.Notice(w, "pulls", "Failed to load pull. Try again later.") 216 231 } 217 232 218 233 userReactions := map[models.ReactionKind]bool{} 219 234 if user != nil { 220 - userReactions = db.GetReactionStatusMap(s.db, user.Did, pull.AtUri()) 235 + userReactions = db.GetReactionStatusMap(s.db, user.Active.Did, pull.AtUri()) 221 236 } 222 237 223 238 labelDefs, err := db.GetLabelDefinitions( ··· 236 251 defs[l.AtUri().String()] = &l 237 252 } 238 253 254 + patch := pull.Submissions[roundIdInt].CombinedPatch() 255 + var diff types.DiffRenderer 256 + diff = patchutil.AsNiceDiff(patch, pull.TargetBranch) 257 + 258 + if interdiff { 259 + currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].CombinedPatch()) 260 + if err != nil { 261 + log.Println("failed to interdiff; current patch malformed") 262 + s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 263 + return 264 + } 265 + 266 + previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].CombinedPatch()) 267 + if err != nil { 268 + log.Println("failed to interdiff; previous patch malformed") 269 + s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 270 + return 271 + } 272 + 273 + diff = patchutil.Interdiff(previousPatch, currentPatch) 274 + } 275 + 239 276 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 240 277 LoggedInUser: user, 241 278 RepoInfo: s.repoResolver.GetRepoInfo(r, user), ··· 247 284 MergeCheck: mergeCheckResponse, 248 285 ResubmitCheck: resubmitResult, 249 286 Pipelines: m, 287 + Diff: diff, 288 + DiffOpts: diffOpts, 289 + ActiveRound: roundIdInt, 290 + IsInterdiff: interdiff, 250 291 251 - OrderedReactionKinds: models.OrderedReactionKinds, 252 - Reactions: reactionMap, 253 - UserReacted: userReactions, 292 + Reactions: reactionMap, 293 + UserReacted: userReactions, 254 294 255 295 LabelDefs: defs, 256 296 }) 257 297 } 258 298 299 + func (s *Pulls) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 300 + pull, ok := r.Context().Value("pull").(*models.Pull) 301 + if !ok { 302 + log.Println("failed to get pull") 303 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 304 + return 305 + } 306 + 307 + http.Redirect(w, r, r.URL.String()+fmt.Sprintf("/round/%d", pull.LastRoundNumber()), http.StatusFound) 308 + } 309 + 259 310 func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse { 260 311 if pull.State == models.PullMerged { 261 312 return types.MergeCheckResponse{} ··· 328 379 return nil 329 380 } 330 381 331 - user := s.oauth.GetUser(r) 382 + user := s.oauth.GetMultiAccountUser(r) 332 383 if user == nil { 333 384 return nil 334 385 } ··· 351 402 } 352 403 353 404 // user can only delete branch if they are a collaborator in the repo that the branch belongs to 354 - perms := s.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo()) 405 + perms := s.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 355 406 if !slices.Contains(perms, "repo:push") { 356 407 return nil 357 408 } ··· 438 489 } 439 490 440 491 func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 441 - user := s.oauth.GetUser(r) 442 - 443 - var diffOpts types.DiffOpts 444 - if d := r.URL.Query().Get("diff"); d == "split" { 445 - diffOpts.Split = true 446 - } 447 - 448 - pull, ok := r.Context().Value("pull").(*models.Pull) 449 - if !ok { 450 - log.Println("failed to get pull") 451 - s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 452 - return 453 - } 454 - 455 - stack, _ := r.Context().Value("stack").(models.Stack) 456 - 457 - roundId := chi.URLParam(r, "round") 458 - roundIdInt, err := strconv.Atoi(roundId) 459 - if err != nil || roundIdInt >= len(pull.Submissions) { 460 - http.Error(w, "bad round id", http.StatusBadRequest) 461 - log.Println("failed to parse round id", err) 462 - return 463 - } 464 - 465 - patch := pull.Submissions[roundIdInt].CombinedPatch() 466 - diff := patchutil.AsNiceDiff(patch, pull.TargetBranch) 467 - 468 - s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 469 - LoggedInUser: user, 470 - RepoInfo: s.repoResolver.GetRepoInfo(r, user), 471 - Pull: pull, 472 - Stack: stack, 473 - Round: roundIdInt, 474 - Submission: pull.Submissions[roundIdInt], 475 - Diff: &diff, 476 - DiffOpts: diffOpts, 477 - }) 478 - 492 + s.repoPullHelper(w, r, false) 479 493 } 480 494 481 495 func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 482 - user := s.oauth.GetUser(r) 483 - 484 - var diffOpts types.DiffOpts 485 - if d := r.URL.Query().Get("diff"); d == "split" { 486 - diffOpts.Split = true 487 - } 488 - 489 - pull, ok := r.Context().Value("pull").(*models.Pull) 490 - if !ok { 491 - log.Println("failed to get pull") 492 - s.pages.Notice(w, "pull-error", "Failed to get pull.") 493 - return 494 - } 495 - 496 - roundId := chi.URLParam(r, "round") 497 - roundIdInt, err := strconv.Atoi(roundId) 498 - if err != nil || roundIdInt >= len(pull.Submissions) { 499 - http.Error(w, "bad round id", http.StatusBadRequest) 500 - log.Println("failed to parse round id", err) 501 - return 502 - } 503 - 504 - if roundIdInt == 0 { 505 - http.Error(w, "bad round id", http.StatusBadRequest) 506 - log.Println("cannot interdiff initial submission") 507 - return 508 - } 509 - 510 - currentPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt].CombinedPatch()) 511 - if err != nil { 512 - log.Println("failed to interdiff; current patch malformed") 513 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; current patch is invalid.") 514 - return 515 - } 516 - 517 - previousPatch, err := patchutil.AsDiff(pull.Submissions[roundIdInt-1].CombinedPatch()) 518 - if err != nil { 519 - log.Println("failed to interdiff; previous patch malformed") 520 - s.pages.Notice(w, fmt.Sprintf("interdiff-error-%d", roundIdInt), "Failed to calculate interdiff; previous patch is invalid.") 521 - return 522 - } 523 - 524 - interdiff := patchutil.Interdiff(previousPatch, currentPatch) 525 - 526 - s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 527 - LoggedInUser: s.oauth.GetUser(r), 528 - RepoInfo: s.repoResolver.GetRepoInfo(r, user), 529 - Pull: pull, 530 - Round: roundIdInt, 531 - Interdiff: interdiff, 532 - DiffOpts: diffOpts, 533 - }) 496 + s.repoPullHelper(w, r, true) 534 497 } 535 498 536 499 func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { ··· 556 519 func (s *Pulls) RepoPulls(w http.ResponseWriter, r *http.Request) { 557 520 l := s.logger.With("handler", "RepoPulls") 558 521 559 - user := s.oauth.GetUser(r) 522 + user := s.oauth.GetMultiAccountUser(r) 560 523 params := r.URL.Query() 561 524 562 525 state := models.PullOpen ··· 701 664 } 702 665 703 666 s.pages.RepoPulls(w, pages.RepoPullsParams{ 704 - LoggedInUser: s.oauth.GetUser(r), 667 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 705 668 RepoInfo: s.repoResolver.GetRepoInfo(r, user), 706 669 Pulls: pulls, 707 670 LabelDefs: defs, ··· 715 678 } 716 679 717 680 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 718 - user := s.oauth.GetUser(r) 681 + user := s.oauth.GetMultiAccountUser(r) 719 682 f, err := s.repoResolver.Resolve(r) 720 683 if err != nil { 721 684 log.Println("failed to get repo and knot", err) ··· 774 737 } 775 738 atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 776 739 Collection: tangled.RepoPullCommentNSID, 777 - Repo: user.Did, 740 + Repo: user.Active.Did, 778 741 Rkey: tid.TID(), 779 742 Record: &lexutil.LexiconTypeDecoder{ 780 743 Val: &tangled.RepoPullComment{ ··· 791 754 } 792 755 793 756 comment := &models.PullComment{ 794 - OwnerDid: user.Did, 757 + OwnerDid: user.Active.Did, 795 758 RepoAt: f.RepoAt().String(), 796 759 PullId: pull.PullId, 797 760 Body: body, ··· 825 788 } 826 789 827 790 func (s *Pulls) NewPull(w http.ResponseWriter, r *http.Request) { 828 - user := s.oauth.GetUser(r) 791 + user := s.oauth.GetMultiAccountUser(r) 829 792 f, err := s.repoResolver.Resolve(r) 830 793 if err != nil { 831 794 log.Println("failed to get repo and knot", err) ··· 893 856 } 894 857 895 858 // Determine PR type based on input parameters 896 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 859 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 897 860 isPushAllowed := roles.IsPushAllowed() 898 861 isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == "" 899 862 isForkBased := fromFork != "" && sourceBranch != "" ··· 993 956 w http.ResponseWriter, 994 957 r *http.Request, 995 958 repo *models.Repo, 996 - user *oauth.User, 959 + user *oauth.MultiAccountUser, 997 960 title, 998 961 body, 999 962 targetBranch, ··· 1050 1013 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked) 1051 1014 } 1052 1015 1053 - func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) { 1016 + func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, title, body, targetBranch, patch string, isStacked bool) { 1054 1017 if err := s.validator.ValidatePatch(&patch); err != nil { 1055 1018 s.logger.Error("patch validation failed", "err", err) 1056 1019 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") ··· 1060 1023 s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked) 1061 1024 } 1062 1025 1063 - func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 1026 + func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.MultiAccountUser, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) { 1064 1027 repoString := strings.SplitN(forkRepo, "/", 2) 1065 1028 forkOwnerDid := repoString[0] 1066 1029 repoName := repoString[1] ··· 1169 1132 w http.ResponseWriter, 1170 1133 r *http.Request, 1171 1134 repo *models.Repo, 1172 - user *oauth.User, 1135 + user *oauth.MultiAccountUser, 1173 1136 title, body, targetBranch string, 1174 1137 patch string, 1175 1138 combined string, ··· 1241 1204 Title: title, 1242 1205 Body: body, 1243 1206 TargetBranch: targetBranch, 1244 - OwnerDid: user.Did, 1207 + OwnerDid: user.Active.Did, 1245 1208 RepoAt: repo.RepoAt(), 1246 1209 Rkey: rkey, 1247 1210 Mentions: mentions, ··· 1273 1236 1274 1237 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1275 1238 Collection: tangled.RepoPullNSID, 1276 - Repo: user.Did, 1239 + Repo: user.Active.Did, 1277 1240 Rkey: rkey, 1278 1241 Record: &lexutil.LexiconTypeDecoder{ 1279 1242 Val: &tangled.RepoPull{ ··· 1310 1273 w http.ResponseWriter, 1311 1274 r *http.Request, 1312 1275 repo *models.Repo, 1313 - user *oauth.User, 1276 + user *oauth.MultiAccountUser, 1314 1277 targetBranch string, 1315 1278 patch string, 1316 1279 sourceRev string, ··· 1378 1341 }) 1379 1342 } 1380 1343 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1381 - Repo: user.Did, 1344 + Repo: user.Active.Did, 1382 1345 Writes: writes, 1383 1346 }) 1384 1347 if err != nil { ··· 1450 1413 } 1451 1414 1452 1415 func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 1453 - user := s.oauth.GetUser(r) 1416 + user := s.oauth.GetMultiAccountUser(r) 1454 1417 1455 1418 s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{ 1456 1419 RepoInfo: s.repoResolver.GetRepoInfo(r, user), ··· 1458 1421 } 1459 1422 1460 1423 func (s *Pulls) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 1461 - user := s.oauth.GetUser(r) 1424 + user := s.oauth.GetMultiAccountUser(r) 1462 1425 f, err := s.repoResolver.Resolve(r) 1463 1426 if err != nil { 1464 1427 log.Println("failed to get repo and knot", err) ··· 1513 1476 } 1514 1477 1515 1478 func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 1516 - user := s.oauth.GetUser(r) 1479 + user := s.oauth.GetMultiAccountUser(r) 1517 1480 1518 - forks, err := db.GetForksByDid(s.db, user.Did) 1481 + forks, err := db.GetForksByDid(s.db, user.Active.Did) 1519 1482 if err != nil { 1520 1483 log.Println("failed to get forks", err) 1521 1484 return ··· 1529 1492 } 1530 1493 1531 1494 func (s *Pulls) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1532 - user := s.oauth.GetUser(r) 1495 + user := s.oauth.GetMultiAccountUser(r) 1533 1496 1534 1497 f, err := s.repoResolver.Resolve(r) 1535 1498 if err != nil { ··· 1622 1585 } 1623 1586 1624 1587 func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1625 - user := s.oauth.GetUser(r) 1588 + user := s.oauth.GetMultiAccountUser(r) 1626 1589 1627 1590 pull, ok := r.Context().Value("pull").(*models.Pull) 1628 1591 if !ok { ··· 1653 1616 } 1654 1617 1655 1618 func (s *Pulls) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1656 - user := s.oauth.GetUser(r) 1619 + user := s.oauth.GetMultiAccountUser(r) 1657 1620 1658 1621 pull, ok := r.Context().Value("pull").(*models.Pull) 1659 1622 if !ok { ··· 1668 1631 return 1669 1632 } 1670 1633 1671 - if user.Did != pull.OwnerDid { 1634 + if user.Active.Did != pull.OwnerDid { 1672 1635 log.Println("unauthorized user") 1673 1636 w.WriteHeader(http.StatusUnauthorized) 1674 1637 return ··· 1680 1643 } 1681 1644 1682 1645 func (s *Pulls) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1683 - user := s.oauth.GetUser(r) 1646 + user := s.oauth.GetMultiAccountUser(r) 1684 1647 1685 1648 pull, ok := r.Context().Value("pull").(*models.Pull) 1686 1649 if !ok { ··· 1695 1658 return 1696 1659 } 1697 1660 1698 - if user.Did != pull.OwnerDid { 1661 + if user.Active.Did != pull.OwnerDid { 1699 1662 log.Println("unauthorized user") 1700 1663 w.WriteHeader(http.StatusUnauthorized) 1701 1664 return 1702 1665 } 1703 1666 1704 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 1667 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 1705 1668 if !roles.IsPushAllowed() { 1706 1669 log.Println("unauthorized user") 1707 1670 w.WriteHeader(http.StatusUnauthorized) ··· 1745 1708 } 1746 1709 1747 1710 func (s *Pulls) resubmitFork(w http.ResponseWriter, r *http.Request) { 1748 - user := s.oauth.GetUser(r) 1711 + user := s.oauth.GetMultiAccountUser(r) 1749 1712 1750 1713 pull, ok := r.Context().Value("pull").(*models.Pull) 1751 1714 if !ok { ··· 1760 1723 return 1761 1724 } 1762 1725 1763 - if user.Did != pull.OwnerDid { 1726 + if user.Active.Did != pull.OwnerDid { 1764 1727 log.Println("unauthorized user") 1765 1728 w.WriteHeader(http.StatusUnauthorized) 1766 1729 return ··· 1845 1808 w http.ResponseWriter, 1846 1809 r *http.Request, 1847 1810 repo *models.Repo, 1848 - user *oauth.User, 1811 + user *oauth.MultiAccountUser, 1849 1812 pull *models.Pull, 1850 1813 patch string, 1851 1814 combined string, ··· 1901 1864 return 1902 1865 } 1903 1866 1904 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1867 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Active.Did, pull.Rkey) 1905 1868 if err != nil { 1906 1869 // failed to get record 1907 1870 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1917 1880 record := pull.AsRecord() 1918 1881 record.PatchBlob = blob.Blob 1919 1882 record.CreatedAt = time.Now().Format(time.RFC3339) 1883 + record.Source.Sha = newSourceRev 1920 1884 1921 1885 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1922 1886 Collection: tangled.RepoPullNSID, 1923 - Repo: user.Did, 1887 + Repo: user.Active.Did, 1924 1888 Rkey: pull.Rkey, 1925 1889 SwapRecord: ex.Cid, 1926 1890 Record: &lexutil.LexiconTypeDecoder{ ··· 1947 1911 w http.ResponseWriter, 1948 1912 r *http.Request, 1949 1913 repo *models.Repo, 1950 - user *oauth.User, 1914 + user *oauth.MultiAccountUser, 1951 1915 pull *models.Pull, 1952 1916 patch string, 1953 1917 stackId string, ··· 2137 2101 } 2138 2102 2139 2103 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 2140 - Repo: user.Did, 2104 + Repo: user.Active.Did, 2141 2105 Writes: writes, 2142 2106 }) 2143 2107 if err != nil { ··· 2151 2115 } 2152 2116 2153 2117 func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) { 2154 - user := s.oauth.GetUser(r) 2118 + user := s.oauth.GetMultiAccountUser(r) 2155 2119 f, err := s.repoResolver.Resolve(r) 2156 2120 if err != nil { 2157 2121 log.Println("failed to resolve repo:", err) ··· 2262 2226 2263 2227 // notify about the pull merge 2264 2228 for _, p := range pullsToMerge { 2265 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2229 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2266 2230 } 2267 2231 2268 2232 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2270 2234 } 2271 2235 2272 2236 func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) { 2273 - user := s.oauth.GetUser(r) 2237 + user := s.oauth.GetMultiAccountUser(r) 2274 2238 2275 2239 f, err := s.repoResolver.Resolve(r) 2276 2240 if err != nil { ··· 2286 2250 } 2287 2251 2288 2252 // auth filter: only owner or collaborators can close 2289 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 2253 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2290 2254 isOwner := roles.IsOwner() 2291 2255 isCollaborator := roles.IsCollaborator() 2292 - isPullAuthor := user.Did == pull.OwnerDid 2256 + isPullAuthor := user.Active.Did == pull.OwnerDid 2293 2257 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2294 2258 if !isCloseAllowed { 2295 2259 log.Println("failed to close pull") ··· 2335 2299 } 2336 2300 2337 2301 for _, p := range pullsToClose { 2338 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2302 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2339 2303 } 2340 2304 2341 2305 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) ··· 2343 2307 } 2344 2308 2345 2309 func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) { 2346 - user := s.oauth.GetUser(r) 2310 + user := s.oauth.GetMultiAccountUser(r) 2347 2311 2348 2312 f, err := s.repoResolver.Resolve(r) 2349 2313 if err != nil { ··· 2360 2324 } 2361 2325 2362 2326 // auth filter: only owner or collaborators can close 2363 - roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())} 2327 + roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Active.Did, f.Knot, f.DidSlashRepo())} 2364 2328 isOwner := roles.IsOwner() 2365 2329 isCollaborator := roles.IsCollaborator() 2366 - isPullAuthor := user.Did == pull.OwnerDid 2330 + isPullAuthor := user.Active.Did == pull.OwnerDid 2367 2331 isCloseAllowed := isOwner || isCollaborator || isPullAuthor 2368 2332 if !isCloseAllowed { 2369 2333 log.Println("failed to close pull") ··· 2409 2373 } 2410 2374 2411 2375 for _, p := range pullsToReopen { 2412 - s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p) 2376 + s.notifier.NewPullState(r.Context(), syntax.DID(user.Active.Did), p) 2413 2377 } 2414 2378 2415 2379 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 2416 2380 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId)) 2417 2381 } 2418 2382 2419 - func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2383 + func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.MultiAccountUser, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) { 2420 2384 formatPatches, err := patchutil.ExtractPatches(patch) 2421 2385 if err != nil { 2422 2386 return nil, fmt.Errorf("Failed to extract patches: %v", err) ··· 2452 2416 Title: title, 2453 2417 Body: body, 2454 2418 TargetBranch: targetBranch, 2455 - OwnerDid: user.Did, 2419 + OwnerDid: user.Active.Did, 2456 2420 RepoAt: repo.RepoAt(), 2457 2421 Rkey: rkey, 2458 2422 Mentions: mentions,
+64 -19
appview/repo/archive.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "io" 5 6 "net/http" 6 7 "net/url" 7 8 "strings" 8 9 9 - "tangled.org/core/api/tangled" 10 - xrpcclient "tangled.org/core/appview/xrpcclient" 11 - 12 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 10 "github.com/go-chi/chi/v5" 14 - "github.com/go-git/go-git/v5/plumbing" 15 11 ) 16 12 17 13 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { ··· 29 25 scheme = "https" 30 26 } 31 27 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 32 - xrpcc := &indigoxrpc.Client{ 33 - Host: host, 34 - } 35 28 didSlashRepo := f.DidSlashRepo() 36 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo) 37 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 38 - l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 29 + 30 + // build the xrpc url 31 + u, err := url.Parse(host) 32 + if err != nil { 33 + l.Error("failed to parse host URL", "err", err) 39 34 rp.pages.Error503(w) 40 35 return 41 36 } 42 - // Set headers for file download, just pass along whatever the knot specifies 43 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 44 - filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 45 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 46 - w.Header().Set("Content-Type", "application/gzip") 47 - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 48 - // Write the archive data directly 49 - w.Write(archiveBytes) 37 + 38 + u.Path = "/xrpc/sh.tangled.repo.archive" 39 + query := url.Values{} 40 + query.Set("format", "tar.gz") 41 + query.Set("prefix", r.URL.Query().Get("prefix")) 42 + query.Set("ref", ref) 43 + query.Set("repo", didSlashRepo) 44 + u.RawQuery = query.Encode() 45 + 46 + xrpcURL := u.String() 47 + 48 + // make the get request 49 + resp, err := http.Get(xrpcURL) 50 + if err != nil { 51 + l.Error("failed to call XRPC repo.archive", "err", err) 52 + rp.pages.Error503(w) 53 + return 54 + } 55 + 56 + // pass through headers from upstream response 57 + if contentDisposition := resp.Header.Get("Content-Disposition"); contentDisposition != "" { 58 + w.Header().Set("Content-Disposition", contentDisposition) 59 + } 60 + if contentType := resp.Header.Get("Content-Type"); contentType != "" { 61 + w.Header().Set("Content-Type", contentType) 62 + } 63 + if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { 64 + w.Header().Set("Content-Length", contentLength) 65 + } 66 + if link := resp.Header.Get("Link"); link != "" { 67 + if resolvedRef, err := extractImmutableLink(link); err == nil { 68 + newLink := fmt.Sprintf("<%s/%s/archive/%s.tar.gz>; rel=\"immutable\"", 69 + rp.config.Core.AppviewHost, f.DidSlashRepo(), resolvedRef) 70 + w.Header().Set("Link", newLink) 71 + } 72 + } 73 + 74 + // stream the archive data directly 75 + if _, err := io.Copy(w, resp.Body); err != nil { 76 + l.Error("failed to write response", "err", err) 77 + } 78 + } 79 + 80 + func extractImmutableLink(linkHeader string) (string, error) { 81 + trimmed := strings.TrimPrefix(linkHeader, "<") 82 + trimmed = strings.TrimSuffix(trimmed, ">; rel=\"immutable\"") 83 + 84 + parsedLink, err := url.Parse(trimmed) 85 + if err != nil { 86 + return "", err 87 + } 88 + 89 + resolvedRef := parsedLink.Query().Get("ref") 90 + if resolvedRef == "" { 91 + return "", fmt.Errorf("no ref found in link header") 92 + } 93 + 94 + return resolvedRef, nil 50 95 }
+6 -6
appview/repo/artifact.go
··· 30 30 31 31 // TODO: proper statuses here on early exit 32 32 func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { 33 - user := rp.oauth.GetUser(r) 33 + user := rp.oauth.GetMultiAccountUser(r) 34 34 tagParam := chi.URLParam(r, "tag") 35 35 f, err := rp.repoResolver.Resolve(r) 36 36 if err != nil { ··· 75 75 76 76 putRecordResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 77 77 Collection: tangled.RepoArtifactNSID, 78 - Repo: user.Did, 78 + Repo: user.Active.Did, 79 79 Rkey: rkey, 80 80 Record: &lexutil.LexiconTypeDecoder{ 81 81 Val: &tangled.RepoArtifact{ ··· 104 104 defer tx.Rollback() 105 105 106 106 artifact := models.Artifact{ 107 - Did: user.Did, 107 + Did: user.Active.Did, 108 108 Rkey: rkey, 109 109 RepoAt: f.RepoAt(), 110 110 Tag: tag.Tag.Hash, ··· 220 220 221 221 // TODO: proper statuses here on early exit 222 222 func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 223 - user := rp.oauth.GetUser(r) 223 + user := rp.oauth.GetMultiAccountUser(r) 224 224 tagParam := chi.URLParam(r, "tag") 225 225 filename := chi.URLParam(r, "file") 226 226 f, err := rp.repoResolver.Resolve(r) ··· 251 251 252 252 artifact := artifacts[0] 253 253 254 - if user.Did != artifact.Did { 254 + if user.Active.Did != artifact.Did { 255 255 log.Println("user not authorized to delete artifact", err) 256 256 rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 257 257 return ··· 259 259 260 260 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 261 261 Collection: tangled.RepoArtifactNSID, 262 - Repo: user.Did, 262 + Repo: user.Active.Did, 263 263 Rkey: artifact.Rkey, 264 264 }) 265 265 if err != nil {
+18 -3
appview/repo/blob.go
··· 76 76 // Create the blob view 77 77 blobView := NewBlobView(resp, rp.config, f, ref, filePath, r.URL.Query()) 78 78 79 - user := rp.oauth.GetUser(r) 79 + user := rp.oauth.GetMultiAccountUser(r) 80 80 81 81 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 82 82 LoggedInUser: user, ··· 219 219 if resp.Content != nil { 220 220 bytes, _ := base64.StdEncoding.DecodeString(*resp.Content) 221 221 view.Contents = string(bytes) 222 - view.Lines = strings.Count(view.Contents, "\n") + 1 222 + view.Lines = countLines(view.Contents) 223 223 } 224 224 225 225 case ".mp4", ".webm", ".ogg", ".mov", ".avi": ··· 238 238 239 239 if resp.Content != nil { 240 240 view.Contents = *resp.Content 241 - view.Lines = strings.Count(view.Contents, "\n") + 1 241 + view.Lines = countLines(view.Contents) 242 242 } 243 243 244 244 // with text, we may be dealing with markdown ··· 291 291 } 292 292 return slices.Contains(textualTypes, mimeType) 293 293 } 294 + 295 + // TODO: dedup with strings 296 + func countLines(content string) int { 297 + if content == "" { 298 + return 0 299 + } 300 + 301 + count := strings.Count(content, "\n") 302 + 303 + if !strings.HasSuffix(content, "\n") { 304 + count++ 305 + } 306 + 307 + return count 308 + }
+1 -1
appview/repo/branches.go
··· 43 43 return 44 44 } 45 45 sortBranches(result.Branches) 46 - user := rp.oauth.GetUser(r) 46 + user := rp.oauth.GetMultiAccountUser(r) 47 47 rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 48 48 LoggedInUser: user, 49 49 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
+2 -2
appview/repo/compare.go
··· 20 20 func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) { 21 21 l := rp.logger.With("handler", "RepoCompareNew") 22 22 23 - user := rp.oauth.GetUser(r) 23 + user := rp.oauth.GetMultiAccountUser(r) 24 24 f, err := rp.repoResolver.Resolve(r) 25 25 if err != nil { 26 26 l.Error("failed to get repo and knot", "err", err) ··· 101 101 func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) { 102 102 l := rp.logger.With("handler", "RepoCompare") 103 103 104 - user := rp.oauth.GetUser(r) 104 + user := rp.oauth.GetMultiAccountUser(r) 105 105 f, err := rp.repoResolver.Resolve(r) 106 106 if err != nil { 107 107 l.Error("failed to get repo and knot", "err", err)
+1 -1
appview/repo/index.go
··· 51 51 Host: host, 52 52 } 53 53 54 - user := rp.oauth.GetUser(r) 54 + user := rp.oauth.GetMultiAccountUser(r) 55 55 56 56 // Build index response from multiple XRPC calls 57 57 result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
+2 -2
appview/repo/log.go
··· 109 109 } 110 110 } 111 111 112 - user := rp.oauth.GetUser(r) 112 + user := rp.oauth.GetMultiAccountUser(r) 113 113 114 114 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 115 115 if err != nil { ··· 197 197 l.Error("failed to GetVerifiedCommits", "err", err) 198 198 } 199 199 200 - user := rp.oauth.GetUser(r) 200 + user := rp.oauth.GetMultiAccountUser(r) 201 201 pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This}) 202 202 if err != nil { 203 203 l.Error("failed to getPipelineStatuses", "err", err)
+34 -34
appview/repo/repo.go
··· 81 81 82 82 // modify the spindle configured for this repo 83 83 func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { 84 - user := rp.oauth.GetUser(r) 84 + user := rp.oauth.GetMultiAccountUser(r) 85 85 l := rp.logger.With("handler", "EditSpindle") 86 - l = l.With("did", user.Did) 86 + l = l.With("did", user.Active.Did) 87 87 88 88 errorId := "operation-error" 89 89 fail := func(msg string, err error) { ··· 107 107 108 108 if !removingSpindle { 109 109 // ensure that this is a valid spindle for this user 110 - validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Did) 110 + validSpindles, err := rp.enforcer.GetSpindlesForUser(user.Active.Did) 111 111 if err != nil { 112 112 fail("Failed to find spindles. Try again later.", err) 113 113 return ··· 168 168 } 169 169 170 170 func (rp *Repo) AddLabelDef(w http.ResponseWriter, r *http.Request) { 171 - user := rp.oauth.GetUser(r) 171 + user := rp.oauth.GetMultiAccountUser(r) 172 172 l := rp.logger.With("handler", "AddLabel") 173 - l = l.With("did", user.Did) 173 + l = l.With("did", user.Active.Did) 174 174 175 175 f, err := rp.repoResolver.Resolve(r) 176 176 if err != nil { ··· 216 216 } 217 217 218 218 label := models.LabelDefinition{ 219 - Did: user.Did, 219 + Did: user.Active.Did, 220 220 Rkey: tid.TID(), 221 221 Name: name, 222 222 ValueType: valueType, ··· 327 327 } 328 328 329 329 func (rp *Repo) DeleteLabelDef(w http.ResponseWriter, r *http.Request) { 330 - user := rp.oauth.GetUser(r) 330 + user := rp.oauth.GetMultiAccountUser(r) 331 331 l := rp.logger.With("handler", "DeleteLabel") 332 - l = l.With("did", user.Did) 332 + l = l.With("did", user.Active.Did) 333 333 334 334 f, err := rp.repoResolver.Resolve(r) 335 335 if err != nil { ··· 435 435 } 436 436 437 437 func (rp *Repo) SubscribeLabel(w http.ResponseWriter, r *http.Request) { 438 - user := rp.oauth.GetUser(r) 438 + user := rp.oauth.GetMultiAccountUser(r) 439 439 l := rp.logger.With("handler", "SubscribeLabel") 440 - l = l.With("did", user.Did) 440 + l = l.With("did", user.Active.Did) 441 441 442 442 f, err := rp.repoResolver.Resolve(r) 443 443 if err != nil { ··· 521 521 } 522 522 523 523 func (rp *Repo) UnsubscribeLabel(w http.ResponseWriter, r *http.Request) { 524 - user := rp.oauth.GetUser(r) 524 + user := rp.oauth.GetMultiAccountUser(r) 525 525 l := rp.logger.With("handler", "UnsubscribeLabel") 526 - l = l.With("did", user.Did) 526 + l = l.With("did", user.Active.Did) 527 527 528 528 f, err := rp.repoResolver.Resolve(r) 529 529 if err != nil { ··· 633 633 } 634 634 state := states[subject] 635 635 636 - user := rp.oauth.GetUser(r) 636 + user := rp.oauth.GetMultiAccountUser(r) 637 637 rp.pages.LabelPanel(w, pages.LabelPanelParams{ 638 638 LoggedInUser: user, 639 639 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), ··· 681 681 } 682 682 state := states[subject] 683 683 684 - user := rp.oauth.GetUser(r) 684 + user := rp.oauth.GetMultiAccountUser(r) 685 685 rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{ 686 686 LoggedInUser: user, 687 687 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), ··· 692 692 } 693 693 694 694 func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 695 - user := rp.oauth.GetUser(r) 695 + user := rp.oauth.GetMultiAccountUser(r) 696 696 l := rp.logger.With("handler", "AddCollaborator") 697 - l = l.With("did", user.Did) 697 + l = l.With("did", user.Active.Did) 698 698 699 699 f, err := rp.repoResolver.Resolve(r) 700 700 if err != nil { ··· 723 723 return 724 724 } 725 725 726 - if collaboratorIdent.DID.String() == user.Did { 726 + if collaboratorIdent.DID.String() == user.Active.Did { 727 727 fail("You seem to be adding yourself as a collaborator.", nil) 728 728 return 729 729 } ··· 738 738 } 739 739 740 740 // emit a record 741 - currentUser := rp.oauth.GetUser(r) 741 + currentUser := rp.oauth.GetMultiAccountUser(r) 742 742 rkey := tid.TID() 743 743 createdAt := time.Now() 744 744 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 745 745 Collection: tangled.RepoCollaboratorNSID, 746 - Repo: currentUser.Did, 746 + Repo: currentUser.Active.Did, 747 747 Rkey: rkey, 748 748 Record: &lexutil.LexiconTypeDecoder{ 749 749 Val: &tangled.RepoCollaborator{ ··· 792 792 } 793 793 794 794 err = db.AddCollaborator(tx, models.Collaborator{ 795 - Did: syntax.DID(currentUser.Did), 795 + Did: syntax.DID(currentUser.Active.Did), 796 796 Rkey: rkey, 797 797 SubjectDid: collaboratorIdent.DID, 798 798 RepoAt: f.RepoAt(), ··· 822 822 } 823 823 824 824 func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 825 - user := rp.oauth.GetUser(r) 825 + user := rp.oauth.GetMultiAccountUser(r) 826 826 l := rp.logger.With("handler", "DeleteRepo") 827 827 828 828 noticeId := "operation-error" ··· 840 840 } 841 841 _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 842 842 Collection: tangled.RepoNSID, 843 - Repo: user.Did, 843 + Repo: user.Active.Did, 844 844 Rkey: f.Rkey, 845 845 }) 846 846 if err != nil { ··· 940 940 ref := chi.URLParam(r, "ref") 941 941 ref, _ = url.PathUnescape(ref) 942 942 943 - user := rp.oauth.GetUser(r) 943 + user := rp.oauth.GetMultiAccountUser(r) 944 944 f, err := rp.repoResolver.Resolve(r) 945 945 if err != nil { 946 946 l.Error("failed to resolve source repo", "err", err) ··· 969 969 r.Context(), 970 970 client, 971 971 &tangled.RepoForkSync_Input{ 972 - Did: user.Did, 972 + Did: user.Active.Did, 973 973 Name: f.Name, 974 974 Source: f.Source, 975 975 Branch: ref, ··· 988 988 func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 989 989 l := rp.logger.With("handler", "ForkRepo") 990 990 991 - user := rp.oauth.GetUser(r) 991 + user := rp.oauth.GetMultiAccountUser(r) 992 992 f, err := rp.repoResolver.Resolve(r) 993 993 if err != nil { 994 994 l.Error("failed to resolve source repo", "err", err) ··· 997 997 998 998 switch r.Method { 999 999 case http.MethodGet: 1000 - user := rp.oauth.GetUser(r) 1001 - knots, err := rp.enforcer.GetKnotsForUser(user.Did) 1000 + user := rp.oauth.GetMultiAccountUser(r) 1001 + knots, err := rp.enforcer.GetKnotsForUser(user.Active.Did) 1002 1002 if err != nil { 1003 1003 rp.pages.Notice(w, "repo", "Invalid user account.") 1004 1004 return ··· 1020 1020 } 1021 1021 l = l.With("targetKnot", targetKnot) 1022 1022 1023 - ok, err := rp.enforcer.E.Enforce(user.Did, targetKnot, targetKnot, "repo:create") 1023 + ok, err := rp.enforcer.E.Enforce(user.Active.Did, targetKnot, targetKnot, "repo:create") 1024 1024 if err != nil || !ok { 1025 1025 rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1026 1026 return ··· 1037 1037 // in the user's account. 1038 1038 existingRepo, err := db.GetRepo( 1039 1039 rp.db, 1040 - orm.FilterEq("did", user.Did), 1040 + orm.FilterEq("did", user.Active.Did), 1041 1041 orm.FilterEq("name", forkName), 1042 1042 ) 1043 1043 if err != nil { ··· 1066 1066 // create an atproto record for this fork 1067 1067 rkey := tid.TID() 1068 1068 repo := &models.Repo{ 1069 - Did: user.Did, 1069 + Did: user.Active.Did, 1070 1070 Name: forkName, 1071 1071 Knot: targetKnot, 1072 1072 Rkey: rkey, ··· 1086 1086 1087 1087 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 1088 1088 Collection: tangled.RepoNSID, 1089 - Repo: user.Did, 1089 + Repo: user.Active.Did, 1090 1090 Rkey: rkey, 1091 1091 Record: &lexutil.LexiconTypeDecoder{ 1092 1092 Val: &record, ··· 1165 1165 } 1166 1166 1167 1167 // acls 1168 - p, _ := securejoin.SecureJoin(user.Did, forkName) 1169 - err = rp.enforcer.AddRepo(user.Did, targetKnot, p) 1168 + p, _ := securejoin.SecureJoin(user.Active.Did, forkName) 1169 + err = rp.enforcer.AddRepo(user.Active.Did, targetKnot, p) 1170 1170 if err != nil { 1171 1171 l.Error("failed to add ACLs", "err", err) 1172 1172 rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 1191 1191 aturi = "" 1192 1192 1193 1193 rp.notifier.NewRepo(r.Context(), repo) 1194 - rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, forkName)) 1194 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, forkName)) 1195 1195 } 1196 1196 } 1197 1197
+5 -22
appview/repo/settings.go
··· 22 22 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 23 23 ) 24 24 25 - type tab = map[string]any 26 - 27 - var ( 28 - // would be great to have ordered maps right about now 29 - settingsTabs []tab = []tab{ 30 - {"Name": "general", "Icon": "sliders-horizontal"}, 31 - {"Name": "access", "Icon": "users"}, 32 - {"Name": "pipelines", "Icon": "layers-2"}, 33 - } 34 - ) 35 - 36 25 func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 37 26 l := rp.logger.With("handler", "SetDefaultBranch") 38 27 ··· 79 68 } 80 69 81 70 func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 82 - user := rp.oauth.GetUser(r) 71 + user := rp.oauth.GetMultiAccountUser(r) 83 72 l := rp.logger.With("handler", "Secrets") 84 - l = l.With("did", user.Did) 73 + l = l.With("did", user.Active.Did) 85 74 86 75 f, err := rp.repoResolver.Resolve(r) 87 76 if err != nil { ··· 185 174 l := rp.logger.With("handler", "generalSettings") 186 175 187 176 f, err := rp.repoResolver.Resolve(r) 188 - user := rp.oauth.GetUser(r) 177 + user := rp.oauth.GetMultiAccountUser(r) 189 178 190 179 scheme := "http" 191 180 if !rp.config.Core.Dev { ··· 262 251 DefaultLabels: defaultLabels, 263 252 SubscribedLabels: subscribedLabels, 264 253 ShouldSubscribeAll: shouldSubscribeAll, 265 - Tabs: settingsTabs, 266 - Tab: "general", 267 254 }) 268 255 } 269 256 ··· 271 258 l := rp.logger.With("handler", "accessSettings") 272 259 273 260 f, err := rp.repoResolver.Resolve(r) 274 - user := rp.oauth.GetUser(r) 261 + user := rp.oauth.GetMultiAccountUser(r) 275 262 276 263 collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) { 277 264 repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot) ··· 308 295 rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{ 309 296 LoggedInUser: user, 310 297 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 311 - Tabs: settingsTabs, 312 - Tab: "access", 313 298 Collaborators: collaborators, 314 299 }) 315 300 } ··· 318 303 l := rp.logger.With("handler", "pipelineSettings") 319 304 320 305 f, err := rp.repoResolver.Resolve(r) 321 - user := rp.oauth.GetUser(r) 306 + user := rp.oauth.GetMultiAccountUser(r) 322 307 323 308 // all spindles that the repo owner is a member of 324 309 spindles, err := rp.enforcer.GetSpindlesForUser(f.Did) ··· 369 354 rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{ 370 355 LoggedInUser: user, 371 356 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 372 - Tabs: settingsTabs, 373 - Tab: "pipelines", 374 357 Spindles: spindles, 375 358 CurrentSpindle: f.Spindle, 376 359 Secrets: niceSecret,
+1 -1
appview/repo/tags.go
··· 69 69 danglingArtifacts = append(danglingArtifacts, a) 70 70 } 71 71 } 72 - user := rp.oauth.GetUser(r) 72 + user := rp.oauth.GetMultiAccountUser(r) 73 73 rp.pages.RepoTags(w, pages.RepoTagsParams{ 74 74 LoggedInUser: user, 75 75 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
+1 -1
appview/repo/tree.go
··· 88 88 http.Redirect(w, r, redirectTo, http.StatusFound) 89 89 return 90 90 } 91 - user := rp.oauth.GetUser(r) 91 + user := rp.oauth.GetMultiAccountUser(r) 92 92 var breadcrumbs [][]string 93 93 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))}) 94 94 if treePath != "" {
+4 -4
appview/reporesolver/resolver.go
··· 55 55 // 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo` 56 56 // 3. [x] remove `ResolvedRepo` 57 57 // 4. [ ] replace reporesolver to reposervice 58 - func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.User) repoinfo.RepoInfo { 58 + func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.MultiAccountUser) repoinfo.RepoInfo { 59 59 ownerId, ook := r.Context().Value("resolvedId").(identity.Identity) 60 60 repo, rok := r.Context().Value("repo").(*models.Repo) 61 61 if !ook || !rok { ··· 69 69 repoAt := repo.RepoAt() 70 70 isStarred := false 71 71 roles := repoinfo.RolesInRepo{} 72 - if user != nil { 73 - isStarred = db.GetStarStatus(rr.execer, user.Did, repoAt) 74 - roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo()) 72 + if user != nil && user.Active != nil { 73 + isStarred = db.GetStarStatus(rr.execer, user.Active.Did, repoAt) 74 + roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Active.Did, repo.Knot, repo.DidSlashRepo()) 75 75 } 76 76 77 77 stats := repo.RepoStats
+6 -27
appview/settings/settings.go
··· 35 35 Config *config.Config 36 36 } 37 37 38 - type tab = map[string]any 39 - 40 - var ( 41 - settingsTabs []tab = []tab{ 42 - {"Name": "profile", "Icon": "user"}, 43 - {"Name": "keys", "Icon": "key"}, 44 - {"Name": "emails", "Icon": "mail"}, 45 - {"Name": "notifications", "Icon": "bell"}, 46 - {"Name": "knots", "Icon": "volleyball"}, 47 - {"Name": "spindles", "Icon": "spool"}, 48 - } 49 - ) 50 - 51 38 func (s *Settings) Router() http.Handler { 52 39 r := chi.NewRouter() 53 40 ··· 81 68 } 82 69 83 70 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 84 - user := s.OAuth.GetUser(r) 71 + user := s.OAuth.GetMultiAccountUser(r) 85 72 86 73 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 87 74 LoggedInUser: user, 88 - Tabs: settingsTabs, 89 - Tab: "profile", 90 75 }) 91 76 } 92 77 93 78 func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) { 94 - user := s.OAuth.GetUser(r) 79 + user := s.OAuth.GetMultiAccountUser(r) 95 80 did := s.OAuth.GetDid(r) 96 81 97 82 prefs, err := db.GetNotificationPreference(s.Db, did) ··· 104 89 s.Pages.UserNotificationSettings(w, pages.UserNotificationSettingsParams{ 105 90 LoggedInUser: user, 106 91 Preferences: prefs, 107 - Tabs: settingsTabs, 108 - Tab: "notifications", 109 92 }) 110 93 } 111 94 ··· 137 120 } 138 121 139 122 func (s *Settings) keysSettings(w http.ResponseWriter, r *http.Request) { 140 - user := s.OAuth.GetUser(r) 141 - pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Did) 123 + user := s.OAuth.GetMultiAccountUser(r) 124 + pubKeys, err := db.GetPublicKeysForDid(s.Db, user.Active.Did) 142 125 if err != nil { 143 126 log.Println(err) 144 127 } ··· 146 129 s.Pages.UserKeysSettings(w, pages.UserKeysSettingsParams{ 147 130 LoggedInUser: user, 148 131 PubKeys: pubKeys, 149 - Tabs: settingsTabs, 150 - Tab: "keys", 151 132 }) 152 133 } 153 134 154 135 func (s *Settings) emailsSettings(w http.ResponseWriter, r *http.Request) { 155 - user := s.OAuth.GetUser(r) 156 - emails, err := db.GetAllEmails(s.Db, user.Did) 136 + user := s.OAuth.GetMultiAccountUser(r) 137 + emails, err := db.GetAllEmails(s.Db, user.Active.Did) 157 138 if err != nil { 158 139 log.Println(err) 159 140 } ··· 161 142 s.Pages.UserEmailsSettings(w, pages.UserEmailsSettingsParams{ 162 143 LoggedInUser: user, 163 144 Emails: emails, 164 - Tabs: settingsTabs, 165 - Tab: "emails", 166 145 }) 167 146 } 168 147
+41 -56
appview/spindles/spindles.go
··· 39 39 Logger *slog.Logger 40 40 } 41 41 42 - type tab = map[string]any 43 - 44 - var ( 45 - spindlesTabs []tab = []tab{ 46 - {"Name": "profile", "Icon": "user"}, 47 - {"Name": "keys", "Icon": "key"}, 48 - {"Name": "emails", "Icon": "mail"}, 49 - {"Name": "notifications", "Icon": "bell"}, 50 - {"Name": "knots", "Icon": "volleyball"}, 51 - {"Name": "spindles", "Icon": "spool"}, 52 - } 53 - ) 54 - 55 42 func (s *Spindles) Router() http.Handler { 56 43 r := chi.NewRouter() 57 44 ··· 69 56 } 70 57 71 58 func (s *Spindles) spindles(w http.ResponseWriter, r *http.Request) { 72 - user := s.OAuth.GetUser(r) 59 + user := s.OAuth.GetMultiAccountUser(r) 73 60 all, err := db.GetSpindles( 74 61 s.Db, 75 - orm.FilterEq("owner", user.Did), 62 + orm.FilterEq("owner", user.Active.Did), 76 63 ) 77 64 if err != nil { 78 65 s.Logger.Error("failed to fetch spindles", "err", err) ··· 83 70 s.Pages.Spindles(w, pages.SpindlesParams{ 84 71 LoggedInUser: user, 85 72 Spindles: all, 86 - Tabs: spindlesTabs, 87 73 Tab: "spindles", 88 74 }) 89 75 } ··· 91 77 func (s *Spindles) dashboard(w http.ResponseWriter, r *http.Request) { 92 78 l := s.Logger.With("handler", "dashboard") 93 79 94 - user := s.OAuth.GetUser(r) 95 - l = l.With("user", user.Did) 80 + user := s.OAuth.GetMultiAccountUser(r) 81 + l = l.With("user", user.Active.Did) 96 82 97 83 instance := chi.URLParam(r, "instance") 98 84 if instance == "" { ··· 103 89 spindles, err := db.GetSpindles( 104 90 s.Db, 105 91 orm.FilterEq("instance", instance), 106 - orm.FilterEq("owner", user.Did), 92 + orm.FilterEq("owner", user.Active.Did), 107 93 orm.FilterIsNot("verified", "null"), 108 94 ) 109 95 if err != nil || len(spindles) != 1 { ··· 143 129 Spindle: spindle, 144 130 Members: members, 145 131 Repos: repoMap, 146 - Tabs: spindlesTabs, 147 132 Tab: "spindles", 148 133 }) 149 134 } ··· 155 140 // 156 141 // if the spindle is not up yet, the user is free to retry verification at a later point 157 142 func (s *Spindles) register(w http.ResponseWriter, r *http.Request) { 158 - user := s.OAuth.GetUser(r) 143 + user := s.OAuth.GetMultiAccountUser(r) 159 144 l := s.Logger.With("handler", "register") 160 145 161 146 noticeId := "register-error" ··· 176 161 return 177 162 } 178 163 l = l.With("instance", instance) 179 - l = l.With("user", user.Did) 164 + l = l.With("user", user.Active.Did) 180 165 181 166 tx, err := s.Db.Begin() 182 167 if err != nil { ··· 190 175 }() 191 176 192 177 err = db.AddSpindle(tx, models.Spindle{ 193 - Owner: syntax.DID(user.Did), 178 + Owner: syntax.DID(user.Active.Did), 194 179 Instance: instance, 195 180 }) 196 181 if err != nil { ··· 214 199 return 215 200 } 216 201 217 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Did, instance) 202 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.SpindleNSID, user.Active.Did, instance) 218 203 var exCid *string 219 204 if ex != nil { 220 205 exCid = ex.Cid ··· 223 208 // re-announce by registering under same rkey 224 209 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 225 210 Collection: tangled.SpindleNSID, 226 - Repo: user.Did, 211 + Repo: user.Active.Did, 227 212 Rkey: instance, 228 213 Record: &lexutil.LexiconTypeDecoder{ 229 214 Val: &tangled.Spindle{ ··· 254 239 } 255 240 256 241 // begin verification 257 - err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 242 + err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev) 258 243 if err != nil { 259 244 l.Error("verification failed", "err", err) 260 245 s.Pages.HxRefresh(w) 261 246 return 262 247 } 263 248 264 - _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 249 + _, err = serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did) 265 250 if err != nil { 266 251 l.Error("failed to mark verified", "err", err) 267 252 s.Pages.HxRefresh(w) ··· 273 258 } 274 259 275 260 func (s *Spindles) delete(w http.ResponseWriter, r *http.Request) { 276 - user := s.OAuth.GetUser(r) 261 + user := s.OAuth.GetMultiAccountUser(r) 277 262 l := s.Logger.With("handler", "delete") 278 263 279 264 noticeId := "operation-error" ··· 291 276 292 277 spindles, err := db.GetSpindles( 293 278 s.Db, 294 - orm.FilterEq("owner", user.Did), 279 + orm.FilterEq("owner", user.Active.Did), 295 280 orm.FilterEq("instance", instance), 296 281 ) 297 282 if err != nil || len(spindles) != 1 { ··· 300 285 return 301 286 } 302 287 303 - if string(spindles[0].Owner) != user.Did { 304 - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 288 + if string(spindles[0].Owner) != user.Active.Did { 289 + l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 305 290 s.Pages.Notice(w, noticeId, "Failed to delete spindle, unauthorized deletion attempt.") 306 291 return 307 292 } ··· 320 305 // remove spindle members first 321 306 err = db.RemoveSpindleMember( 322 307 tx, 323 - orm.FilterEq("did", user.Did), 308 + orm.FilterEq("did", user.Active.Did), 324 309 orm.FilterEq("instance", instance), 325 310 ) 326 311 if err != nil { ··· 331 316 332 317 err = db.DeleteSpindle( 333 318 tx, 334 - orm.FilterEq("owner", user.Did), 319 + orm.FilterEq("owner", user.Active.Did), 335 320 orm.FilterEq("instance", instance), 336 321 ) 337 322 if err != nil { ··· 359 344 360 345 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 361 346 Collection: tangled.SpindleNSID, 362 - Repo: user.Did, 347 + Repo: user.Active.Did, 363 348 Rkey: instance, 364 349 }) 365 350 if err != nil { ··· 391 376 } 392 377 393 378 func (s *Spindles) retry(w http.ResponseWriter, r *http.Request) { 394 - user := s.OAuth.GetUser(r) 379 + user := s.OAuth.GetMultiAccountUser(r) 395 380 l := s.Logger.With("handler", "retry") 396 381 397 382 noticeId := "operation-error" ··· 407 392 return 408 393 } 409 394 l = l.With("instance", instance) 410 - l = l.With("user", user.Did) 395 + l = l.With("user", user.Active.Did) 411 396 412 397 spindles, err := db.GetSpindles( 413 398 s.Db, 414 - orm.FilterEq("owner", user.Did), 399 + orm.FilterEq("owner", user.Active.Did), 415 400 orm.FilterEq("instance", instance), 416 401 ) 417 402 if err != nil || len(spindles) != 1 { ··· 420 405 return 421 406 } 422 407 423 - if string(spindles[0].Owner) != user.Did { 424 - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 408 + if string(spindles[0].Owner) != user.Active.Did { 409 + l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 425 410 s.Pages.Notice(w, noticeId, "Failed to verify spindle, unauthorized verification attempt.") 426 411 return 427 412 } 428 413 429 414 // begin verification 430 - err = serververify.RunVerification(r.Context(), instance, user.Did, s.Config.Core.Dev) 415 + err = serververify.RunVerification(r.Context(), instance, user.Active.Did, s.Config.Core.Dev) 431 416 if err != nil { 432 417 l.Error("verification failed", "err", err) 433 418 ··· 445 430 return 446 431 } 447 432 448 - rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Did) 433 + rowId, err := serververify.MarkSpindleVerified(s.Db, s.Enforcer, instance, user.Active.Did) 449 434 if err != nil { 450 435 l.Error("failed to mark verified", "err", err) 451 436 s.Pages.Notice(w, noticeId, err.Error()) ··· 473 458 } 474 459 475 460 func (s *Spindles) addMember(w http.ResponseWriter, r *http.Request) { 476 - user := s.OAuth.GetUser(r) 461 + user := s.OAuth.GetMultiAccountUser(r) 477 462 l := s.Logger.With("handler", "addMember") 478 463 479 464 instance := chi.URLParam(r, "instance") ··· 483 468 return 484 469 } 485 470 l = l.With("instance", instance) 486 - l = l.With("user", user.Did) 471 + l = l.With("user", user.Active.Did) 487 472 488 473 spindles, err := db.GetSpindles( 489 474 s.Db, 490 - orm.FilterEq("owner", user.Did), 475 + orm.FilterEq("owner", user.Active.Did), 491 476 orm.FilterEq("instance", instance), 492 477 ) 493 478 if err != nil || len(spindles) != 1 { ··· 502 487 s.Pages.Notice(w, noticeId, defaultErr) 503 488 } 504 489 505 - if string(spindles[0].Owner) != user.Did { 506 - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 490 + if string(spindles[0].Owner) != user.Active.Did { 491 + l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 507 492 s.Pages.Notice(w, noticeId, "Failed to add member, unauthorized attempt.") 508 493 return 509 494 } ··· 552 537 553 538 // add member to db 554 539 if err = db.AddSpindleMember(tx, models.SpindleMember{ 555 - Did: syntax.DID(user.Did), 540 + Did: syntax.DID(user.Active.Did), 556 541 Rkey: rkey, 557 542 Instance: instance, 558 543 Subject: memberId.DID, ··· 570 555 571 556 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 572 557 Collection: tangled.SpindleMemberNSID, 573 - Repo: user.Did, 558 + Repo: user.Active.Did, 574 559 Rkey: rkey, 575 560 Record: &lexutil.LexiconTypeDecoder{ 576 561 Val: &tangled.SpindleMember{ ··· 603 588 } 604 589 605 590 func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) { 606 - user := s.OAuth.GetUser(r) 591 + user := s.OAuth.GetMultiAccountUser(r) 607 592 l := s.Logger.With("handler", "removeMember") 608 593 609 594 noticeId := "operation-error" ··· 619 604 return 620 605 } 621 606 l = l.With("instance", instance) 622 - l = l.With("user", user.Did) 607 + l = l.With("user", user.Active.Did) 623 608 624 609 spindles, err := db.GetSpindles( 625 610 s.Db, 626 - orm.FilterEq("owner", user.Did), 611 + orm.FilterEq("owner", user.Active.Did), 627 612 orm.FilterEq("instance", instance), 628 613 ) 629 614 if err != nil || len(spindles) != 1 { ··· 632 617 return 633 618 } 634 619 635 - if string(spindles[0].Owner) != user.Did { 636 - l.Error("unauthorized", "user", user.Did, "owner", spindles[0].Owner) 620 + if string(spindles[0].Owner) != user.Active.Did { 621 + l.Error("unauthorized", "user", user.Active.Did, "owner", spindles[0].Owner) 637 622 s.Pages.Notice(w, noticeId, "Failed to remove member, unauthorized attempt.") 638 623 return 639 624 } ··· 668 653 // get the record from the DB first: 669 654 members, err := db.GetSpindleMembers( 670 655 s.Db, 671 - orm.FilterEq("did", user.Did), 656 + orm.FilterEq("did", user.Active.Did), 672 657 orm.FilterEq("instance", instance), 673 658 orm.FilterEq("subject", memberId.DID), 674 659 ) ··· 681 666 // remove from db 682 667 if err = db.RemoveSpindleMember( 683 668 tx, 684 - orm.FilterEq("did", user.Did), 669 + orm.FilterEq("did", user.Active.Did), 685 670 orm.FilterEq("instance", instance), 686 671 orm.FilterEq("subject", memberId.DID), 687 672 ); err != nil { ··· 707 692 // remove from pds 708 693 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 709 694 Collection: tangled.SpindleMemberNSID, 710 - Repo: user.Did, 695 + Repo: user.Active.Did, 711 696 Rkey: members[0].Rkey, 712 697 }) 713 698 if err != nil {
+83
appview/state/accounts.go
··· 1 + package state 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + ) 8 + 9 + func (s *State) SwitchAccount(w http.ResponseWriter, r *http.Request) { 10 + l := s.logger.With("handler", "SwitchAccount") 11 + 12 + if err := r.ParseForm(); err != nil { 13 + l.Error("failed to parse form", "err", err) 14 + http.Error(w, "invalid request", http.StatusBadRequest) 15 + return 16 + } 17 + 18 + did := r.FormValue("did") 19 + if did == "" { 20 + http.Error(w, "missing did", http.StatusBadRequest) 21 + return 22 + } 23 + 24 + if err := s.oauth.SwitchAccount(w, r, did); err != nil { 25 + l.Error("failed to switch account", "err", err) 26 + s.pages.HxRedirect(w, "/login?error=session") 27 + return 28 + } 29 + 30 + l.Info("switched account", "did", did) 31 + s.pages.HxRedirect(w, "/") 32 + } 33 + 34 + func (s *State) RemoveAccount(w http.ResponseWriter, r *http.Request) { 35 + l := s.logger.With("handler", "RemoveAccount") 36 + 37 + did := chi.URLParam(r, "did") 38 + if did == "" { 39 + http.Error(w, "missing did", http.StatusBadRequest) 40 + return 41 + } 42 + 43 + currentUser := s.oauth.GetMultiAccountUser(r) 44 + isCurrentAccount := currentUser != nil && currentUser.Active.Did == did 45 + 46 + var remainingAccounts []string 47 + if currentUser != nil { 48 + for _, acc := range currentUser.Accounts { 49 + if acc.Did != did { 50 + remainingAccounts = append(remainingAccounts, acc.Did) 51 + } 52 + } 53 + } 54 + 55 + if err := s.oauth.RemoveAccount(w, r, did); err != nil { 56 + l.Error("failed to remove account", "err", err) 57 + http.Error(w, "failed to remove account", http.StatusInternalServerError) 58 + return 59 + } 60 + 61 + l.Info("removed account", "did", did) 62 + 63 + if isCurrentAccount { 64 + if len(remainingAccounts) > 0 { 65 + nextDid := remainingAccounts[0] 66 + if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil { 67 + l.Error("failed to switch to next account", "err", err) 68 + s.pages.HxRedirect(w, "/login") 69 + return 70 + } 71 + s.pages.HxRefresh(w) 72 + return 73 + } 74 + 75 + if err := s.oauth.DeleteSession(w, r); err != nil { 76 + l.Error("failed to delete session", "err", err) 77 + } 78 + s.pages.HxRedirect(w, "/login") 79 + return 80 + } 81 + 82 + s.pages.HxRefresh(w) 83 + }
+7 -7
appview/state/follow.go
··· 15 15 ) 16 16 17 17 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 18 - currentUser := s.oauth.GetUser(r) 18 + currentUser := s.oauth.GetMultiAccountUser(r) 19 19 20 20 subject := r.URL.Query().Get("subject") 21 21 if subject == "" { ··· 29 29 return 30 30 } 31 31 32 - if currentUser.Did == subjectIdent.DID.String() { 32 + if currentUser.Active.Did == subjectIdent.DID.String() { 33 33 log.Println("cant follow or unfollow yourself") 34 34 return 35 35 } ··· 46 46 rkey := tid.TID() 47 47 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 48 48 Collection: tangled.GraphFollowNSID, 49 - Repo: currentUser.Did, 49 + Repo: currentUser.Active.Did, 50 50 Rkey: rkey, 51 51 Record: &lexutil.LexiconTypeDecoder{ 52 52 Val: &tangled.GraphFollow{ ··· 62 62 log.Println("created atproto record: ", resp.Uri) 63 63 64 64 follow := &models.Follow{ 65 - UserDid: currentUser.Did, 65 + UserDid: currentUser.Active.Did, 66 66 SubjectDid: subjectIdent.DID.String(), 67 67 Rkey: rkey, 68 68 } ··· 89 89 return 90 90 case http.MethodDelete: 91 91 // find the record in the db 92 - follow, err := db.GetFollow(s.db, currentUser.Did, subjectIdent.DID.String()) 92 + follow, err := db.GetFollow(s.db, currentUser.Active.Did, subjectIdent.DID.String()) 93 93 if err != nil { 94 94 log.Println("failed to get follow relationship") 95 95 return ··· 97 97 98 98 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 99 99 Collection: tangled.GraphFollowNSID, 100 - Repo: currentUser.Did, 100 + Repo: currentUser.Active.Did, 101 101 Rkey: follow.Rkey, 102 102 }) 103 103 ··· 106 106 return 107 107 } 108 108 109 - err = db.DeleteFollowByRkey(s.db, currentUser.Did, follow.Rkey) 109 + err = db.DeleteFollowByRkey(s.db, currentUser.Active.Did, follow.Rkey) 110 110 if err != nil { 111 111 log.Println("failed to delete follow from DB") 112 112 // this is not an issue, the firehose event might have already done this
+1 -1
appview/state/gfi.go
··· 15 15 ) 16 16 17 17 func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) { 18 - user := s.oauth.GetUser(r) 18 + user := s.oauth.GetMultiAccountUser(r) 19 19 20 20 page := pagination.FromContext(r.Context()) 21 21
+1 -1
appview/state/knotstream.go
··· 122 122 if ce == nil { 123 123 continue 124 124 } 125 - if ce.Email == ke.Address { 125 + if ce.Email == ke.Address || ce.Email == record.CommitterDid { 126 126 count += int(ce.Count) 127 127 } 128 128 }
+57 -7
appview/state/login.go
··· 5 5 "net/http" 6 6 "strings" 7 7 8 + "tangled.org/core/appview/oauth" 8 9 "tangled.org/core/appview/pages" 9 10 ) 10 11 ··· 15 16 case http.MethodGet: 16 17 returnURL := r.URL.Query().Get("return_url") 17 18 errorCode := r.URL.Query().Get("error") 19 + addAccount := r.URL.Query().Get("mode") == "add_account" 20 + 21 + user := s.oauth.GetMultiAccountUser(r) 22 + if user == nil { 23 + registry := s.oauth.GetAccounts(r) 24 + if len(registry.Accounts) > 0 { 25 + user = &oauth.MultiAccountUser{ 26 + Active: nil, 27 + Accounts: registry.Accounts, 28 + } 29 + } 30 + } 18 31 s.pages.Login(w, pages.LoginParams{ 19 - ReturnUrl: returnURL, 20 - ErrorCode: errorCode, 32 + ReturnUrl: returnURL, 33 + ErrorCode: errorCode, 34 + AddAccount: addAccount, 35 + LoggedInUser: user, 21 36 }) 22 37 case http.MethodPost: 23 38 handle := r.FormValue("handle") 39 + returnURL := r.FormValue("return_url") 40 + addAccount := r.FormValue("add_account") == "true" 24 41 25 42 // when users copy their handle from bsky.app, it tends to have these characters around it: 26 43 // ··· 44 61 return 45 62 } 46 63 64 + if err := s.oauth.SetAuthReturn(w, r, returnURL, addAccount); err != nil { 65 + l.Error("failed to set auth return", "err", err) 66 + } 67 + 47 68 redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle) 48 69 if err != nil { 49 70 l.Error("failed to start auth", "err", err) ··· 58 79 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 59 80 l := s.logger.With("handler", "Logout") 60 81 61 - err := s.oauth.DeleteSession(w, r) 62 - if err != nil { 63 - l.Error("failed to logout", "err", err) 64 - } else { 65 - l.Info("logged out successfully") 82 + currentUser := s.oauth.GetMultiAccountUser(r) 83 + if currentUser == nil || currentUser.Active == nil { 84 + s.pages.HxRedirect(w, "/login") 85 + return 66 86 } 67 87 88 + currentDid := currentUser.Active.Did 89 + 90 + var remainingAccounts []string 91 + for _, acc := range currentUser.Accounts { 92 + if acc.Did != currentDid { 93 + remainingAccounts = append(remainingAccounts, acc.Did) 94 + } 95 + } 96 + 97 + if err := s.oauth.RemoveAccount(w, r, currentDid); err != nil { 98 + l.Error("failed to remove account from registry", "err", err) 99 + } 100 + 101 + if err := s.oauth.DeleteSession(w, r); err != nil { 102 + l.Error("failed to delete session", "err", err) 103 + } 104 + 105 + if len(remainingAccounts) > 0 { 106 + nextDid := remainingAccounts[0] 107 + if err := s.oauth.SwitchAccount(w, r, nextDid); err != nil { 108 + l.Error("failed to switch to next account", "err", err) 109 + s.pages.HxRedirect(w, "/login") 110 + return 111 + } 112 + l.Info("switched to next account after logout", "did", nextDid) 113 + s.pages.HxRefresh(w) 114 + return 115 + } 116 + 117 + l.Info("logged out last account") 68 118 s.pages.HxRedirect(w, "/login") 69 119 }
+222 -43
appview/state/profile.go
··· 77 77 return nil, fmt.Errorf("failed to get follower stats: %w", err) 78 78 } 79 79 80 - loggedInUser := s.oauth.GetUser(r) 80 + loggedInUser := s.oauth.GetMultiAccountUser(r) 81 81 followStatus := models.IsNotFollowing 82 82 if loggedInUser != nil { 83 - followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did) 83 + followStatus = db.GetFollowStatus(s.db, loggedInUser.Active.Did, did) 84 84 } 85 85 86 86 now := time.Now() ··· 162 162 l.Error("failed to create timeline", "err", err) 163 163 } 164 164 165 - // populate commit counts in the timeline, using the punchcard 166 - now := time.Now() 167 - for _, p := range profile.Punchcard.Punches { 168 - years := now.Year() - p.Date.Year() 169 - months := int(now.Month() - p.Date.Month()) 170 - monthsAgo := years*12 + months 171 - if monthsAgo >= 0 && monthsAgo < len(timeline.ByMonth) { 172 - timeline.ByMonth[monthsAgo].Commits += p.Count 173 - } 174 - } 175 - 176 165 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 177 - LoggedInUser: s.oauth.GetUser(r), 166 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 178 167 Card: profile, 179 168 Repos: pinnedRepos, 180 169 CollaboratingRepos: pinnedCollaboratingRepos, ··· 205 194 } 206 195 207 196 err = s.pages.ProfileRepos(w, pages.ProfileReposParams{ 208 - LoggedInUser: s.oauth.GetUser(r), 197 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 209 198 Repos: repos, 210 199 Card: profile, 211 200 }) ··· 234 223 } 235 224 236 225 err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{ 237 - LoggedInUser: s.oauth.GetUser(r), 226 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 238 227 Repos: repos, 239 228 Card: profile, 240 229 }) ··· 259 248 } 260 249 261 250 err = s.pages.ProfileStrings(w, pages.ProfileStringsParams{ 262 - LoggedInUser: s.oauth.GetUser(r), 251 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 263 252 Strings: strings, 264 253 Card: profile, 265 254 }) ··· 283 272 } 284 273 l = l.With("profileDid", profile.UserDid) 285 274 286 - loggedInUser := s.oauth.GetUser(r) 275 + loggedInUser := s.oauth.GetMultiAccountUser(r) 287 276 params := FollowsPageParams{ 288 277 Card: profile, 289 278 } ··· 316 305 317 306 loggedInUserFollowing := make(map[string]struct{}) 318 307 if loggedInUser != nil { 319 - following, err := db.GetFollowing(s.db, loggedInUser.Did) 308 + following, err := db.GetFollowing(s.db, loggedInUser.Active.Did) 320 309 if err != nil { 321 - l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did) 310 + l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Active.Did) 322 311 return &params, err 323 312 } 324 313 loggedInUserFollowing = make(map[string]struct{}, len(following)) ··· 333 322 followStatus := models.IsNotFollowing 334 323 if _, exists := loggedInUserFollowing[did]; exists { 335 324 followStatus = models.IsFollowing 336 - } else if loggedInUser != nil && loggedInUser.Did == did { 325 + } else if loggedInUser != nil && loggedInUser.Active.Did == did { 337 326 followStatus = models.IsSelf 338 327 } 339 328 ··· 367 356 } 368 357 369 358 s.pages.ProfileFollowers(w, pages.ProfileFollowersParams{ 370 - LoggedInUser: s.oauth.GetUser(r), 359 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 371 360 Followers: followPage.Follows, 372 361 Card: followPage.Card, 373 362 }) ··· 381 370 } 382 371 383 372 s.pages.ProfileFollowing(w, pages.ProfileFollowingParams{ 384 - LoggedInUser: s.oauth.GetUser(r), 373 + LoggedInUser: s.oauth.GetMultiAccountUser(r), 385 374 Following: followPage.Follows, 386 375 Card: followPage.Card, 387 376 }) ··· 530 519 } 531 520 532 521 func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { 533 - user := s.oauth.GetUser(r) 522 + user := s.oauth.GetMultiAccountUser(r) 534 523 535 524 err := r.ParseForm() 536 525 if err != nil { ··· 539 528 return 540 529 } 541 530 542 - profile, err := db.GetProfile(s.db, user.Did) 531 + profile, err := db.GetProfile(s.db, user.Active.Did) 543 532 if err != nil { 544 - log.Printf("getting profile data for %s: %s", user.Did, err) 533 + log.Printf("getting profile data for %s: %s", user.Active.Did, err) 545 534 } 546 535 547 536 profile.Description = r.FormValue("description") ··· 578 567 } 579 568 580 569 func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { 581 - user := s.oauth.GetUser(r) 570 + user := s.oauth.GetMultiAccountUser(r) 582 571 583 572 err := r.ParseForm() 584 573 if err != nil { ··· 587 576 return 588 577 } 589 578 590 - profile, err := db.GetProfile(s.db, user.Did) 579 + profile, err := db.GetProfile(s.db, user.Active.Did) 591 580 if err != nil { 592 - log.Printf("getting profile data for %s: %s", user.Did, err) 581 + log.Printf("getting profile data for %s: %s", user.Active.Did, err) 593 582 } 594 583 595 584 i := 0 ··· 617 606 } 618 607 619 608 func (s *State) updateProfile(profile *models.Profile, w http.ResponseWriter, r *http.Request) { 620 - user := s.oauth.GetUser(r) 609 + user := s.oauth.GetMultiAccountUser(r) 621 610 tx, err := s.db.BeginTx(r.Context(), nil) 622 611 if err != nil { 623 612 log.Println("failed to start transaction", err) ··· 644 633 vanityStats = append(vanityStats, string(v.Kind)) 645 634 } 646 635 647 - ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 636 + ex, _ := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Active.Did, "self") 648 637 var cid *string 649 638 if ex != nil { 650 639 cid = ex.Cid ··· 652 641 653 642 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 654 643 Collection: tangled.ActorProfileNSID, 655 - Repo: user.Did, 644 + Repo: user.Active.Did, 656 645 Rkey: "self", 657 646 Record: &lexutil.LexiconTypeDecoder{ 658 647 Val: &tangled.ActorProfile{ ··· 681 670 682 671 s.notifier.UpdateProfile(r.Context(), profile) 683 672 684 - s.pages.HxRedirect(w, "/"+user.Did) 673 + s.pages.HxRedirect(w, "/"+user.Active.Did) 685 674 } 686 675 687 676 func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 688 - user := s.oauth.GetUser(r) 677 + user := s.oauth.GetMultiAccountUser(r) 689 678 690 - profile, err := db.GetProfile(s.db, user.Did) 679 + profile, err := db.GetProfile(s.db, user.Active.Did) 691 680 if err != nil { 692 - log.Printf("getting profile data for %s: %s", user.Did, err) 681 + log.Printf("getting profile data for %s: %s", user.Active.Did, err) 693 682 } 694 683 695 684 s.pages.EditBioFragment(w, pages.EditBioParams{ ··· 699 688 } 700 689 701 690 func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 702 - user := s.oauth.GetUser(r) 691 + user := s.oauth.GetMultiAccountUser(r) 703 692 704 - profile, err := db.GetProfile(s.db, user.Did) 693 + profile, err := db.GetProfile(s.db, user.Active.Did) 705 694 if err != nil { 706 - log.Printf("getting profile data for %s: %s", user.Did, err) 695 + log.Printf("getting profile data for %s: %s", user.Active.Did, err) 707 696 } 708 697 709 - repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did)) 698 + repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Active.Did)) 710 699 if err != nil { 711 - log.Printf("getting repos for %s: %s", user.Did, err) 700 + log.Printf("getting repos for %s: %s", user.Active.Did, err) 712 701 } 713 702 714 - collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did) 703 + collaboratingRepos, err := db.CollaboratingIn(s.db, user.Active.Did) 715 704 if err != nil { 716 - log.Printf("getting collaborating repos for %s: %s", user.Did, err) 705 + log.Printf("getting collaborating repos for %s: %s", user.Active.Did, err) 717 706 } 718 707 719 708 allRepos := []pages.PinnedRepo{} ··· 739 728 AllRepos: allRepos, 740 729 }) 741 730 } 731 + 732 + func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { 733 + l := s.logger.With("handler", "UploadProfileAvatar") 734 + user := s.oauth.GetUser(r) 735 + l = l.With("did", user.Did) 736 + 737 + // Parse multipart form (10MB max) 738 + if err := r.ParseMultipartForm(10 << 20); err != nil { 739 + l.Error("failed to parse form", "err", err) 740 + s.pages.Notice(w, "avatar-error", "Failed to parse form") 741 + return 742 + } 743 + 744 + file, handler, err := r.FormFile("avatar") 745 + if err != nil { 746 + l.Error("failed to read avatar file", "err", err) 747 + s.pages.Notice(w, "avatar-error", "Failed to read avatar file") 748 + return 749 + } 750 + defer file.Close() 751 + 752 + if handler.Size > 1000000 { 753 + l.Warn("avatar file too large", "size", handler.Size) 754 + s.pages.Notice(w, "avatar-error", "Avatar file too large (max 1MB)") 755 + return 756 + } 757 + 758 + contentType := handler.Header.Get("Content-Type") 759 + if contentType != "image/png" && contentType != "image/jpeg" { 760 + l.Warn("invalid image type", "contentType", contentType) 761 + s.pages.Notice(w, "avatar-error", "Invalid image type (only PNG and JPEG allowed)") 762 + return 763 + } 764 + 765 + client, err := s.oauth.AuthorizedClient(r) 766 + if err != nil { 767 + l.Error("failed to get PDS client", "err", err) 768 + s.pages.Notice(w, "avatar-error", "Failed to connect to your PDS") 769 + return 770 + } 771 + 772 + uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) 773 + if err != nil { 774 + l.Error("failed to upload avatar blob", "err", err) 775 + s.pages.Notice(w, "avatar-error", "Failed to upload avatar to your PDS") 776 + return 777 + } 778 + 779 + l.Info("uploaded avatar blob", "cid", uploadBlobResp.Blob.Ref.String()) 780 + 781 + // get current profile record from PDS to get its CID for swap 782 + getRecordResp, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 783 + if err != nil { 784 + l.Error("failed to get current profile record", "err", err) 785 + s.pages.Notice(w, "avatar-error", "Failed to get current profile from your PDS") 786 + return 787 + } 788 + 789 + var profileRecord *tangled.ActorProfile 790 + if getRecordResp.Value != nil { 791 + if val, ok := getRecordResp.Value.Val.(*tangled.ActorProfile); ok { 792 + profileRecord = val 793 + } else { 794 + l.Warn("profile record type assertion failed, creating new record") 795 + profileRecord = &tangled.ActorProfile{} 796 + } 797 + } else { 798 + l.Warn("no existing profile record, creating new record") 799 + profileRecord = &tangled.ActorProfile{} 800 + } 801 + 802 + profileRecord.Avatar = uploadBlobResp.Blob 803 + 804 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 805 + Collection: tangled.ActorProfileNSID, 806 + Repo: user.Did, 807 + Rkey: "self", 808 + Record: &lexutil.LexiconTypeDecoder{Val: profileRecord}, 809 + SwapRecord: getRecordResp.Cid, 810 + }) 811 + 812 + if err != nil { 813 + l.Error("failed to update profile record", "err", err) 814 + s.pages.Notice(w, "avatar-error", "Failed to update profile on your PDS") 815 + return 816 + } 817 + 818 + l.Info("successfully updated profile with avatar") 819 + 820 + profile, err := db.GetProfile(s.db, user.Did) 821 + if err != nil { 822 + l.Warn("getting profile data from DB", "err", err) 823 + profile = &models.Profile{Did: user.Did} 824 + } 825 + profile.Avatar = uploadBlobResp.Blob.Ref.String() 826 + 827 + tx, err := s.db.BeginTx(r.Context(), nil) 828 + if err != nil { 829 + l.Error("failed to start transaction", "err", err) 830 + s.pages.HxRefresh(w) 831 + w.WriteHeader(http.StatusOK) 832 + return 833 + } 834 + 835 + err = db.UpsertProfile(tx, profile) 836 + if err != nil { 837 + l.Error("failed to update profile in DB", "err", err) 838 + s.pages.HxRefresh(w) 839 + w.WriteHeader(http.StatusOK) 840 + return 841 + } 842 + 843 + s.pages.HxRedirect(w, r.Header.Get("Referer")) 844 + } 845 + 846 + func (s *State) RemoveProfileAvatar(w http.ResponseWriter, r *http.Request) { 847 + l := s.logger.With("handler", "RemoveProfileAvatar") 848 + user := s.oauth.GetUser(r) 849 + l = l.With("did", user.Did) 850 + 851 + client, err := s.oauth.AuthorizedClient(r) 852 + if err != nil { 853 + l.Error("failed to get PDS client", "err", err) 854 + s.pages.Notice(w, "avatar-error", "Failed to connect to your PDS") 855 + return 856 + } 857 + 858 + getRecordResp, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 859 + if err != nil { 860 + l.Error("failed to get current profile record", "err", err) 861 + s.pages.Notice(w, "avatar-error", "Failed to get current profile from your PDS") 862 + return 863 + } 864 + 865 + var profileRecord *tangled.ActorProfile 866 + if getRecordResp.Value != nil { 867 + if val, ok := getRecordResp.Value.Val.(*tangled.ActorProfile); ok { 868 + profileRecord = val 869 + } else { 870 + l.Warn("profile record type assertion failed") 871 + profileRecord = &tangled.ActorProfile{} 872 + } 873 + } else { 874 + l.Warn("no existing profile record") 875 + profileRecord = &tangled.ActorProfile{} 876 + } 877 + 878 + profileRecord.Avatar = nil 879 + 880 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 881 + Collection: tangled.ActorProfileNSID, 882 + Repo: user.Did, 883 + Rkey: "self", 884 + Record: &lexutil.LexiconTypeDecoder{Val: profileRecord}, 885 + SwapRecord: getRecordResp.Cid, 886 + }) 887 + 888 + if err != nil { 889 + l.Error("failed to update profile record", "err", err) 890 + s.pages.Notice(w, "avatar-error", "Failed to remove avatar from your PDS") 891 + return 892 + } 893 + 894 + l.Info("successfully removed avatar from PDS") 895 + 896 + profile, err := db.GetProfile(s.db, user.Did) 897 + if err != nil { 898 + l.Warn("getting profile data from DB", "err", err) 899 + profile = &models.Profile{Did: user.Did} 900 + } 901 + profile.Avatar = "" 902 + 903 + tx, err := s.db.BeginTx(r.Context(), nil) 904 + if err != nil { 905 + l.Error("failed to start transaction", "err", err) 906 + s.pages.HxRefresh(w) 907 + w.WriteHeader(http.StatusOK) 908 + return 909 + } 910 + 911 + err = db.UpsertProfile(tx, profile) 912 + if err != nil { 913 + l.Error("failed to update profile in DB", "err", err) 914 + s.pages.HxRefresh(w) 915 + w.WriteHeader(http.StatusOK) 916 + return 917 + } 918 + 919 + s.pages.HxRedirect(w, r.Header.Get("Referer")) 920 + }
+7 -7
appview/state/reaction.go
··· 17 17 ) 18 18 19 19 func (s *State) React(w http.ResponseWriter, r *http.Request) { 20 - currentUser := s.oauth.GetUser(r) 20 + currentUser := s.oauth.GetMultiAccountUser(r) 21 21 22 22 subject := r.URL.Query().Get("subject") 23 23 if subject == "" { ··· 49 49 rkey := tid.TID() 50 50 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 51 51 Collection: tangled.FeedReactionNSID, 52 - Repo: currentUser.Did, 52 + Repo: currentUser.Active.Did, 53 53 Rkey: rkey, 54 54 Record: &lexutil.LexiconTypeDecoder{ 55 55 Val: &tangled.FeedReaction{ ··· 64 64 return 65 65 } 66 66 67 - err = db.AddReaction(s.db, currentUser.Did, subjectUri, reactionKind, rkey) 67 + err = db.AddReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind, rkey) 68 68 if err != nil { 69 69 log.Println("failed to react", err) 70 70 return ··· 87 87 88 88 return 89 89 case http.MethodDelete: 90 - reaction, err := db.GetReaction(s.db, currentUser.Did, subjectUri, reactionKind) 90 + reaction, err := db.GetReaction(s.db, currentUser.Active.Did, subjectUri, reactionKind) 91 91 if err != nil { 92 - log.Println("failed to get reaction relationship for", currentUser.Did, subjectUri) 92 + log.Println("failed to get reaction relationship for", currentUser.Active.Did, subjectUri) 93 93 return 94 94 } 95 95 96 96 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 97 97 Collection: tangled.FeedReactionNSID, 98 - Repo: currentUser.Did, 98 + Repo: currentUser.Active.Did, 99 99 Rkey: reaction.Rkey, 100 100 }) 101 101 ··· 104 104 return 105 105 } 106 106 107 - err = db.DeleteReactionByRkey(s.db, currentUser.Did, reaction.Rkey) 107 + err = db.DeleteReactionByRkey(s.db, currentUser.Active.Did, reaction.Rkey) 108 108 if err != nil { 109 109 log.Println("failed to delete reaction from DB") 110 110 // this is not an issue, the firehose event might have already done this
+5
appview/state/router.go
··· 130 130 r.Post("/login", s.Login) 131 131 r.Post("/logout", s.Logout) 132 132 133 + r.Post("/account/switch", s.SwitchAccount) 134 + r.With(middleware.AuthMiddleware(s.oauth)).Delete("/account/{did}", s.RemoveAccount) 135 + 133 136 r.Route("/repo", func(r chi.Router) { 134 137 r.Route("/new", func(r chi.Router) { 135 138 r.Use(middleware.AuthMiddleware(s.oauth)) ··· 162 165 r.Get("/edit-pins", s.EditPinsFragment) 163 166 r.Post("/bio", s.UpdateProfileBio) 164 167 r.Post("/pins", s.UpdateProfilePins) 168 + r.Post("/avatar", s.UploadProfileAvatar) 169 + r.Delete("/avatar", s.RemoveProfileAvatar) 165 170 }) 166 171 167 172 r.Mount("/settings", s.SettingsRouter())
+6 -6
appview/state/star.go
··· 16 16 ) 17 17 18 18 func (s *State) Star(w http.ResponseWriter, r *http.Request) { 19 - currentUser := s.oauth.GetUser(r) 19 + currentUser := s.oauth.GetMultiAccountUser(r) 20 20 21 21 subject := r.URL.Query().Get("subject") 22 22 if subject == "" { ··· 42 42 rkey := tid.TID() 43 43 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 44 Collection: tangled.FeedStarNSID, 45 - Repo: currentUser.Did, 45 + Repo: currentUser.Active.Did, 46 46 Rkey: rkey, 47 47 Record: &lexutil.LexiconTypeDecoder{ 48 48 Val: &tangled.FeedStar{ ··· 57 57 log.Println("created atproto record: ", resp.Uri) 58 58 59 59 star := &models.Star{ 60 - Did: currentUser.Did, 60 + Did: currentUser.Active.Did, 61 61 RepoAt: subjectUri, 62 62 Rkey: rkey, 63 63 } ··· 84 84 return 85 85 case http.MethodDelete: 86 86 // find the record in the db 87 - star, err := db.GetStar(s.db, currentUser.Did, subjectUri) 87 + star, err := db.GetStar(s.db, currentUser.Active.Did, subjectUri) 88 88 if err != nil { 89 89 log.Println("failed to get star relationship") 90 90 return ··· 92 92 93 93 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 94 94 Collection: tangled.FeedStarNSID, 95 - Repo: currentUser.Did, 95 + Repo: currentUser.Active.Did, 96 96 Rkey: star.Rkey, 97 97 }) 98 98 ··· 101 101 return 102 102 } 103 103 104 - err = db.DeleteStarByRkey(s.db, currentUser.Did, star.Rkey) 104 + err = db.DeleteStarByRkey(s.db, currentUser.Active.Did, star.Rkey) 105 105 if err != nil { 106 106 log.Println("failed to delete star from DB") 107 107 // this is not an issue, the firehose event might have already done this
+23 -23
appview/state/state.go
··· 92 92 return nil, fmt.Errorf("failed to create posthog client: %w", err) 93 93 } 94 94 95 - pages := pages.NewPages(config, res, log.SubLogger(logger, "pages")) 95 + pages := pages.NewPages(config, res, d, log.SubLogger(logger, "pages")) 96 96 oauth, err := oauth.New(config, posthog, d, enforcer, res, log.SubLogger(logger, "oauth")) 97 97 if err != nil { 98 98 return nil, fmt.Errorf("failed to start oauth handler: %w", err) ··· 213 213 } 214 214 215 215 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) { 216 - user := s.oauth.GetUser(r) 216 + user := s.oauth.GetMultiAccountUser(r) 217 217 s.pages.TermsOfService(w, pages.TermsOfServiceParams{ 218 218 LoggedInUser: user, 219 219 }) 220 220 } 221 221 222 222 func (s *State) PrivacyPolicy(w http.ResponseWriter, r *http.Request) { 223 - user := s.oauth.GetUser(r) 223 + user := s.oauth.GetMultiAccountUser(r) 224 224 s.pages.PrivacyPolicy(w, pages.PrivacyPolicyParams{ 225 225 LoggedInUser: user, 226 226 }) 227 227 } 228 228 229 229 func (s *State) Brand(w http.ResponseWriter, r *http.Request) { 230 - user := s.oauth.GetUser(r) 230 + user := s.oauth.GetMultiAccountUser(r) 231 231 s.pages.Brand(w, pages.BrandParams{ 232 232 LoggedInUser: user, 233 233 }) 234 234 } 235 235 236 236 func (s *State) HomeOrTimeline(w http.ResponseWriter, r *http.Request) { 237 - if s.oauth.GetUser(r) != nil { 237 + if s.oauth.GetMultiAccountUser(r) != nil { 238 238 s.Timeline(w, r) 239 239 return 240 240 } ··· 242 242 } 243 243 244 244 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 245 - user := s.oauth.GetUser(r) 245 + user := s.oauth.GetMultiAccountUser(r) 246 246 247 247 // TODO: set this flag based on the UI 248 248 filtered := false 249 249 250 250 var userDid string 251 - if user != nil { 252 - userDid = user.Did 251 + if user != nil && user.Active != nil { 252 + userDid = user.Active.Did 253 253 } 254 254 timeline, err := db.MakeTimeline(s.db, 50, userDid, filtered) 255 255 if err != nil { ··· 278 278 } 279 279 280 280 func (s *State) UpgradeBanner(w http.ResponseWriter, r *http.Request) { 281 - user := s.oauth.GetUser(r) 281 + user := s.oauth.GetMultiAccountUser(r) 282 282 if user == nil { 283 283 return 284 284 } 285 285 286 286 l := s.logger.With("handler", "UpgradeBanner") 287 - l = l.With("did", user.Did) 287 + l = l.With("did", user.Active.Did) 288 288 289 289 regs, err := db.GetRegistrations( 290 290 s.db, 291 - orm.FilterEq("did", user.Did), 291 + orm.FilterEq("did", user.Active.Did), 292 292 orm.FilterEq("needs_upgrade", 1), 293 293 ) 294 294 if err != nil { ··· 297 297 298 298 spindles, err := db.GetSpindles( 299 299 s.db, 300 - orm.FilterEq("owner", user.Did), 300 + orm.FilterEq("owner", user.Active.Did), 301 301 orm.FilterEq("needs_upgrade", 1), 302 302 ) 303 303 if err != nil { ··· 411 411 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 412 412 switch r.Method { 413 413 case http.MethodGet: 414 - user := s.oauth.GetUser(r) 415 - knots, err := s.enforcer.GetKnotsForUser(user.Did) 414 + user := s.oauth.GetMultiAccountUser(r) 415 + knots, err := s.enforcer.GetKnotsForUser(user.Active.Did) 416 416 if err != nil { 417 417 s.pages.Notice(w, "repo", "Invalid user account.") 418 418 return ··· 426 426 case http.MethodPost: 427 427 l := s.logger.With("handler", "NewRepo") 428 428 429 - user := s.oauth.GetUser(r) 430 - l = l.With("did", user.Did) 429 + user := s.oauth.GetMultiAccountUser(r) 430 + l = l.With("did", user.Active.Did) 431 431 432 432 // form validation 433 433 domain := r.FormValue("domain") ··· 459 459 description := r.FormValue("description") 460 460 461 461 // ACL validation 462 - ok, err := s.enforcer.E.Enforce(user.Did, domain, domain, "repo:create") 462 + ok, err := s.enforcer.E.Enforce(user.Active.Did, domain, domain, "repo:create") 463 463 if err != nil || !ok { 464 464 l.Info("unauthorized") 465 465 s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") ··· 469 469 // Check for existing repos 470 470 existingRepo, err := db.GetRepo( 471 471 s.db, 472 - orm.FilterEq("did", user.Did), 472 + orm.FilterEq("did", user.Active.Did), 473 473 orm.FilterEq("name", repoName), 474 474 ) 475 475 if err == nil && existingRepo != nil { ··· 481 481 // create atproto record for this repo 482 482 rkey := tid.TID() 483 483 repo := &models.Repo{ 484 - Did: user.Did, 484 + Did: user.Active.Did, 485 485 Name: repoName, 486 486 Knot: domain, 487 487 Rkey: rkey, ··· 500 500 501 501 atresp, err := comatproto.RepoPutRecord(r.Context(), atpClient, &comatproto.RepoPutRecord_Input{ 502 502 Collection: tangled.RepoNSID, 503 - Repo: user.Did, 503 + Repo: user.Active.Did, 504 504 Rkey: rkey, 505 505 Record: &lexutil.LexiconTypeDecoder{ 506 506 Val: &record, ··· 577 577 } 578 578 579 579 // acls 580 - p, _ := securejoin.SecureJoin(user.Did, repoName) 581 - err = s.enforcer.AddRepo(user.Did, domain, p) 580 + p, _ := securejoin.SecureJoin(user.Active.Did, repoName) 581 + err = s.enforcer.AddRepo(user.Active.Did, domain, p) 582 582 if err != nil { 583 583 l.Error("acl setup failed", "err", err) 584 584 s.pages.Notice(w, "repo", "Failed to set up repository permissions.") ··· 603 603 aturi = "" 604 604 605 605 s.notifier.NewRepo(r.Context(), repo) 606 - s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Did, repoName)) 606 + s.pages.HxLocation(w, fmt.Sprintf("/%s/%s", user.Active.Did, repoName)) 607 607 } 608 608 } 609 609
+19 -19
appview/strings/strings.go
··· 82 82 } 83 83 84 84 s.Pages.StringsTimeline(w, pages.StringTimelineParams{ 85 - LoggedInUser: s.OAuth.GetUser(r), 85 + LoggedInUser: s.OAuth.GetMultiAccountUser(r), 86 86 Strings: strings, 87 87 }) 88 88 } ··· 153 153 if err != nil { 154 154 l.Error("failed to get star count", "err", err) 155 155 } 156 - user := s.OAuth.GetUser(r) 156 + user := s.OAuth.GetMultiAccountUser(r) 157 157 isStarred := false 158 158 if user != nil { 159 - isStarred = db.GetStarStatus(s.Db, user.Did, string.AtUri()) 159 + isStarred = db.GetStarStatus(s.Db, user.Active.Did, string.AtUri()) 160 160 } 161 161 162 162 s.Pages.SingleString(w, pages.SingleStringParams{ ··· 178 178 func (s *Strings) edit(w http.ResponseWriter, r *http.Request) { 179 179 l := s.Logger.With("handler", "edit") 180 180 181 - user := s.OAuth.GetUser(r) 181 + user := s.OAuth.GetMultiAccountUser(r) 182 182 183 183 id, ok := r.Context().Value("resolvedId").(identity.Identity) 184 184 if !ok { ··· 216 216 first := all[0] 217 217 218 218 // verify that the logged in user owns this string 219 - if user.Did != id.DID.String() { 220 - l.Error("unauthorized request", "expected", id.DID, "got", user.Did) 219 + if user.Active.Did != id.DID.String() { 220 + l.Error("unauthorized request", "expected", id.DID, "got", user.Active.Did) 221 221 w.WriteHeader(http.StatusUnauthorized) 222 222 return 223 223 } ··· 226 226 case http.MethodGet: 227 227 // return the form with prefilled fields 228 228 s.Pages.PutString(w, pages.PutStringParams{ 229 - LoggedInUser: s.OAuth.GetUser(r), 229 + LoggedInUser: s.OAuth.GetMultiAccountUser(r), 230 230 Action: "edit", 231 231 String: first, 232 232 }) ··· 299 299 s.Notifier.EditString(r.Context(), &entry) 300 300 301 301 // if that went okay, redir to the string 302 - s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+entry.Rkey) 302 + s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+entry.Rkey) 303 303 } 304 304 305 305 } 306 306 307 307 func (s *Strings) create(w http.ResponseWriter, r *http.Request) { 308 308 l := s.Logger.With("handler", "create") 309 - user := s.OAuth.GetUser(r) 309 + user := s.OAuth.GetMultiAccountUser(r) 310 310 311 311 switch r.Method { 312 312 case http.MethodGet: 313 313 s.Pages.PutString(w, pages.PutStringParams{ 314 - LoggedInUser: s.OAuth.GetUser(r), 314 + LoggedInUser: s.OAuth.GetMultiAccountUser(r), 315 315 Action: "new", 316 316 }) 317 317 case http.MethodPost: ··· 335 335 description := r.FormValue("description") 336 336 337 337 string := models.String{ 338 - Did: syntax.DID(user.Did), 338 + Did: syntax.DID(user.Active.Did), 339 339 Rkey: tid.TID(), 340 340 Filename: filename, 341 341 Description: description, ··· 353 353 354 354 resp, err := comatproto.RepoPutRecord(r.Context(), client, &atproto.RepoPutRecord_Input{ 355 355 Collection: tangled.StringNSID, 356 - Repo: user.Did, 356 + Repo: user.Active.Did, 357 357 Rkey: string.Rkey, 358 358 Record: &lexutil.LexiconTypeDecoder{ 359 359 Val: &record, ··· 375 375 s.Notifier.NewString(r.Context(), &string) 376 376 377 377 // successful 378 - s.Pages.HxRedirect(w, "/strings/"+user.Did+"/"+string.Rkey) 378 + s.Pages.HxRedirect(w, "/strings/"+user.Active.Did+"/"+string.Rkey) 379 379 } 380 380 } 381 381 382 382 func (s *Strings) delete(w http.ResponseWriter, r *http.Request) { 383 383 l := s.Logger.With("handler", "create") 384 - user := s.OAuth.GetUser(r) 384 + user := s.OAuth.GetMultiAccountUser(r) 385 385 fail := func(msg string, err error) { 386 386 l.Error(msg, "err", err) 387 387 s.Pages.Notice(w, "error", msg) ··· 402 402 return 403 403 } 404 404 405 - if user.Did != id.DID.String() { 406 - fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Did, id.DID.String())) 405 + if user.Active.Did != id.DID.String() { 406 + fail("You cannot delete this string", fmt.Errorf("unauthorized deletion, %s != %s", user.Active.Did, id.DID.String())) 407 407 return 408 408 } 409 409 410 410 if err := db.DeleteString( 411 411 s.Db, 412 - orm.FilterEq("did", user.Did), 412 + orm.FilterEq("did", user.Active.Did), 413 413 orm.FilterEq("rkey", rkey), 414 414 ); err != nil { 415 415 fail("Failed to delete string.", err) 416 416 return 417 417 } 418 418 419 - s.Notifier.DeleteString(r.Context(), user.Did, rkey) 419 + s.Notifier.DeleteString(r.Context(), user.Active.Did, rkey) 420 420 421 - s.Pages.HxRedirect(w, "/strings/"+user.Did) 421 + s.Pages.HxRedirect(w, "/strings/"+user.Active.Did) 422 422 } 423 423 424 424 func (s *Strings) comment(w http.ResponseWriter, r *http.Request) {
+3113 -3022
avatar/package-lock.json
··· 1 1 { 2 - "name": "avatar", 3 - "version": "0.0.0", 4 - "lockfileVersion": 3, 5 - "requires": true, 6 - "packages": { 7 - "": { 8 - "name": "avatar", 9 - "version": "0.0.0", 10 - "devDependencies": { 11 - "@cloudflare/vitest-pool-workers": "^0.8.19", 12 - "vitest": "~3.0.7", 13 - "wrangler": "^4.14.1" 14 - } 15 - }, 16 - "node_modules/@cloudflare/kv-asset-handler": { 17 - "version": "0.4.0", 18 - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", 19 - "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", 20 - "dev": true, 21 - "license": "MIT OR Apache-2.0", 22 - "dependencies": { 23 - "mime": "^3.0.0" 24 - }, 25 - "engines": { 26 - "node": ">=18.0.0" 27 - } 28 - }, 29 - "node_modules/@cloudflare/unenv-preset": { 30 - "version": "2.3.1", 31 - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz", 32 - "integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==", 33 - "dev": true, 34 - "license": "MIT OR Apache-2.0", 35 - "peerDependencies": { 36 - "unenv": "2.0.0-rc.15", 37 - "workerd": "^1.20250320.0" 38 - }, 39 - "peerDependenciesMeta": { 40 - "workerd": { 41 - "optional": true 42 - } 43 - } 44 - }, 45 - "node_modules/@cloudflare/vitest-pool-workers": { 46 - "version": "0.8.24", 47 - "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.24.tgz", 48 - "integrity": "sha512-wT2PABJQ9YLYWrVu4CRZOjvmjHkdbMyLTZPU9n/7JEMM3pgG8dY41F1Rj31UsXRQaXX39A/CTPGlk58dcMUysA==", 49 - "dev": true, 50 - "license": "MIT", 51 - "dependencies": { 52 - "birpc": "0.2.14", 53 - "cjs-module-lexer": "^1.2.3", 54 - "devalue": "^4.3.0", 55 - "miniflare": "4.20250428.1", 56 - "semver": "^7.7.1", 57 - "wrangler": "4.14.1", 58 - "zod": "^3.22.3" 59 - }, 60 - "peerDependencies": { 61 - "@vitest/runner": "2.0.x - 3.1.x", 62 - "@vitest/snapshot": "2.0.x - 3.1.x", 63 - "vitest": "2.0.x - 3.1.x" 64 - } 65 - }, 66 - "node_modules/@cloudflare/workerd-darwin-64": { 67 - "version": "1.20250428.0", 68 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250428.0.tgz", 69 - "integrity": "sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==", 70 - "cpu": [ 71 - "x64" 72 - ], 73 - "dev": true, 74 - "license": "Apache-2.0", 75 - "optional": true, 76 - "os": [ 77 - "darwin" 78 - ], 79 - "engines": { 80 - "node": ">=16" 81 - } 82 - }, 83 - "node_modules/@cloudflare/workerd-darwin-arm64": { 84 - "version": "1.20250428.0", 85 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250428.0.tgz", 86 - "integrity": "sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==", 87 - "cpu": [ 88 - "arm64" 89 - ], 90 - "dev": true, 91 - "license": "Apache-2.0", 92 - "optional": true, 93 - "os": [ 94 - "darwin" 95 - ], 96 - "engines": { 97 - "node": ">=16" 98 - } 99 - }, 100 - "node_modules/@cloudflare/workerd-linux-64": { 101 - "version": "1.20250428.0", 102 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250428.0.tgz", 103 - "integrity": "sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==", 104 - "cpu": [ 105 - "x64" 106 - ], 107 - "dev": true, 108 - "license": "Apache-2.0", 109 - "optional": true, 110 - "os": [ 111 - "linux" 112 - ], 113 - "engines": { 114 - "node": ">=16" 115 - } 116 - }, 117 - "node_modules/@cloudflare/workerd-linux-arm64": { 118 - "version": "1.20250428.0", 119 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250428.0.tgz", 120 - "integrity": "sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==", 121 - "cpu": [ 122 - "arm64" 123 - ], 124 - "dev": true, 125 - "license": "Apache-2.0", 126 - "optional": true, 127 - "os": [ 128 - "linux" 129 - ], 130 - "engines": { 131 - "node": ">=16" 132 - } 133 - }, 134 - "node_modules/@cloudflare/workerd-windows-64": { 135 - "version": "1.20250428.0", 136 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250428.0.tgz", 137 - "integrity": "sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==", 138 - "cpu": [ 139 - "x64" 140 - ], 141 - "dev": true, 142 - "license": "Apache-2.0", 143 - "optional": true, 144 - "os": [ 145 - "win32" 146 - ], 147 - "engines": { 148 - "node": ">=16" 149 - } 150 - }, 151 - "node_modules/@cspotcode/source-map-support": { 152 - "version": "0.8.1", 153 - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 154 - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 155 - "dev": true, 156 - "license": "MIT", 157 - "dependencies": { 158 - "@jridgewell/trace-mapping": "0.3.9" 159 - }, 160 - "engines": { 161 - "node": ">=12" 162 - } 163 - }, 164 - "node_modules/@emnapi/runtime": { 165 - "version": "1.4.3", 166 - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", 167 - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", 168 - "dev": true, 169 - "license": "MIT", 170 - "optional": true, 171 - "dependencies": { 172 - "tslib": "^2.4.0" 173 - } 174 - }, 175 - "node_modules/@esbuild/aix-ppc64": { 176 - "version": "0.25.3", 177 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", 178 - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", 179 - "cpu": [ 180 - "ppc64" 181 - ], 182 - "dev": true, 183 - "license": "MIT", 184 - "optional": true, 185 - "os": [ 186 - "aix" 187 - ], 188 - "engines": { 189 - "node": ">=18" 190 - } 191 - }, 192 - "node_modules/@esbuild/android-arm": { 193 - "version": "0.25.3", 194 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", 195 - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", 196 - "cpu": [ 197 - "arm" 198 - ], 199 - "dev": true, 200 - "license": "MIT", 201 - "optional": true, 202 - "os": [ 203 - "android" 204 - ], 205 - "engines": { 206 - "node": ">=18" 207 - } 208 - }, 209 - "node_modules/@esbuild/android-arm64": { 210 - "version": "0.25.3", 211 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", 212 - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", 213 - "cpu": [ 214 - "arm64" 215 - ], 216 - "dev": true, 217 - "license": "MIT", 218 - "optional": true, 219 - "os": [ 220 - "android" 221 - ], 222 - "engines": { 223 - "node": ">=18" 224 - } 225 - }, 226 - "node_modules/@esbuild/android-x64": { 227 - "version": "0.25.3", 228 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", 229 - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", 230 - "cpu": [ 231 - "x64" 232 - ], 233 - "dev": true, 234 - "license": "MIT", 235 - "optional": true, 236 - "os": [ 237 - "android" 238 - ], 239 - "engines": { 240 - "node": ">=18" 241 - } 242 - }, 243 - "node_modules/@esbuild/darwin-arm64": { 244 - "version": "0.25.3", 245 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", 246 - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", 247 - "cpu": [ 248 - "arm64" 249 - ], 250 - "dev": true, 251 - "license": "MIT", 252 - "optional": true, 253 - "os": [ 254 - "darwin" 255 - ], 256 - "engines": { 257 - "node": ">=18" 258 - } 259 - }, 260 - "node_modules/@esbuild/darwin-x64": { 261 - "version": "0.25.3", 262 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", 263 - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", 264 - "cpu": [ 265 - "x64" 266 - ], 267 - "dev": true, 268 - "license": "MIT", 269 - "optional": true, 270 - "os": [ 271 - "darwin" 272 - ], 273 - "engines": { 274 - "node": ">=18" 275 - } 276 - }, 277 - "node_modules/@esbuild/freebsd-arm64": { 278 - "version": "0.25.3", 279 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", 280 - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", 281 - "cpu": [ 282 - "arm64" 283 - ], 284 - "dev": true, 285 - "license": "MIT", 286 - "optional": true, 287 - "os": [ 288 - "freebsd" 289 - ], 290 - "engines": { 291 - "node": ">=18" 292 - } 293 - }, 294 - "node_modules/@esbuild/freebsd-x64": { 295 - "version": "0.25.3", 296 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", 297 - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", 298 - "cpu": [ 299 - "x64" 300 - ], 301 - "dev": true, 302 - "license": "MIT", 303 - "optional": true, 304 - "os": [ 305 - "freebsd" 306 - ], 307 - "engines": { 308 - "node": ">=18" 309 - } 310 - }, 311 - "node_modules/@esbuild/linux-arm": { 312 - "version": "0.25.3", 313 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", 314 - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", 315 - "cpu": [ 316 - "arm" 317 - ], 318 - "dev": true, 319 - "license": "MIT", 320 - "optional": true, 321 - "os": [ 322 - "linux" 323 - ], 324 - "engines": { 325 - "node": ">=18" 326 - } 327 - }, 328 - "node_modules/@esbuild/linux-arm64": { 329 - "version": "0.25.3", 330 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", 331 - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", 332 - "cpu": [ 333 - "arm64" 334 - ], 335 - "dev": true, 336 - "license": "MIT", 337 - "optional": true, 338 - "os": [ 339 - "linux" 340 - ], 341 - "engines": { 342 - "node": ">=18" 343 - } 344 - }, 345 - "node_modules/@esbuild/linux-ia32": { 346 - "version": "0.25.3", 347 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", 348 - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", 349 - "cpu": [ 350 - "ia32" 351 - ], 352 - "dev": true, 353 - "license": "MIT", 354 - "optional": true, 355 - "os": [ 356 - "linux" 357 - ], 358 - "engines": { 359 - "node": ">=18" 360 - } 361 - }, 362 - "node_modules/@esbuild/linux-loong64": { 363 - "version": "0.25.3", 364 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", 365 - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", 366 - "cpu": [ 367 - "loong64" 368 - ], 369 - "dev": true, 370 - "license": "MIT", 371 - "optional": true, 372 - "os": [ 373 - "linux" 374 - ], 375 - "engines": { 376 - "node": ">=18" 377 - } 378 - }, 379 - "node_modules/@esbuild/linux-mips64el": { 380 - "version": "0.25.3", 381 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", 382 - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", 383 - "cpu": [ 384 - "mips64el" 385 - ], 386 - "dev": true, 387 - "license": "MIT", 388 - "optional": true, 389 - "os": [ 390 - "linux" 391 - ], 392 - "engines": { 393 - "node": ">=18" 394 - } 395 - }, 396 - "node_modules/@esbuild/linux-ppc64": { 397 - "version": "0.25.3", 398 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", 399 - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", 400 - "cpu": [ 401 - "ppc64" 402 - ], 403 - "dev": true, 404 - "license": "MIT", 405 - "optional": true, 406 - "os": [ 407 - "linux" 408 - ], 409 - "engines": { 410 - "node": ">=18" 411 - } 412 - }, 413 - "node_modules/@esbuild/linux-riscv64": { 414 - "version": "0.25.3", 415 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", 416 - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", 417 - "cpu": [ 418 - "riscv64" 419 - ], 420 - "dev": true, 421 - "license": "MIT", 422 - "optional": true, 423 - "os": [ 424 - "linux" 425 - ], 426 - "engines": { 427 - "node": ">=18" 428 - } 429 - }, 430 - "node_modules/@esbuild/linux-s390x": { 431 - "version": "0.25.3", 432 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", 433 - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", 434 - "cpu": [ 435 - "s390x" 436 - ], 437 - "dev": true, 438 - "license": "MIT", 439 - "optional": true, 440 - "os": [ 441 - "linux" 442 - ], 443 - "engines": { 444 - "node": ">=18" 445 - } 446 - }, 447 - "node_modules/@esbuild/linux-x64": { 448 - "version": "0.25.3", 449 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", 450 - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", 451 - "cpu": [ 452 - "x64" 453 - ], 454 - "dev": true, 455 - "license": "MIT", 456 - "optional": true, 457 - "os": [ 458 - "linux" 459 - ], 460 - "engines": { 461 - "node": ">=18" 462 - } 463 - }, 464 - "node_modules/@esbuild/netbsd-arm64": { 465 - "version": "0.25.3", 466 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", 467 - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", 468 - "cpu": [ 469 - "arm64" 470 - ], 471 - "dev": true, 472 - "license": "MIT", 473 - "optional": true, 474 - "os": [ 475 - "netbsd" 476 - ], 477 - "engines": { 478 - "node": ">=18" 479 - } 480 - }, 481 - "node_modules/@esbuild/netbsd-x64": { 482 - "version": "0.25.3", 483 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", 484 - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", 485 - "cpu": [ 486 - "x64" 487 - ], 488 - "dev": true, 489 - "license": "MIT", 490 - "optional": true, 491 - "os": [ 492 - "netbsd" 493 - ], 494 - "engines": { 495 - "node": ">=18" 496 - } 497 - }, 498 - "node_modules/@esbuild/openbsd-arm64": { 499 - "version": "0.25.3", 500 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", 501 - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", 502 - "cpu": [ 503 - "arm64" 504 - ], 505 - "dev": true, 506 - "license": "MIT", 507 - "optional": true, 508 - "os": [ 509 - "openbsd" 510 - ], 511 - "engines": { 512 - "node": ">=18" 513 - } 514 - }, 515 - "node_modules/@esbuild/openbsd-x64": { 516 - "version": "0.25.3", 517 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", 518 - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", 519 - "cpu": [ 520 - "x64" 521 - ], 522 - "dev": true, 523 - "license": "MIT", 524 - "optional": true, 525 - "os": [ 526 - "openbsd" 527 - ], 528 - "engines": { 529 - "node": ">=18" 530 - } 531 - }, 532 - "node_modules/@esbuild/sunos-x64": { 533 - "version": "0.25.3", 534 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", 535 - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", 536 - "cpu": [ 537 - "x64" 538 - ], 539 - "dev": true, 540 - "license": "MIT", 541 - "optional": true, 542 - "os": [ 543 - "sunos" 544 - ], 545 - "engines": { 546 - "node": ">=18" 547 - } 548 - }, 549 - "node_modules/@esbuild/win32-arm64": { 550 - "version": "0.25.3", 551 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", 552 - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", 553 - "cpu": [ 554 - "arm64" 555 - ], 556 - "dev": true, 557 - "license": "MIT", 558 - "optional": true, 559 - "os": [ 560 - "win32" 561 - ], 562 - "engines": { 563 - "node": ">=18" 564 - } 565 - }, 566 - "node_modules/@esbuild/win32-ia32": { 567 - "version": "0.25.3", 568 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", 569 - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", 570 - "cpu": [ 571 - "ia32" 572 - ], 573 - "dev": true, 574 - "license": "MIT", 575 - "optional": true, 576 - "os": [ 577 - "win32" 578 - ], 579 - "engines": { 580 - "node": ">=18" 581 - } 582 - }, 583 - "node_modules/@esbuild/win32-x64": { 584 - "version": "0.25.3", 585 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", 586 - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", 587 - "cpu": [ 588 - "x64" 589 - ], 590 - "dev": true, 591 - "license": "MIT", 592 - "optional": true, 593 - "os": [ 594 - "win32" 595 - ], 596 - "engines": { 597 - "node": ">=18" 598 - } 599 - }, 600 - "node_modules/@fastify/busboy": { 601 - "version": "2.1.1", 602 - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 603 - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 604 - "dev": true, 605 - "license": "MIT", 606 - "engines": { 607 - "node": ">=14" 608 - } 609 - }, 610 - "node_modules/@img/sharp-darwin-arm64": { 611 - "version": "0.33.5", 612 - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", 613 - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", 614 - "cpu": [ 615 - "arm64" 616 - ], 617 - "dev": true, 618 - "license": "Apache-2.0", 619 - "optional": true, 620 - "os": [ 621 - "darwin" 622 - ], 623 - "engines": { 624 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 625 - }, 626 - "funding": { 627 - "url": "https://opencollective.com/libvips" 628 - }, 629 - "optionalDependencies": { 630 - "@img/sharp-libvips-darwin-arm64": "1.0.4" 631 - } 632 - }, 633 - "node_modules/@img/sharp-darwin-x64": { 634 - "version": "0.33.5", 635 - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", 636 - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", 637 - "cpu": [ 638 - "x64" 639 - ], 640 - "dev": true, 641 - "license": "Apache-2.0", 642 - "optional": true, 643 - "os": [ 644 - "darwin" 645 - ], 646 - "engines": { 647 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 648 - }, 649 - "funding": { 650 - "url": "https://opencollective.com/libvips" 651 - }, 652 - "optionalDependencies": { 653 - "@img/sharp-libvips-darwin-x64": "1.0.4" 654 - } 655 - }, 656 - "node_modules/@img/sharp-libvips-darwin-arm64": { 657 - "version": "1.0.4", 658 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", 659 - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", 660 - "cpu": [ 661 - "arm64" 662 - ], 663 - "dev": true, 664 - "license": "LGPL-3.0-or-later", 665 - "optional": true, 666 - "os": [ 667 - "darwin" 668 - ], 669 - "funding": { 670 - "url": "https://opencollective.com/libvips" 671 - } 672 - }, 673 - "node_modules/@img/sharp-libvips-darwin-x64": { 674 - "version": "1.0.4", 675 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", 676 - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", 677 - "cpu": [ 678 - "x64" 679 - ], 680 - "dev": true, 681 - "license": "LGPL-3.0-or-later", 682 - "optional": true, 683 - "os": [ 684 - "darwin" 685 - ], 686 - "funding": { 687 - "url": "https://opencollective.com/libvips" 688 - } 689 - }, 690 - "node_modules/@img/sharp-libvips-linux-arm": { 691 - "version": "1.0.5", 692 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", 693 - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", 694 - "cpu": [ 695 - "arm" 696 - ], 697 - "dev": true, 698 - "license": "LGPL-3.0-or-later", 699 - "optional": true, 700 - "os": [ 701 - "linux" 702 - ], 703 - "funding": { 704 - "url": "https://opencollective.com/libvips" 705 - } 706 - }, 707 - "node_modules/@img/sharp-libvips-linux-arm64": { 708 - "version": "1.0.4", 709 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", 710 - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", 711 - "cpu": [ 712 - "arm64" 713 - ], 714 - "dev": true, 715 - "license": "LGPL-3.0-or-later", 716 - "optional": true, 717 - "os": [ 718 - "linux" 719 - ], 720 - "funding": { 721 - "url": "https://opencollective.com/libvips" 722 - } 723 - }, 724 - "node_modules/@img/sharp-libvips-linux-s390x": { 725 - "version": "1.0.4", 726 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", 727 - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", 728 - "cpu": [ 729 - "s390x" 730 - ], 731 - "dev": true, 732 - "license": "LGPL-3.0-or-later", 733 - "optional": true, 734 - "os": [ 735 - "linux" 736 - ], 737 - "funding": { 738 - "url": "https://opencollective.com/libvips" 739 - } 740 - }, 741 - "node_modules/@img/sharp-libvips-linux-x64": { 742 - "version": "1.0.4", 743 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", 744 - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", 745 - "cpu": [ 746 - "x64" 747 - ], 748 - "dev": true, 749 - "license": "LGPL-3.0-or-later", 750 - "optional": true, 751 - "os": [ 752 - "linux" 753 - ], 754 - "funding": { 755 - "url": "https://opencollective.com/libvips" 756 - } 757 - }, 758 - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 759 - "version": "1.0.4", 760 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", 761 - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", 762 - "cpu": [ 763 - "arm64" 764 - ], 765 - "dev": true, 766 - "license": "LGPL-3.0-or-later", 767 - "optional": true, 768 - "os": [ 769 - "linux" 770 - ], 771 - "funding": { 772 - "url": "https://opencollective.com/libvips" 773 - } 774 - }, 775 - "node_modules/@img/sharp-libvips-linuxmusl-x64": { 776 - "version": "1.0.4", 777 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", 778 - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", 779 - "cpu": [ 780 - "x64" 781 - ], 782 - "dev": true, 783 - "license": "LGPL-3.0-or-later", 784 - "optional": true, 785 - "os": [ 786 - "linux" 787 - ], 788 - "funding": { 789 - "url": "https://opencollective.com/libvips" 790 - } 791 - }, 792 - "node_modules/@img/sharp-linux-arm": { 793 - "version": "0.33.5", 794 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", 795 - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", 796 - "cpu": [ 797 - "arm" 798 - ], 799 - "dev": true, 800 - "license": "Apache-2.0", 801 - "optional": true, 802 - "os": [ 803 - "linux" 804 - ], 805 - "engines": { 806 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 807 - }, 808 - "funding": { 809 - "url": "https://opencollective.com/libvips" 810 - }, 811 - "optionalDependencies": { 812 - "@img/sharp-libvips-linux-arm": "1.0.5" 813 - } 814 - }, 815 - "node_modules/@img/sharp-linux-arm64": { 816 - "version": "0.33.5", 817 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", 818 - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", 819 - "cpu": [ 820 - "arm64" 821 - ], 822 - "dev": true, 823 - "license": "Apache-2.0", 824 - "optional": true, 825 - "os": [ 826 - "linux" 827 - ], 828 - "engines": { 829 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 830 - }, 831 - "funding": { 832 - "url": "https://opencollective.com/libvips" 833 - }, 834 - "optionalDependencies": { 835 - "@img/sharp-libvips-linux-arm64": "1.0.4" 836 - } 837 - }, 838 - "node_modules/@img/sharp-linux-s390x": { 839 - "version": "0.33.5", 840 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", 841 - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", 842 - "cpu": [ 843 - "s390x" 844 - ], 845 - "dev": true, 846 - "license": "Apache-2.0", 847 - "optional": true, 848 - "os": [ 849 - "linux" 850 - ], 851 - "engines": { 852 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 853 - }, 854 - "funding": { 855 - "url": "https://opencollective.com/libvips" 856 - }, 857 - "optionalDependencies": { 858 - "@img/sharp-libvips-linux-s390x": "1.0.4" 859 - } 860 - }, 861 - "node_modules/@img/sharp-linux-x64": { 862 - "version": "0.33.5", 863 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", 864 - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", 865 - "cpu": [ 866 - "x64" 867 - ], 868 - "dev": true, 869 - "license": "Apache-2.0", 870 - "optional": true, 871 - "os": [ 872 - "linux" 873 - ], 874 - "engines": { 875 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 876 - }, 877 - "funding": { 878 - "url": "https://opencollective.com/libvips" 879 - }, 880 - "optionalDependencies": { 881 - "@img/sharp-libvips-linux-x64": "1.0.4" 882 - } 883 - }, 884 - "node_modules/@img/sharp-linuxmusl-arm64": { 885 - "version": "0.33.5", 886 - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", 887 - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", 888 - "cpu": [ 889 - "arm64" 890 - ], 891 - "dev": true, 892 - "license": "Apache-2.0", 893 - "optional": true, 894 - "os": [ 895 - "linux" 896 - ], 897 - "engines": { 898 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 899 - }, 900 - "funding": { 901 - "url": "https://opencollective.com/libvips" 902 - }, 903 - "optionalDependencies": { 904 - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" 905 - } 906 - }, 907 - "node_modules/@img/sharp-linuxmusl-x64": { 908 - "version": "0.33.5", 909 - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", 910 - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", 911 - "cpu": [ 912 - "x64" 913 - ], 914 - "dev": true, 915 - "license": "Apache-2.0", 916 - "optional": true, 917 - "os": [ 918 - "linux" 919 - ], 920 - "engines": { 921 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 922 - }, 923 - "funding": { 924 - "url": "https://opencollective.com/libvips" 925 - }, 926 - "optionalDependencies": { 927 - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" 928 - } 929 - }, 930 - "node_modules/@img/sharp-wasm32": { 931 - "version": "0.33.5", 932 - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", 933 - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", 934 - "cpu": [ 935 - "wasm32" 936 - ], 937 - "dev": true, 938 - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 939 - "optional": true, 940 - "dependencies": { 941 - "@emnapi/runtime": "^1.2.0" 942 - }, 943 - "engines": { 944 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 945 - }, 946 - "funding": { 947 - "url": "https://opencollective.com/libvips" 948 - } 949 - }, 950 - "node_modules/@img/sharp-win32-ia32": { 951 - "version": "0.33.5", 952 - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", 953 - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", 954 - "cpu": [ 955 - "ia32" 956 - ], 957 - "dev": true, 958 - "license": "Apache-2.0 AND LGPL-3.0-or-later", 959 - "optional": true, 960 - "os": [ 961 - "win32" 962 - ], 963 - "engines": { 964 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 965 - }, 966 - "funding": { 967 - "url": "https://opencollective.com/libvips" 968 - } 969 - }, 970 - "node_modules/@img/sharp-win32-x64": { 971 - "version": "0.33.5", 972 - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", 973 - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", 974 - "cpu": [ 975 - "x64" 976 - ], 977 - "dev": true, 978 - "license": "Apache-2.0 AND LGPL-3.0-or-later", 979 - "optional": true, 980 - "os": [ 981 - "win32" 982 - ], 983 - "engines": { 984 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 985 - }, 986 - "funding": { 987 - "url": "https://opencollective.com/libvips" 988 - } 989 - }, 990 - "node_modules/@jridgewell/resolve-uri": { 991 - "version": "3.1.2", 992 - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 993 - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 994 - "dev": true, 995 - "license": "MIT", 996 - "engines": { 997 - "node": ">=6.0.0" 998 - } 999 - }, 1000 - "node_modules/@jridgewell/sourcemap-codec": { 1001 - "version": "1.5.0", 1002 - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 1003 - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 1004 - "dev": true, 1005 - "license": "MIT" 1006 - }, 1007 - "node_modules/@jridgewell/trace-mapping": { 1008 - "version": "0.3.9", 1009 - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 1010 - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 1011 - "dev": true, 1012 - "license": "MIT", 1013 - "dependencies": { 1014 - "@jridgewell/resolve-uri": "^3.0.3", 1015 - "@jridgewell/sourcemap-codec": "^1.4.10" 1016 - } 1017 - }, 1018 - "node_modules/@rollup/rollup-android-arm-eabi": { 1019 - "version": "4.40.1", 1020 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", 1021 - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", 1022 - "cpu": [ 1023 - "arm" 1024 - ], 1025 - "dev": true, 1026 - "license": "MIT", 1027 - "optional": true, 1028 - "os": [ 1029 - "android" 1030 - ] 1031 - }, 1032 - "node_modules/@rollup/rollup-android-arm64": { 1033 - "version": "4.40.1", 1034 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", 1035 - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", 1036 - "cpu": [ 1037 - "arm64" 1038 - ], 1039 - "dev": true, 1040 - "license": "MIT", 1041 - "optional": true, 1042 - "os": [ 1043 - "android" 1044 - ] 1045 - }, 1046 - "node_modules/@rollup/rollup-darwin-arm64": { 1047 - "version": "4.40.1", 1048 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", 1049 - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", 1050 - "cpu": [ 1051 - "arm64" 1052 - ], 1053 - "dev": true, 1054 - "license": "MIT", 1055 - "optional": true, 1056 - "os": [ 1057 - "darwin" 1058 - ] 1059 - }, 1060 - "node_modules/@rollup/rollup-darwin-x64": { 1061 - "version": "4.40.1", 1062 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", 1063 - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", 1064 - "cpu": [ 1065 - "x64" 1066 - ], 1067 - "dev": true, 1068 - "license": "MIT", 1069 - "optional": true, 1070 - "os": [ 1071 - "darwin" 1072 - ] 1073 - }, 1074 - "node_modules/@rollup/rollup-freebsd-arm64": { 1075 - "version": "4.40.1", 1076 - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", 1077 - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", 1078 - "cpu": [ 1079 - "arm64" 1080 - ], 1081 - "dev": true, 1082 - "license": "MIT", 1083 - "optional": true, 1084 - "os": [ 1085 - "freebsd" 1086 - ] 1087 - }, 1088 - "node_modules/@rollup/rollup-freebsd-x64": { 1089 - "version": "4.40.1", 1090 - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", 1091 - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", 1092 - "cpu": [ 1093 - "x64" 1094 - ], 1095 - "dev": true, 1096 - "license": "MIT", 1097 - "optional": true, 1098 - "os": [ 1099 - "freebsd" 1100 - ] 1101 - }, 1102 - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1103 - "version": "4.40.1", 1104 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", 1105 - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", 1106 - "cpu": [ 1107 - "arm" 1108 - ], 1109 - "dev": true, 1110 - "license": "MIT", 1111 - "optional": true, 1112 - "os": [ 1113 - "linux" 1114 - ] 1115 - }, 1116 - "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1117 - "version": "4.40.1", 1118 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", 1119 - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", 1120 - "cpu": [ 1121 - "arm" 1122 - ], 1123 - "dev": true, 1124 - "license": "MIT", 1125 - "optional": true, 1126 - "os": [ 1127 - "linux" 1128 - ] 1129 - }, 1130 - "node_modules/@rollup/rollup-linux-arm64-gnu": { 1131 - "version": "4.40.1", 1132 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", 1133 - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", 1134 - "cpu": [ 1135 - "arm64" 1136 - ], 1137 - "dev": true, 1138 - "license": "MIT", 1139 - "optional": true, 1140 - "os": [ 1141 - "linux" 1142 - ] 1143 - }, 1144 - "node_modules/@rollup/rollup-linux-arm64-musl": { 1145 - "version": "4.40.1", 1146 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", 1147 - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", 1148 - "cpu": [ 1149 - "arm64" 1150 - ], 1151 - "dev": true, 1152 - "license": "MIT", 1153 - "optional": true, 1154 - "os": [ 1155 - "linux" 1156 - ] 1157 - }, 1158 - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 1159 - "version": "4.40.1", 1160 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", 1161 - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", 1162 - "cpu": [ 1163 - "loong64" 1164 - ], 1165 - "dev": true, 1166 - "license": "MIT", 1167 - "optional": true, 1168 - "os": [ 1169 - "linux" 1170 - ] 1171 - }, 1172 - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 1173 - "version": "4.40.1", 1174 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", 1175 - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", 1176 - "cpu": [ 1177 - "ppc64" 1178 - ], 1179 - "dev": true, 1180 - "license": "MIT", 1181 - "optional": true, 1182 - "os": [ 1183 - "linux" 1184 - ] 1185 - }, 1186 - "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1187 - "version": "4.40.1", 1188 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", 1189 - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", 1190 - "cpu": [ 1191 - "riscv64" 1192 - ], 1193 - "dev": true, 1194 - "license": "MIT", 1195 - "optional": true, 1196 - "os": [ 1197 - "linux" 1198 - ] 1199 - }, 1200 - "node_modules/@rollup/rollup-linux-riscv64-musl": { 1201 - "version": "4.40.1", 1202 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", 1203 - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", 1204 - "cpu": [ 1205 - "riscv64" 1206 - ], 1207 - "dev": true, 1208 - "license": "MIT", 1209 - "optional": true, 1210 - "os": [ 1211 - "linux" 1212 - ] 1213 - }, 1214 - "node_modules/@rollup/rollup-linux-s390x-gnu": { 1215 - "version": "4.40.1", 1216 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", 1217 - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", 1218 - "cpu": [ 1219 - "s390x" 1220 - ], 1221 - "dev": true, 1222 - "license": "MIT", 1223 - "optional": true, 1224 - "os": [ 1225 - "linux" 1226 - ] 1227 - }, 1228 - "node_modules/@rollup/rollup-linux-x64-gnu": { 1229 - "version": "4.40.1", 1230 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", 1231 - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", 1232 - "cpu": [ 1233 - "x64" 1234 - ], 1235 - "dev": true, 1236 - "license": "MIT", 1237 - "optional": true, 1238 - "os": [ 1239 - "linux" 1240 - ] 1241 - }, 1242 - "node_modules/@rollup/rollup-linux-x64-musl": { 1243 - "version": "4.40.1", 1244 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", 1245 - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", 1246 - "cpu": [ 1247 - "x64" 1248 - ], 1249 - "dev": true, 1250 - "license": "MIT", 1251 - "optional": true, 1252 - "os": [ 1253 - "linux" 1254 - ] 1255 - }, 1256 - "node_modules/@rollup/rollup-win32-arm64-msvc": { 1257 - "version": "4.40.1", 1258 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", 1259 - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", 1260 - "cpu": [ 1261 - "arm64" 1262 - ], 1263 - "dev": true, 1264 - "license": "MIT", 1265 - "optional": true, 1266 - "os": [ 1267 - "win32" 1268 - ] 1269 - }, 1270 - "node_modules/@rollup/rollup-win32-ia32-msvc": { 1271 - "version": "4.40.1", 1272 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", 1273 - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", 1274 - "cpu": [ 1275 - "ia32" 1276 - ], 1277 - "dev": true, 1278 - "license": "MIT", 1279 - "optional": true, 1280 - "os": [ 1281 - "win32" 1282 - ] 1283 - }, 1284 - "node_modules/@rollup/rollup-win32-x64-msvc": { 1285 - "version": "4.40.1", 1286 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", 1287 - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", 1288 - "cpu": [ 1289 - "x64" 1290 - ], 1291 - "dev": true, 1292 - "license": "MIT", 1293 - "optional": true, 1294 - "os": [ 1295 - "win32" 1296 - ] 1297 - }, 1298 - "node_modules/@types/estree": { 1299 - "version": "1.0.7", 1300 - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 1301 - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 1302 - "dev": true, 1303 - "license": "MIT" 1304 - }, 1305 - "node_modules/@vitest/expect": { 1306 - "version": "3.0.9", 1307 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", 1308 - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", 1309 - "dev": true, 1310 - "license": "MIT", 1311 - "dependencies": { 1312 - "@vitest/spy": "3.0.9", 1313 - "@vitest/utils": "3.0.9", 1314 - "chai": "^5.2.0", 1315 - "tinyrainbow": "^2.0.0" 1316 - }, 1317 - "funding": { 1318 - "url": "https://opencollective.com/vitest" 1319 - } 1320 - }, 1321 - "node_modules/@vitest/mocker": { 1322 - "version": "3.0.9", 1323 - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", 1324 - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", 1325 - "dev": true, 1326 - "license": "MIT", 1327 - "dependencies": { 1328 - "@vitest/spy": "3.0.9", 1329 - "estree-walker": "^3.0.3", 1330 - "magic-string": "^0.30.17" 1331 - }, 1332 - "funding": { 1333 - "url": "https://opencollective.com/vitest" 1334 - }, 1335 - "peerDependencies": { 1336 - "msw": "^2.4.9", 1337 - "vite": "^5.0.0 || ^6.0.0" 1338 - }, 1339 - "peerDependenciesMeta": { 1340 - "msw": { 1341 - "optional": true 1342 - }, 1343 - "vite": { 1344 - "optional": true 1345 - } 1346 - } 1347 - }, 1348 - "node_modules/@vitest/pretty-format": { 1349 - "version": "3.1.2", 1350 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", 1351 - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", 1352 - "dev": true, 1353 - "license": "MIT", 1354 - "dependencies": { 1355 - "tinyrainbow": "^2.0.0" 1356 - }, 1357 - "funding": { 1358 - "url": "https://opencollective.com/vitest" 1359 - } 1360 - }, 1361 - "node_modules/@vitest/runner": { 1362 - "version": "3.0.9", 1363 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", 1364 - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", 1365 - "dev": true, 1366 - "license": "MIT", 1367 - "dependencies": { 1368 - "@vitest/utils": "3.0.9", 1369 - "pathe": "^2.0.3" 1370 - }, 1371 - "funding": { 1372 - "url": "https://opencollective.com/vitest" 1373 - } 1374 - }, 1375 - "node_modules/@vitest/snapshot": { 1376 - "version": "3.0.9", 1377 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", 1378 - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", 1379 - "dev": true, 1380 - "license": "MIT", 1381 - "dependencies": { 1382 - "@vitest/pretty-format": "3.0.9", 1383 - "magic-string": "^0.30.17", 1384 - "pathe": "^2.0.3" 1385 - }, 1386 - "funding": { 1387 - "url": "https://opencollective.com/vitest" 1388 - } 1389 - }, 1390 - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { 1391 - "version": "3.0.9", 1392 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1393 - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1394 - "dev": true, 1395 - "license": "MIT", 1396 - "dependencies": { 1397 - "tinyrainbow": "^2.0.0" 1398 - }, 1399 - "funding": { 1400 - "url": "https://opencollective.com/vitest" 1401 - } 1402 - }, 1403 - "node_modules/@vitest/spy": { 1404 - "version": "3.0.9", 1405 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", 1406 - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", 1407 - "dev": true, 1408 - "license": "MIT", 1409 - "dependencies": { 1410 - "tinyspy": "^3.0.2" 1411 - }, 1412 - "funding": { 1413 - "url": "https://opencollective.com/vitest" 1414 - } 1415 - }, 1416 - "node_modules/@vitest/utils": { 1417 - "version": "3.0.9", 1418 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", 1419 - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", 1420 - "dev": true, 1421 - "license": "MIT", 1422 - "dependencies": { 1423 - "@vitest/pretty-format": "3.0.9", 1424 - "loupe": "^3.1.3", 1425 - "tinyrainbow": "^2.0.0" 1426 - }, 1427 - "funding": { 1428 - "url": "https://opencollective.com/vitest" 1429 - } 1430 - }, 1431 - "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { 1432 - "version": "3.0.9", 1433 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1434 - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1435 - "dev": true, 1436 - "license": "MIT", 1437 - "dependencies": { 1438 - "tinyrainbow": "^2.0.0" 1439 - }, 1440 - "funding": { 1441 - "url": "https://opencollective.com/vitest" 1442 - } 1443 - }, 1444 - "node_modules/acorn": { 1445 - "version": "8.14.0", 1446 - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 1447 - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 1448 - "dev": true, 1449 - "license": "MIT", 1450 - "bin": { 1451 - "acorn": "bin/acorn" 1452 - }, 1453 - "engines": { 1454 - "node": ">=0.4.0" 1455 - } 1456 - }, 1457 - "node_modules/acorn-walk": { 1458 - "version": "8.3.2", 1459 - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", 1460 - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", 1461 - "dev": true, 1462 - "license": "MIT", 1463 - "engines": { 1464 - "node": ">=0.4.0" 1465 - } 1466 - }, 1467 - "node_modules/as-table": { 1468 - "version": "1.0.55", 1469 - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", 1470 - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", 1471 - "dev": true, 1472 - "license": "MIT", 1473 - "dependencies": { 1474 - "printable-characters": "^1.0.42" 1475 - } 1476 - }, 1477 - "node_modules/assertion-error": { 1478 - "version": "2.0.1", 1479 - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1480 - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1481 - "dev": true, 1482 - "license": "MIT", 1483 - "engines": { 1484 - "node": ">=12" 1485 - } 1486 - }, 1487 - "node_modules/birpc": { 1488 - "version": "0.2.14", 1489 - "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", 1490 - "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", 1491 - "dev": true, 1492 - "license": "MIT", 1493 - "funding": { 1494 - "url": "https://github.com/sponsors/antfu" 1495 - } 1496 - }, 1497 - "node_modules/blake3-wasm": { 1498 - "version": "2.1.5", 1499 - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 1500 - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 1501 - "dev": true, 1502 - "license": "MIT" 1503 - }, 1504 - "node_modules/cac": { 1505 - "version": "6.7.14", 1506 - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 1507 - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 1508 - "dev": true, 1509 - "license": "MIT", 1510 - "engines": { 1511 - "node": ">=8" 1512 - } 1513 - }, 1514 - "node_modules/chai": { 1515 - "version": "5.2.0", 1516 - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", 1517 - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", 1518 - "dev": true, 1519 - "license": "MIT", 1520 - "dependencies": { 1521 - "assertion-error": "^2.0.1", 1522 - "check-error": "^2.1.1", 1523 - "deep-eql": "^5.0.1", 1524 - "loupe": "^3.1.0", 1525 - "pathval": "^2.0.0" 1526 - }, 1527 - "engines": { 1528 - "node": ">=12" 1529 - } 1530 - }, 1531 - "node_modules/check-error": { 1532 - "version": "2.1.1", 1533 - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 1534 - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 1535 - "dev": true, 1536 - "license": "MIT", 1537 - "engines": { 1538 - "node": ">= 16" 1539 - } 1540 - }, 1541 - "node_modules/cjs-module-lexer": { 1542 - "version": "1.4.3", 1543 - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", 1544 - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", 1545 - "dev": true, 1546 - "license": "MIT" 1547 - }, 1548 - "node_modules/color": { 1549 - "version": "4.2.3", 1550 - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 1551 - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 1552 - "dev": true, 1553 - "license": "MIT", 1554 - "optional": true, 1555 - "dependencies": { 1556 - "color-convert": "^2.0.1", 1557 - "color-string": "^1.9.0" 1558 - }, 1559 - "engines": { 1560 - "node": ">=12.5.0" 1561 - } 1562 - }, 1563 - "node_modules/color-convert": { 1564 - "version": "2.0.1", 1565 - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1566 - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1567 - "dev": true, 1568 - "license": "MIT", 1569 - "optional": true, 1570 - "dependencies": { 1571 - "color-name": "~1.1.4" 1572 - }, 1573 - "engines": { 1574 - "node": ">=7.0.0" 1575 - } 1576 - }, 1577 - "node_modules/color-name": { 1578 - "version": "1.1.4", 1579 - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1580 - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1581 - "dev": true, 1582 - "license": "MIT", 1583 - "optional": true 1584 - }, 1585 - "node_modules/color-string": { 1586 - "version": "1.9.1", 1587 - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1588 - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1589 - "dev": true, 1590 - "license": "MIT", 1591 - "optional": true, 1592 - "dependencies": { 1593 - "color-name": "^1.0.0", 1594 - "simple-swizzle": "^0.2.2" 1595 - } 1596 - }, 1597 - "node_modules/cookie": { 1598 - "version": "0.7.2", 1599 - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 1600 - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 1601 - "dev": true, 1602 - "license": "MIT", 1603 - "engines": { 1604 - "node": ">= 0.6" 1605 - } 1606 - }, 1607 - "node_modules/data-uri-to-buffer": { 1608 - "version": "2.0.2", 1609 - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", 1610 - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", 1611 - "dev": true, 1612 - "license": "MIT" 1613 - }, 1614 - "node_modules/debug": { 1615 - "version": "4.4.0", 1616 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1617 - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1618 - "dev": true, 1619 - "license": "MIT", 1620 - "dependencies": { 1621 - "ms": "^2.1.3" 1622 - }, 1623 - "engines": { 1624 - "node": ">=6.0" 1625 - }, 1626 - "peerDependenciesMeta": { 1627 - "supports-color": { 1628 - "optional": true 1629 - } 1630 - } 1631 - }, 1632 - "node_modules/deep-eql": { 1633 - "version": "5.0.2", 1634 - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 1635 - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 1636 - "dev": true, 1637 - "license": "MIT", 1638 - "engines": { 1639 - "node": ">=6" 1640 - } 1641 - }, 1642 - "node_modules/defu": { 1643 - "version": "6.1.4", 1644 - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 1645 - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 1646 - "dev": true, 1647 - "license": "MIT" 1648 - }, 1649 - "node_modules/detect-libc": { 1650 - "version": "2.0.4", 1651 - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", 1652 - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", 1653 - "dev": true, 1654 - "license": "Apache-2.0", 1655 - "optional": true, 1656 - "engines": { 1657 - "node": ">=8" 1658 - } 1659 - }, 1660 - "node_modules/devalue": { 1661 - "version": "4.3.3", 1662 - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", 1663 - "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", 1664 - "dev": true, 1665 - "license": "MIT" 1666 - }, 1667 - "node_modules/es-module-lexer": { 1668 - "version": "1.7.0", 1669 - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1670 - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1671 - "dev": true, 1672 - "license": "MIT" 1673 - }, 1674 - "node_modules/esbuild": { 1675 - "version": "0.25.3", 1676 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", 1677 - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", 1678 - "dev": true, 1679 - "hasInstallScript": true, 1680 - "license": "MIT", 1681 - "bin": { 1682 - "esbuild": "bin/esbuild" 1683 - }, 1684 - "engines": { 1685 - "node": ">=18" 1686 - }, 1687 - "optionalDependencies": { 1688 - "@esbuild/aix-ppc64": "0.25.3", 1689 - "@esbuild/android-arm": "0.25.3", 1690 - "@esbuild/android-arm64": "0.25.3", 1691 - "@esbuild/android-x64": "0.25.3", 1692 - "@esbuild/darwin-arm64": "0.25.3", 1693 - "@esbuild/darwin-x64": "0.25.3", 1694 - "@esbuild/freebsd-arm64": "0.25.3", 1695 - "@esbuild/freebsd-x64": "0.25.3", 1696 - "@esbuild/linux-arm": "0.25.3", 1697 - "@esbuild/linux-arm64": "0.25.3", 1698 - "@esbuild/linux-ia32": "0.25.3", 1699 - "@esbuild/linux-loong64": "0.25.3", 1700 - "@esbuild/linux-mips64el": "0.25.3", 1701 - "@esbuild/linux-ppc64": "0.25.3", 1702 - "@esbuild/linux-riscv64": "0.25.3", 1703 - "@esbuild/linux-s390x": "0.25.3", 1704 - "@esbuild/linux-x64": "0.25.3", 1705 - "@esbuild/netbsd-arm64": "0.25.3", 1706 - "@esbuild/netbsd-x64": "0.25.3", 1707 - "@esbuild/openbsd-arm64": "0.25.3", 1708 - "@esbuild/openbsd-x64": "0.25.3", 1709 - "@esbuild/sunos-x64": "0.25.3", 1710 - "@esbuild/win32-arm64": "0.25.3", 1711 - "@esbuild/win32-ia32": "0.25.3", 1712 - "@esbuild/win32-x64": "0.25.3" 1713 - } 1714 - }, 1715 - "node_modules/estree-walker": { 1716 - "version": "3.0.3", 1717 - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1718 - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1719 - "dev": true, 1720 - "license": "MIT", 1721 - "dependencies": { 1722 - "@types/estree": "^1.0.0" 1723 - } 1724 - }, 1725 - "node_modules/exit-hook": { 1726 - "version": "2.2.1", 1727 - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 1728 - "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 1729 - "dev": true, 1730 - "license": "MIT", 1731 - "engines": { 1732 - "node": ">=6" 1733 - }, 1734 - "funding": { 1735 - "url": "https://github.com/sponsors/sindresorhus" 1736 - } 1737 - }, 1738 - "node_modules/expect-type": { 1739 - "version": "1.2.1", 1740 - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", 1741 - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", 1742 - "dev": true, 1743 - "license": "Apache-2.0", 1744 - "engines": { 1745 - "node": ">=12.0.0" 1746 - } 1747 - }, 1748 - "node_modules/exsolve": { 1749 - "version": "1.0.5", 1750 - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", 1751 - "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", 1752 - "dev": true, 1753 - "license": "MIT" 1754 - }, 1755 - "node_modules/fdir": { 1756 - "version": "6.4.4", 1757 - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", 1758 - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 1759 - "dev": true, 1760 - "license": "MIT", 1761 - "peerDependencies": { 1762 - "picomatch": "^3 || ^4" 1763 - }, 1764 - "peerDependenciesMeta": { 1765 - "picomatch": { 1766 - "optional": true 1767 - } 1768 - } 1769 - }, 1770 - "node_modules/fsevents": { 1771 - "version": "2.3.3", 1772 - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1773 - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1774 - "dev": true, 1775 - "hasInstallScript": true, 1776 - "license": "MIT", 1777 - "optional": true, 1778 - "os": [ 1779 - "darwin" 1780 - ], 1781 - "engines": { 1782 - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1783 - } 1784 - }, 1785 - "node_modules/get-source": { 1786 - "version": "2.0.12", 1787 - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", 1788 - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", 1789 - "dev": true, 1790 - "license": "Unlicense", 1791 - "dependencies": { 1792 - "data-uri-to-buffer": "^2.0.0", 1793 - "source-map": "^0.6.1" 1794 - } 1795 - }, 1796 - "node_modules/glob-to-regexp": { 1797 - "version": "0.4.1", 1798 - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 1799 - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 1800 - "dev": true, 1801 - "license": "BSD-2-Clause" 1802 - }, 1803 - "node_modules/is-arrayish": { 1804 - "version": "0.3.2", 1805 - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 1806 - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 1807 - "dev": true, 1808 - "license": "MIT", 1809 - "optional": true 1810 - }, 1811 - "node_modules/loupe": { 1812 - "version": "3.1.3", 1813 - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", 1814 - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", 1815 - "dev": true, 1816 - "license": "MIT" 1817 - }, 1818 - "node_modules/magic-string": { 1819 - "version": "0.30.17", 1820 - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 1821 - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 1822 - "dev": true, 1823 - "license": "MIT", 1824 - "dependencies": { 1825 - "@jridgewell/sourcemap-codec": "^1.5.0" 1826 - } 1827 - }, 1828 - "node_modules/mime": { 1829 - "version": "3.0.0", 1830 - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1831 - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1832 - "dev": true, 1833 - "license": "MIT", 1834 - "bin": { 1835 - "mime": "cli.js" 1836 - }, 1837 - "engines": { 1838 - "node": ">=10.0.0" 1839 - } 1840 - }, 1841 - "node_modules/miniflare": { 1842 - "version": "4.20250428.1", 1843 - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250428.1.tgz", 1844 - "integrity": "sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==", 1845 - "dev": true, 1846 - "license": "MIT", 1847 - "dependencies": { 1848 - "@cspotcode/source-map-support": "0.8.1", 1849 - "acorn": "8.14.0", 1850 - "acorn-walk": "8.3.2", 1851 - "exit-hook": "2.2.1", 1852 - "glob-to-regexp": "0.4.1", 1853 - "stoppable": "1.1.0", 1854 - "undici": "^5.28.5", 1855 - "workerd": "1.20250428.0", 1856 - "ws": "8.18.0", 1857 - "youch": "3.3.4", 1858 - "zod": "3.22.3" 1859 - }, 1860 - "bin": { 1861 - "miniflare": "bootstrap.js" 1862 - }, 1863 - "engines": { 1864 - "node": ">=18.0.0" 1865 - } 1866 - }, 1867 - "node_modules/miniflare/node_modules/zod": { 1868 - "version": "3.22.3", 1869 - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", 1870 - "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", 1871 - "dev": true, 1872 - "license": "MIT", 1873 - "funding": { 1874 - "url": "https://github.com/sponsors/colinhacks" 1875 - } 1876 - }, 1877 - "node_modules/ms": { 1878 - "version": "2.1.3", 1879 - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1880 - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1881 - "dev": true, 1882 - "license": "MIT" 1883 - }, 1884 - "node_modules/mustache": { 1885 - "version": "4.2.0", 1886 - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 1887 - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 1888 - "dev": true, 1889 - "license": "MIT", 1890 - "bin": { 1891 - "mustache": "bin/mustache" 1892 - } 1893 - }, 1894 - "node_modules/nanoid": { 1895 - "version": "3.3.11", 1896 - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1897 - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1898 - "dev": true, 1899 - "funding": [ 1900 - { 1901 - "type": "github", 1902 - "url": "https://github.com/sponsors/ai" 1903 - } 1904 - ], 1905 - "license": "MIT", 1906 - "bin": { 1907 - "nanoid": "bin/nanoid.cjs" 1908 - }, 1909 - "engines": { 1910 - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1911 - } 1912 - }, 1913 - "node_modules/ohash": { 1914 - "version": "2.0.11", 1915 - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", 1916 - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", 1917 - "dev": true, 1918 - "license": "MIT" 1919 - }, 1920 - "node_modules/path-to-regexp": { 1921 - "version": "6.3.0", 1922 - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 1923 - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 1924 - "dev": true, 1925 - "license": "MIT" 1926 - }, 1927 - "node_modules/pathe": { 1928 - "version": "2.0.3", 1929 - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 1930 - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 1931 - "dev": true, 1932 - "license": "MIT" 1933 - }, 1934 - "node_modules/pathval": { 1935 - "version": "2.0.0", 1936 - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 1937 - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 1938 - "dev": true, 1939 - "license": "MIT", 1940 - "engines": { 1941 - "node": ">= 14.16" 1942 - } 1943 - }, 1944 - "node_modules/picocolors": { 1945 - "version": "1.1.1", 1946 - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1947 - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1948 - "dev": true, 1949 - "license": "ISC" 1950 - }, 1951 - "node_modules/picomatch": { 1952 - "version": "4.0.2", 1953 - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 1954 - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 1955 - "dev": true, 1956 - "license": "MIT", 1957 - "engines": { 1958 - "node": ">=12" 1959 - }, 1960 - "funding": { 1961 - "url": "https://github.com/sponsors/jonschlinkert" 1962 - } 1963 - }, 1964 - "node_modules/postcss": { 1965 - "version": "8.5.3", 1966 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", 1967 - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", 1968 - "dev": true, 1969 - "funding": [ 1970 - { 1971 - "type": "opencollective", 1972 - "url": "https://opencollective.com/postcss/" 1973 - }, 1974 - { 1975 - "type": "tidelift", 1976 - "url": "https://tidelift.com/funding/github/npm/postcss" 1977 - }, 1978 - { 1979 - "type": "github", 1980 - "url": "https://github.com/sponsors/ai" 1981 - } 1982 - ], 1983 - "license": "MIT", 1984 - "dependencies": { 1985 - "nanoid": "^3.3.8", 1986 - "picocolors": "^1.1.1", 1987 - "source-map-js": "^1.2.1" 1988 - }, 1989 - "engines": { 1990 - "node": "^10 || ^12 || >=14" 1991 - } 1992 - }, 1993 - "node_modules/printable-characters": { 1994 - "version": "1.0.42", 1995 - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", 1996 - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", 1997 - "dev": true, 1998 - "license": "Unlicense" 1999 - }, 2000 - "node_modules/rollup": { 2001 - "version": "4.40.1", 2002 - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", 2003 - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", 2004 - "dev": true, 2005 - "license": "MIT", 2006 - "dependencies": { 2007 - "@types/estree": "1.0.7" 2008 - }, 2009 - "bin": { 2010 - "rollup": "dist/bin/rollup" 2011 - }, 2012 - "engines": { 2013 - "node": ">=18.0.0", 2014 - "npm": ">=8.0.0" 2015 - }, 2016 - "optionalDependencies": { 2017 - "@rollup/rollup-android-arm-eabi": "4.40.1", 2018 - "@rollup/rollup-android-arm64": "4.40.1", 2019 - "@rollup/rollup-darwin-arm64": "4.40.1", 2020 - "@rollup/rollup-darwin-x64": "4.40.1", 2021 - "@rollup/rollup-freebsd-arm64": "4.40.1", 2022 - "@rollup/rollup-freebsd-x64": "4.40.1", 2023 - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", 2024 - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", 2025 - "@rollup/rollup-linux-arm64-gnu": "4.40.1", 2026 - "@rollup/rollup-linux-arm64-musl": "4.40.1", 2027 - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", 2028 - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", 2029 - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", 2030 - "@rollup/rollup-linux-riscv64-musl": "4.40.1", 2031 - "@rollup/rollup-linux-s390x-gnu": "4.40.1", 2032 - "@rollup/rollup-linux-x64-gnu": "4.40.1", 2033 - "@rollup/rollup-linux-x64-musl": "4.40.1", 2034 - "@rollup/rollup-win32-arm64-msvc": "4.40.1", 2035 - "@rollup/rollup-win32-ia32-msvc": "4.40.1", 2036 - "@rollup/rollup-win32-x64-msvc": "4.40.1", 2037 - "fsevents": "~2.3.2" 2038 - } 2039 - }, 2040 - "node_modules/semver": { 2041 - "version": "7.7.1", 2042 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 2043 - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 2044 - "dev": true, 2045 - "license": "ISC", 2046 - "bin": { 2047 - "semver": "bin/semver.js" 2048 - }, 2049 - "engines": { 2050 - "node": ">=10" 2051 - } 2052 - }, 2053 - "node_modules/sharp": { 2054 - "version": "0.33.5", 2055 - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", 2056 - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", 2057 - "dev": true, 2058 - "hasInstallScript": true, 2059 - "license": "Apache-2.0", 2060 - "optional": true, 2061 - "dependencies": { 2062 - "color": "^4.2.3", 2063 - "detect-libc": "^2.0.3", 2064 - "semver": "^7.6.3" 2065 - }, 2066 - "engines": { 2067 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 2068 - }, 2069 - "funding": { 2070 - "url": "https://opencollective.com/libvips" 2071 - }, 2072 - "optionalDependencies": { 2073 - "@img/sharp-darwin-arm64": "0.33.5", 2074 - "@img/sharp-darwin-x64": "0.33.5", 2075 - "@img/sharp-libvips-darwin-arm64": "1.0.4", 2076 - "@img/sharp-libvips-darwin-x64": "1.0.4", 2077 - "@img/sharp-libvips-linux-arm": "1.0.5", 2078 - "@img/sharp-libvips-linux-arm64": "1.0.4", 2079 - "@img/sharp-libvips-linux-s390x": "1.0.4", 2080 - "@img/sharp-libvips-linux-x64": "1.0.4", 2081 - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", 2082 - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", 2083 - "@img/sharp-linux-arm": "0.33.5", 2084 - "@img/sharp-linux-arm64": "0.33.5", 2085 - "@img/sharp-linux-s390x": "0.33.5", 2086 - "@img/sharp-linux-x64": "0.33.5", 2087 - "@img/sharp-linuxmusl-arm64": "0.33.5", 2088 - "@img/sharp-linuxmusl-x64": "0.33.5", 2089 - "@img/sharp-wasm32": "0.33.5", 2090 - "@img/sharp-win32-ia32": "0.33.5", 2091 - "@img/sharp-win32-x64": "0.33.5" 2092 - } 2093 - }, 2094 - "node_modules/siginfo": { 2095 - "version": "2.0.0", 2096 - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 2097 - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 2098 - "dev": true, 2099 - "license": "ISC" 2100 - }, 2101 - "node_modules/simple-swizzle": { 2102 - "version": "0.2.2", 2103 - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 2104 - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 2105 - "dev": true, 2106 - "license": "MIT", 2107 - "optional": true, 2108 - "dependencies": { 2109 - "is-arrayish": "^0.3.1" 2110 - } 2111 - }, 2112 - "node_modules/source-map": { 2113 - "version": "0.6.1", 2114 - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 2115 - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 2116 - "dev": true, 2117 - "license": "BSD-3-Clause", 2118 - "engines": { 2119 - "node": ">=0.10.0" 2120 - } 2121 - }, 2122 - "node_modules/source-map-js": { 2123 - "version": "1.2.1", 2124 - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2125 - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2126 - "dev": true, 2127 - "license": "BSD-3-Clause", 2128 - "engines": { 2129 - "node": ">=0.10.0" 2130 - } 2131 - }, 2132 - "node_modules/stackback": { 2133 - "version": "0.0.2", 2134 - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 2135 - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 2136 - "dev": true, 2137 - "license": "MIT" 2138 - }, 2139 - "node_modules/stacktracey": { 2140 - "version": "2.1.8", 2141 - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", 2142 - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", 2143 - "dev": true, 2144 - "license": "Unlicense", 2145 - "dependencies": { 2146 - "as-table": "^1.0.36", 2147 - "get-source": "^2.0.12" 2148 - } 2149 - }, 2150 - "node_modules/std-env": { 2151 - "version": "3.9.0", 2152 - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", 2153 - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", 2154 - "dev": true, 2155 - "license": "MIT" 2156 - }, 2157 - "node_modules/stoppable": { 2158 - "version": "1.1.0", 2159 - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 2160 - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 2161 - "dev": true, 2162 - "license": "MIT", 2163 - "engines": { 2164 - "node": ">=4", 2165 - "npm": ">=6" 2166 - } 2167 - }, 2168 - "node_modules/tinybench": { 2169 - "version": "2.9.0", 2170 - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 2171 - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 2172 - "dev": true, 2173 - "license": "MIT" 2174 - }, 2175 - "node_modules/tinyexec": { 2176 - "version": "0.3.2", 2177 - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 2178 - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 2179 - "dev": true, 2180 - "license": "MIT" 2181 - }, 2182 - "node_modules/tinyglobby": { 2183 - "version": "0.2.13", 2184 - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", 2185 - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", 2186 - "dev": true, 2187 - "license": "MIT", 2188 - "dependencies": { 2189 - "fdir": "^6.4.4", 2190 - "picomatch": "^4.0.2" 2191 - }, 2192 - "engines": { 2193 - "node": ">=12.0.0" 2194 - }, 2195 - "funding": { 2196 - "url": "https://github.com/sponsors/SuperchupuDev" 2197 - } 2198 - }, 2199 - "node_modules/tinypool": { 2200 - "version": "1.0.2", 2201 - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", 2202 - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", 2203 - "dev": true, 2204 - "license": "MIT", 2205 - "engines": { 2206 - "node": "^18.0.0 || >=20.0.0" 2207 - } 2208 - }, 2209 - "node_modules/tinyrainbow": { 2210 - "version": "2.0.0", 2211 - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", 2212 - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", 2213 - "dev": true, 2214 - "license": "MIT", 2215 - "engines": { 2216 - "node": ">=14.0.0" 2217 - } 2218 - }, 2219 - "node_modules/tinyspy": { 2220 - "version": "3.0.2", 2221 - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", 2222 - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", 2223 - "dev": true, 2224 - "license": "MIT", 2225 - "engines": { 2226 - "node": ">=14.0.0" 2227 - } 2228 - }, 2229 - "node_modules/tslib": { 2230 - "version": "2.8.1", 2231 - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2232 - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2233 - "dev": true, 2234 - "license": "0BSD", 2235 - "optional": true 2236 - }, 2237 - "node_modules/ufo": { 2238 - "version": "1.6.1", 2239 - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", 2240 - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", 2241 - "dev": true, 2242 - "license": "MIT" 2243 - }, 2244 - "node_modules/undici": { 2245 - "version": "5.29.0", 2246 - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 2247 - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 2248 - "dev": true, 2249 - "license": "MIT", 2250 - "dependencies": { 2251 - "@fastify/busboy": "^2.0.0" 2252 - }, 2253 - "engines": { 2254 - "node": ">=14.0" 2255 - } 2256 - }, 2257 - "node_modules/unenv": { 2258 - "version": "2.0.0-rc.15", 2259 - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz", 2260 - "integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==", 2261 - "dev": true, 2262 - "license": "MIT", 2263 - "dependencies": { 2264 - "defu": "^6.1.4", 2265 - "exsolve": "^1.0.4", 2266 - "ohash": "^2.0.11", 2267 - "pathe": "^2.0.3", 2268 - "ufo": "^1.5.4" 2269 - } 2270 - }, 2271 - "node_modules/vite": { 2272 - "version": "6.3.4", 2273 - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", 2274 - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", 2275 - "dev": true, 2276 - "license": "MIT", 2277 - "dependencies": { 2278 - "esbuild": "^0.25.0", 2279 - "fdir": "^6.4.4", 2280 - "picomatch": "^4.0.2", 2281 - "postcss": "^8.5.3", 2282 - "rollup": "^4.34.9", 2283 - "tinyglobby": "^0.2.13" 2284 - }, 2285 - "bin": { 2286 - "vite": "bin/vite.js" 2287 - }, 2288 - "engines": { 2289 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2290 - }, 2291 - "funding": { 2292 - "url": "https://github.com/vitejs/vite?sponsor=1" 2293 - }, 2294 - "optionalDependencies": { 2295 - "fsevents": "~2.3.3" 2296 - }, 2297 - "peerDependencies": { 2298 - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2299 - "jiti": ">=1.21.0", 2300 - "less": "*", 2301 - "lightningcss": "^1.21.0", 2302 - "sass": "*", 2303 - "sass-embedded": "*", 2304 - "stylus": "*", 2305 - "sugarss": "*", 2306 - "terser": "^5.16.0", 2307 - "tsx": "^4.8.1", 2308 - "yaml": "^2.4.2" 2309 - }, 2310 - "peerDependenciesMeta": { 2311 - "@types/node": { 2312 - "optional": true 2313 - }, 2314 - "jiti": { 2315 - "optional": true 2316 - }, 2317 - "less": { 2318 - "optional": true 2319 - }, 2320 - "lightningcss": { 2321 - "optional": true 2322 - }, 2323 - "sass": { 2324 - "optional": true 2325 - }, 2326 - "sass-embedded": { 2327 - "optional": true 2328 - }, 2329 - "stylus": { 2330 - "optional": true 2331 - }, 2332 - "sugarss": { 2333 - "optional": true 2334 - }, 2335 - "terser": { 2336 - "optional": true 2337 - }, 2338 - "tsx": { 2339 - "optional": true 2340 - }, 2341 - "yaml": { 2342 - "optional": true 2343 - } 2344 - } 2345 - }, 2346 - "node_modules/vite-node": { 2347 - "version": "3.0.9", 2348 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", 2349 - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", 2350 - "dev": true, 2351 - "license": "MIT", 2352 - "dependencies": { 2353 - "cac": "^6.7.14", 2354 - "debug": "^4.4.0", 2355 - "es-module-lexer": "^1.6.0", 2356 - "pathe": "^2.0.3", 2357 - "vite": "^5.0.0 || ^6.0.0" 2358 - }, 2359 - "bin": { 2360 - "vite-node": "vite-node.mjs" 2361 - }, 2362 - "engines": { 2363 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2364 - }, 2365 - "funding": { 2366 - "url": "https://opencollective.com/vitest" 2367 - } 2368 - }, 2369 - "node_modules/vitest": { 2370 - "version": "3.0.9", 2371 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", 2372 - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", 2373 - "dev": true, 2374 - "license": "MIT", 2375 - "dependencies": { 2376 - "@vitest/expect": "3.0.9", 2377 - "@vitest/mocker": "3.0.9", 2378 - "@vitest/pretty-format": "^3.0.9", 2379 - "@vitest/runner": "3.0.9", 2380 - "@vitest/snapshot": "3.0.9", 2381 - "@vitest/spy": "3.0.9", 2382 - "@vitest/utils": "3.0.9", 2383 - "chai": "^5.2.0", 2384 - "debug": "^4.4.0", 2385 - "expect-type": "^1.1.0", 2386 - "magic-string": "^0.30.17", 2387 - "pathe": "^2.0.3", 2388 - "std-env": "^3.8.0", 2389 - "tinybench": "^2.9.0", 2390 - "tinyexec": "^0.3.2", 2391 - "tinypool": "^1.0.2", 2392 - "tinyrainbow": "^2.0.0", 2393 - "vite": "^5.0.0 || ^6.0.0", 2394 - "vite-node": "3.0.9", 2395 - "why-is-node-running": "^2.3.0" 2396 - }, 2397 - "bin": { 2398 - "vitest": "vitest.mjs" 2399 - }, 2400 - "engines": { 2401 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2402 - }, 2403 - "funding": { 2404 - "url": "https://opencollective.com/vitest" 2405 - }, 2406 - "peerDependencies": { 2407 - "@edge-runtime/vm": "*", 2408 - "@types/debug": "^4.1.12", 2409 - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2410 - "@vitest/browser": "3.0.9", 2411 - "@vitest/ui": "3.0.9", 2412 - "happy-dom": "*", 2413 - "jsdom": "*" 2414 - }, 2415 - "peerDependenciesMeta": { 2416 - "@edge-runtime/vm": { 2417 - "optional": true 2418 - }, 2419 - "@types/debug": { 2420 - "optional": true 2421 - }, 2422 - "@types/node": { 2423 - "optional": true 2424 - }, 2425 - "@vitest/browser": { 2426 - "optional": true 2427 - }, 2428 - "@vitest/ui": { 2429 - "optional": true 2430 - }, 2431 - "happy-dom": { 2432 - "optional": true 2433 - }, 2434 - "jsdom": { 2435 - "optional": true 2436 - } 2437 - } 2438 - }, 2439 - "node_modules/why-is-node-running": { 2440 - "version": "2.3.0", 2441 - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 2442 - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 2443 - "dev": true, 2444 - "license": "MIT", 2445 - "dependencies": { 2446 - "siginfo": "^2.0.0", 2447 - "stackback": "0.0.2" 2448 - }, 2449 - "bin": { 2450 - "why-is-node-running": "cli.js" 2451 - }, 2452 - "engines": { 2453 - "node": ">=8" 2454 - } 2455 - }, 2456 - "node_modules/workerd": { 2457 - "version": "1.20250428.0", 2458 - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250428.0.tgz", 2459 - "integrity": "sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==", 2460 - "dev": true, 2461 - "hasInstallScript": true, 2462 - "license": "Apache-2.0", 2463 - "bin": { 2464 - "workerd": "bin/workerd" 2465 - }, 2466 - "engines": { 2467 - "node": ">=16" 2468 - }, 2469 - "optionalDependencies": { 2470 - "@cloudflare/workerd-darwin-64": "1.20250428.0", 2471 - "@cloudflare/workerd-darwin-arm64": "1.20250428.0", 2472 - "@cloudflare/workerd-linux-64": "1.20250428.0", 2473 - "@cloudflare/workerd-linux-arm64": "1.20250428.0", 2474 - "@cloudflare/workerd-windows-64": "1.20250428.0" 2475 - } 2476 - }, 2477 - "node_modules/wrangler": { 2478 - "version": "4.14.1", 2479 - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.14.1.tgz", 2480 - "integrity": "sha512-EU7IThP7i68TBftJJSveogvWZ5k/WRijcJh3UclDWiWWhDZTPbL6LOJEFhHKqFzHOaC4Y2Aewt48rfTz0e7oCw==", 2481 - "dev": true, 2482 - "license": "MIT OR Apache-2.0", 2483 - "dependencies": { 2484 - "@cloudflare/kv-asset-handler": "0.4.0", 2485 - "@cloudflare/unenv-preset": "2.3.1", 2486 - "blake3-wasm": "2.1.5", 2487 - "esbuild": "0.25.2", 2488 - "miniflare": "4.20250428.1", 2489 - "path-to-regexp": "6.3.0", 2490 - "unenv": "2.0.0-rc.15", 2491 - "workerd": "1.20250428.0" 2492 - }, 2493 - "bin": { 2494 - "wrangler": "bin/wrangler.js", 2495 - "wrangler2": "bin/wrangler.js" 2496 - }, 2497 - "engines": { 2498 - "node": ">=18.0.0" 2499 - }, 2500 - "optionalDependencies": { 2501 - "fsevents": "~2.3.2", 2502 - "sharp": "^0.33.5" 2503 - }, 2504 - "peerDependencies": { 2505 - "@cloudflare/workers-types": "^4.20250428.0" 2506 - }, 2507 - "peerDependenciesMeta": { 2508 - "@cloudflare/workers-types": { 2509 - "optional": true 2510 - } 2511 - } 2512 - }, 2513 - "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { 2514 - "version": "0.25.2", 2515 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", 2516 - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", 2517 - "cpu": [ 2518 - "ppc64" 2519 - ], 2520 - "dev": true, 2521 - "license": "MIT", 2522 - "optional": true, 2523 - "os": [ 2524 - "aix" 2525 - ], 2526 - "engines": { 2527 - "node": ">=18" 2528 - } 2529 - }, 2530 - "node_modules/wrangler/node_modules/@esbuild/android-arm": { 2531 - "version": "0.25.2", 2532 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", 2533 - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", 2534 - "cpu": [ 2535 - "arm" 2536 - ], 2537 - "dev": true, 2538 - "license": "MIT", 2539 - "optional": true, 2540 - "os": [ 2541 - "android" 2542 - ], 2543 - "engines": { 2544 - "node": ">=18" 2545 - } 2546 - }, 2547 - "node_modules/wrangler/node_modules/@esbuild/android-arm64": { 2548 - "version": "0.25.2", 2549 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", 2550 - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", 2551 - "cpu": [ 2552 - "arm64" 2553 - ], 2554 - "dev": true, 2555 - "license": "MIT", 2556 - "optional": true, 2557 - "os": [ 2558 - "android" 2559 - ], 2560 - "engines": { 2561 - "node": ">=18" 2562 - } 2563 - }, 2564 - "node_modules/wrangler/node_modules/@esbuild/android-x64": { 2565 - "version": "0.25.2", 2566 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", 2567 - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", 2568 - "cpu": [ 2569 - "x64" 2570 - ], 2571 - "dev": true, 2572 - "license": "MIT", 2573 - "optional": true, 2574 - "os": [ 2575 - "android" 2576 - ], 2577 - "engines": { 2578 - "node": ">=18" 2579 - } 2580 - }, 2581 - "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { 2582 - "version": "0.25.2", 2583 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", 2584 - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", 2585 - "cpu": [ 2586 - "arm64" 2587 - ], 2588 - "dev": true, 2589 - "license": "MIT", 2590 - "optional": true, 2591 - "os": [ 2592 - "darwin" 2593 - ], 2594 - "engines": { 2595 - "node": ">=18" 2596 - } 2597 - }, 2598 - "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { 2599 - "version": "0.25.2", 2600 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", 2601 - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", 2602 - "cpu": [ 2603 - "x64" 2604 - ], 2605 - "dev": true, 2606 - "license": "MIT", 2607 - "optional": true, 2608 - "os": [ 2609 - "darwin" 2610 - ], 2611 - "engines": { 2612 - "node": ">=18" 2613 - } 2614 - }, 2615 - "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { 2616 - "version": "0.25.2", 2617 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", 2618 - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", 2619 - "cpu": [ 2620 - "arm64" 2621 - ], 2622 - "dev": true, 2623 - "license": "MIT", 2624 - "optional": true, 2625 - "os": [ 2626 - "freebsd" 2627 - ], 2628 - "engines": { 2629 - "node": ">=18" 2630 - } 2631 - }, 2632 - "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { 2633 - "version": "0.25.2", 2634 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", 2635 - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", 2636 - "cpu": [ 2637 - "x64" 2638 - ], 2639 - "dev": true, 2640 - "license": "MIT", 2641 - "optional": true, 2642 - "os": [ 2643 - "freebsd" 2644 - ], 2645 - "engines": { 2646 - "node": ">=18" 2647 - } 2648 - }, 2649 - "node_modules/wrangler/node_modules/@esbuild/linux-arm": { 2650 - "version": "0.25.2", 2651 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", 2652 - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", 2653 - "cpu": [ 2654 - "arm" 2655 - ], 2656 - "dev": true, 2657 - "license": "MIT", 2658 - "optional": true, 2659 - "os": [ 2660 - "linux" 2661 - ], 2662 - "engines": { 2663 - "node": ">=18" 2664 - } 2665 - }, 2666 - "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { 2667 - "version": "0.25.2", 2668 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", 2669 - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", 2670 - "cpu": [ 2671 - "arm64" 2672 - ], 2673 - "dev": true, 2674 - "license": "MIT", 2675 - "optional": true, 2676 - "os": [ 2677 - "linux" 2678 - ], 2679 - "engines": { 2680 - "node": ">=18" 2681 - } 2682 - }, 2683 - "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { 2684 - "version": "0.25.2", 2685 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", 2686 - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", 2687 - "cpu": [ 2688 - "ia32" 2689 - ], 2690 - "dev": true, 2691 - "license": "MIT", 2692 - "optional": true, 2693 - "os": [ 2694 - "linux" 2695 - ], 2696 - "engines": { 2697 - "node": ">=18" 2698 - } 2699 - }, 2700 - "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { 2701 - "version": "0.25.2", 2702 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", 2703 - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", 2704 - "cpu": [ 2705 - "loong64" 2706 - ], 2707 - "dev": true, 2708 - "license": "MIT", 2709 - "optional": true, 2710 - "os": [ 2711 - "linux" 2712 - ], 2713 - "engines": { 2714 - "node": ">=18" 2715 - } 2716 - }, 2717 - "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { 2718 - "version": "0.25.2", 2719 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", 2720 - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", 2721 - "cpu": [ 2722 - "mips64el" 2723 - ], 2724 - "dev": true, 2725 - "license": "MIT", 2726 - "optional": true, 2727 - "os": [ 2728 - "linux" 2729 - ], 2730 - "engines": { 2731 - "node": ">=18" 2732 - } 2733 - }, 2734 - "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { 2735 - "version": "0.25.2", 2736 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", 2737 - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", 2738 - "cpu": [ 2739 - "ppc64" 2740 - ], 2741 - "dev": true, 2742 - "license": "MIT", 2743 - "optional": true, 2744 - "os": [ 2745 - "linux" 2746 - ], 2747 - "engines": { 2748 - "node": ">=18" 2749 - } 2750 - }, 2751 - "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { 2752 - "version": "0.25.2", 2753 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", 2754 - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", 2755 - "cpu": [ 2756 - "riscv64" 2757 - ], 2758 - "dev": true, 2759 - "license": "MIT", 2760 - "optional": true, 2761 - "os": [ 2762 - "linux" 2763 - ], 2764 - "engines": { 2765 - "node": ">=18" 2766 - } 2767 - }, 2768 - "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { 2769 - "version": "0.25.2", 2770 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", 2771 - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", 2772 - "cpu": [ 2773 - "s390x" 2774 - ], 2775 - "dev": true, 2776 - "license": "MIT", 2777 - "optional": true, 2778 - "os": [ 2779 - "linux" 2780 - ], 2781 - "engines": { 2782 - "node": ">=18" 2783 - } 2784 - }, 2785 - "node_modules/wrangler/node_modules/@esbuild/linux-x64": { 2786 - "version": "0.25.2", 2787 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", 2788 - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", 2789 - "cpu": [ 2790 - "x64" 2791 - ], 2792 - "dev": true, 2793 - "license": "MIT", 2794 - "optional": true, 2795 - "os": [ 2796 - "linux" 2797 - ], 2798 - "engines": { 2799 - "node": ">=18" 2800 - } 2801 - }, 2802 - "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { 2803 - "version": "0.25.2", 2804 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", 2805 - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", 2806 - "cpu": [ 2807 - "arm64" 2808 - ], 2809 - "dev": true, 2810 - "license": "MIT", 2811 - "optional": true, 2812 - "os": [ 2813 - "netbsd" 2814 - ], 2815 - "engines": { 2816 - "node": ">=18" 2817 - } 2818 - }, 2819 - "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { 2820 - "version": "0.25.2", 2821 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", 2822 - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", 2823 - "cpu": [ 2824 - "x64" 2825 - ], 2826 - "dev": true, 2827 - "license": "MIT", 2828 - "optional": true, 2829 - "os": [ 2830 - "netbsd" 2831 - ], 2832 - "engines": { 2833 - "node": ">=18" 2834 - } 2835 - }, 2836 - "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { 2837 - "version": "0.25.2", 2838 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", 2839 - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", 2840 - "cpu": [ 2841 - "arm64" 2842 - ], 2843 - "dev": true, 2844 - "license": "MIT", 2845 - "optional": true, 2846 - "os": [ 2847 - "openbsd" 2848 - ], 2849 - "engines": { 2850 - "node": ">=18" 2851 - } 2852 - }, 2853 - "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { 2854 - "version": "0.25.2", 2855 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", 2856 - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", 2857 - "cpu": [ 2858 - "x64" 2859 - ], 2860 - "dev": true, 2861 - "license": "MIT", 2862 - "optional": true, 2863 - "os": [ 2864 - "openbsd" 2865 - ], 2866 - "engines": { 2867 - "node": ">=18" 2868 - } 2869 - }, 2870 - "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { 2871 - "version": "0.25.2", 2872 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", 2873 - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", 2874 - "cpu": [ 2875 - "x64" 2876 - ], 2877 - "dev": true, 2878 - "license": "MIT", 2879 - "optional": true, 2880 - "os": [ 2881 - "sunos" 2882 - ], 2883 - "engines": { 2884 - "node": ">=18" 2885 - } 2886 - }, 2887 - "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { 2888 - "version": "0.25.2", 2889 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", 2890 - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", 2891 - "cpu": [ 2892 - "arm64" 2893 - ], 2894 - "dev": true, 2895 - "license": "MIT", 2896 - "optional": true, 2897 - "os": [ 2898 - "win32" 2899 - ], 2900 - "engines": { 2901 - "node": ">=18" 2902 - } 2903 - }, 2904 - "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { 2905 - "version": "0.25.2", 2906 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", 2907 - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", 2908 - "cpu": [ 2909 - "ia32" 2910 - ], 2911 - "dev": true, 2912 - "license": "MIT", 2913 - "optional": true, 2914 - "os": [ 2915 - "win32" 2916 - ], 2917 - "engines": { 2918 - "node": ">=18" 2919 - } 2920 - }, 2921 - "node_modules/wrangler/node_modules/@esbuild/win32-x64": { 2922 - "version": "0.25.2", 2923 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", 2924 - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", 2925 - "cpu": [ 2926 - "x64" 2927 - ], 2928 - "dev": true, 2929 - "license": "MIT", 2930 - "optional": true, 2931 - "os": [ 2932 - "win32" 2933 - ], 2934 - "engines": { 2935 - "node": ">=18" 2936 - } 2937 - }, 2938 - "node_modules/wrangler/node_modules/esbuild": { 2939 - "version": "0.25.2", 2940 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", 2941 - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 2942 - "dev": true, 2943 - "hasInstallScript": true, 2944 - "license": "MIT", 2945 - "bin": { 2946 - "esbuild": "bin/esbuild" 2947 - }, 2948 - "engines": { 2949 - "node": ">=18" 2950 - }, 2951 - "optionalDependencies": { 2952 - "@esbuild/aix-ppc64": "0.25.2", 2953 - "@esbuild/android-arm": "0.25.2", 2954 - "@esbuild/android-arm64": "0.25.2", 2955 - "@esbuild/android-x64": "0.25.2", 2956 - "@esbuild/darwin-arm64": "0.25.2", 2957 - "@esbuild/darwin-x64": "0.25.2", 2958 - "@esbuild/freebsd-arm64": "0.25.2", 2959 - "@esbuild/freebsd-x64": "0.25.2", 2960 - "@esbuild/linux-arm": "0.25.2", 2961 - "@esbuild/linux-arm64": "0.25.2", 2962 - "@esbuild/linux-ia32": "0.25.2", 2963 - "@esbuild/linux-loong64": "0.25.2", 2964 - "@esbuild/linux-mips64el": "0.25.2", 2965 - "@esbuild/linux-ppc64": "0.25.2", 2966 - "@esbuild/linux-riscv64": "0.25.2", 2967 - "@esbuild/linux-s390x": "0.25.2", 2968 - "@esbuild/linux-x64": "0.25.2", 2969 - "@esbuild/netbsd-arm64": "0.25.2", 2970 - "@esbuild/netbsd-x64": "0.25.2", 2971 - "@esbuild/openbsd-arm64": "0.25.2", 2972 - "@esbuild/openbsd-x64": "0.25.2", 2973 - "@esbuild/sunos-x64": "0.25.2", 2974 - "@esbuild/win32-arm64": "0.25.2", 2975 - "@esbuild/win32-ia32": "0.25.2", 2976 - "@esbuild/win32-x64": "0.25.2" 2977 - } 2978 - }, 2979 - "node_modules/ws": { 2980 - "version": "8.18.0", 2981 - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 2982 - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 2983 - "dev": true, 2984 - "license": "MIT", 2985 - "engines": { 2986 - "node": ">=10.0.0" 2987 - }, 2988 - "peerDependencies": { 2989 - "bufferutil": "^4.0.1", 2990 - "utf-8-validate": ">=5.0.2" 2991 - }, 2992 - "peerDependenciesMeta": { 2993 - "bufferutil": { 2994 - "optional": true 2995 - }, 2996 - "utf-8-validate": { 2997 - "optional": true 2998 - } 2999 - } 3000 - }, 3001 - "node_modules/youch": { 3002 - "version": "3.3.4", 3003 - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", 3004 - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", 3005 - "dev": true, 3006 - "license": "MIT", 3007 - "dependencies": { 3008 - "cookie": "^0.7.1", 3009 - "mustache": "^4.2.0", 3010 - "stacktracey": "^2.1.8" 3011 - } 3012 - }, 3013 - "node_modules/zod": { 3014 - "version": "3.24.3", 3015 - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", 3016 - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", 3017 - "dev": true, 3018 - "license": "MIT", 3019 - "funding": { 3020 - "url": "https://github.com/sponsors/colinhacks" 3021 - } 3022 - } 3023 - } 2 + "name": "avatar", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "avatar", 9 + "version": "0.0.0", 10 + "dependencies": { 11 + "@atcute/identity-resolver": "^1.2.2" 12 + }, 13 + "devDependencies": { 14 + "@cloudflare/vitest-pool-workers": "^0.8.19", 15 + "vitest": "~3.0.7", 16 + "wrangler": "^4.14.1" 17 + } 18 + }, 19 + "node_modules/@atcute/identity": { 20 + "version": "1.1.3", 21 + "resolved": "https://registry.npmjs.org/@atcute/identity/-/identity-1.1.3.tgz", 22 + "integrity": "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng==", 23 + "license": "0BSD", 24 + "peer": true, 25 + "dependencies": { 26 + "@atcute/lexicons": "^1.2.4", 27 + "@badrap/valita": "^0.4.6" 28 + } 29 + }, 30 + "node_modules/@atcute/identity-resolver": { 31 + "version": "1.2.2", 32 + "resolved": "https://registry.npmjs.org/@atcute/identity-resolver/-/identity-resolver-1.2.2.tgz", 33 + "integrity": "sha512-eUh/UH4bFvuXS0X7epYCeJC/kj4rbBXfSRumLEH4smMVwNOgTo7cL/0Srty+P/qVPoZEyXdfEbS0PHJyzoXmHw==", 34 + "license": "0BSD", 35 + "dependencies": { 36 + "@atcute/lexicons": "^1.2.6", 37 + "@atcute/util-fetch": "^1.0.5", 38 + "@badrap/valita": "^0.4.6" 39 + }, 40 + "peerDependencies": { 41 + "@atcute/identity": "^1.0.0" 42 + } 43 + }, 44 + "node_modules/@atcute/lexicons": { 45 + "version": "1.2.6", 46 + "resolved": "https://registry.npmjs.org/@atcute/lexicons/-/lexicons-1.2.6.tgz", 47 + "integrity": "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA==", 48 + "license": "0BSD", 49 + "dependencies": { 50 + "@atcute/uint8array": "^1.0.6", 51 + "@atcute/util-text": "^0.0.1", 52 + "@standard-schema/spec": "^1.1.0", 53 + "esm-env": "^1.2.2" 54 + } 55 + }, 56 + "node_modules/@atcute/uint8array": { 57 + "version": "1.0.6", 58 + "resolved": "https://registry.npmjs.org/@atcute/uint8array/-/uint8array-1.0.6.tgz", 59 + "integrity": "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A==", 60 + "license": "0BSD" 61 + }, 62 + "node_modules/@atcute/util-fetch": { 63 + "version": "1.0.5", 64 + "resolved": "https://registry.npmjs.org/@atcute/util-fetch/-/util-fetch-1.0.5.tgz", 65 + "integrity": "sha512-qjHj01BGxjSjIFdPiAjSARnodJIIyKxnCMMEcXMESo9TAyND6XZQqrie5fia+LlYWVXdpsTds8uFQwc9jdKTig==", 66 + "license": "0BSD", 67 + "dependencies": { 68 + "@badrap/valita": "^0.4.6" 69 + } 70 + }, 71 + "node_modules/@atcute/util-text": { 72 + "version": "0.0.1", 73 + "resolved": "https://registry.npmjs.org/@atcute/util-text/-/util-text-0.0.1.tgz", 74 + "integrity": "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g==", 75 + "license": "0BSD", 76 + "dependencies": { 77 + "unicode-segmenter": "^0.14.4" 78 + } 79 + }, 80 + "node_modules/@badrap/valita": { 81 + "version": "0.4.6", 82 + "resolved": "https://registry.npmjs.org/@badrap/valita/-/valita-0.4.6.tgz", 83 + "integrity": "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==", 84 + "license": "MIT", 85 + "engines": { 86 + "node": ">= 18" 87 + } 88 + }, 89 + "node_modules/@cloudflare/kv-asset-handler": { 90 + "version": "0.4.0", 91 + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", 92 + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", 93 + "dev": true, 94 + "license": "MIT OR Apache-2.0", 95 + "dependencies": { 96 + "mime": "^3.0.0" 97 + }, 98 + "engines": { 99 + "node": ">=18.0.0" 100 + } 101 + }, 102 + "node_modules/@cloudflare/unenv-preset": { 103 + "version": "2.3.1", 104 + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz", 105 + "integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==", 106 + "dev": true, 107 + "license": "MIT OR Apache-2.0", 108 + "peerDependencies": { 109 + "unenv": "2.0.0-rc.15", 110 + "workerd": "^1.20250320.0" 111 + }, 112 + "peerDependenciesMeta": { 113 + "workerd": { 114 + "optional": true 115 + } 116 + } 117 + }, 118 + "node_modules/@cloudflare/vitest-pool-workers": { 119 + "version": "0.8.24", 120 + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.24.tgz", 121 + "integrity": "sha512-wT2PABJQ9YLYWrVu4CRZOjvmjHkdbMyLTZPU9n/7JEMM3pgG8dY41F1Rj31UsXRQaXX39A/CTPGlk58dcMUysA==", 122 + "dev": true, 123 + "license": "MIT", 124 + "dependencies": { 125 + "birpc": "0.2.14", 126 + "cjs-module-lexer": "^1.2.3", 127 + "devalue": "^4.3.0", 128 + "miniflare": "4.20250428.1", 129 + "semver": "^7.7.1", 130 + "wrangler": "4.14.1", 131 + "zod": "^3.22.3" 132 + }, 133 + "peerDependencies": { 134 + "@vitest/runner": "2.0.x - 3.1.x", 135 + "@vitest/snapshot": "2.0.x - 3.1.x", 136 + "vitest": "2.0.x - 3.1.x" 137 + } 138 + }, 139 + "node_modules/@cloudflare/workerd-darwin-64": { 140 + "version": "1.20250428.0", 141 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250428.0.tgz", 142 + "integrity": "sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==", 143 + "cpu": [ 144 + "x64" 145 + ], 146 + "dev": true, 147 + "license": "Apache-2.0", 148 + "optional": true, 149 + "os": [ 150 + "darwin" 151 + ], 152 + "engines": { 153 + "node": ">=16" 154 + } 155 + }, 156 + "node_modules/@cloudflare/workerd-darwin-arm64": { 157 + "version": "1.20250428.0", 158 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250428.0.tgz", 159 + "integrity": "sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==", 160 + "cpu": [ 161 + "arm64" 162 + ], 163 + "dev": true, 164 + "license": "Apache-2.0", 165 + "optional": true, 166 + "os": [ 167 + "darwin" 168 + ], 169 + "engines": { 170 + "node": ">=16" 171 + } 172 + }, 173 + "node_modules/@cloudflare/workerd-linux-64": { 174 + "version": "1.20250428.0", 175 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250428.0.tgz", 176 + "integrity": "sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==", 177 + "cpu": [ 178 + "x64" 179 + ], 180 + "dev": true, 181 + "license": "Apache-2.0", 182 + "optional": true, 183 + "os": [ 184 + "linux" 185 + ], 186 + "engines": { 187 + "node": ">=16" 188 + } 189 + }, 190 + "node_modules/@cloudflare/workerd-linux-arm64": { 191 + "version": "1.20250428.0", 192 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250428.0.tgz", 193 + "integrity": "sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==", 194 + "cpu": [ 195 + "arm64" 196 + ], 197 + "dev": true, 198 + "license": "Apache-2.0", 199 + "optional": true, 200 + "os": [ 201 + "linux" 202 + ], 203 + "engines": { 204 + "node": ">=16" 205 + } 206 + }, 207 + "node_modules/@cloudflare/workerd-windows-64": { 208 + "version": "1.20250428.0", 209 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250428.0.tgz", 210 + "integrity": "sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==", 211 + "cpu": [ 212 + "x64" 213 + ], 214 + "dev": true, 215 + "license": "Apache-2.0", 216 + "optional": true, 217 + "os": [ 218 + "win32" 219 + ], 220 + "engines": { 221 + "node": ">=16" 222 + } 223 + }, 224 + "node_modules/@cspotcode/source-map-support": { 225 + "version": "0.8.1", 226 + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 227 + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 228 + "dev": true, 229 + "license": "MIT", 230 + "dependencies": { 231 + "@jridgewell/trace-mapping": "0.3.9" 232 + }, 233 + "engines": { 234 + "node": ">=12" 235 + } 236 + }, 237 + "node_modules/@emnapi/runtime": { 238 + "version": "1.4.3", 239 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", 240 + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", 241 + "dev": true, 242 + "license": "MIT", 243 + "optional": true, 244 + "dependencies": { 245 + "tslib": "^2.4.0" 246 + } 247 + }, 248 + "node_modules/@esbuild/aix-ppc64": { 249 + "version": "0.25.3", 250 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", 251 + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", 252 + "cpu": [ 253 + "ppc64" 254 + ], 255 + "dev": true, 256 + "license": "MIT", 257 + "optional": true, 258 + "os": [ 259 + "aix" 260 + ], 261 + "engines": { 262 + "node": ">=18" 263 + } 264 + }, 265 + "node_modules/@esbuild/android-arm": { 266 + "version": "0.25.3", 267 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", 268 + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", 269 + "cpu": [ 270 + "arm" 271 + ], 272 + "dev": true, 273 + "license": "MIT", 274 + "optional": true, 275 + "os": [ 276 + "android" 277 + ], 278 + "engines": { 279 + "node": ">=18" 280 + } 281 + }, 282 + "node_modules/@esbuild/android-arm64": { 283 + "version": "0.25.3", 284 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", 285 + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", 286 + "cpu": [ 287 + "arm64" 288 + ], 289 + "dev": true, 290 + "license": "MIT", 291 + "optional": true, 292 + "os": [ 293 + "android" 294 + ], 295 + "engines": { 296 + "node": ">=18" 297 + } 298 + }, 299 + "node_modules/@esbuild/android-x64": { 300 + "version": "0.25.3", 301 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", 302 + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", 303 + "cpu": [ 304 + "x64" 305 + ], 306 + "dev": true, 307 + "license": "MIT", 308 + "optional": true, 309 + "os": [ 310 + "android" 311 + ], 312 + "engines": { 313 + "node": ">=18" 314 + } 315 + }, 316 + "node_modules/@esbuild/darwin-arm64": { 317 + "version": "0.25.3", 318 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", 319 + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", 320 + "cpu": [ 321 + "arm64" 322 + ], 323 + "dev": true, 324 + "license": "MIT", 325 + "optional": true, 326 + "os": [ 327 + "darwin" 328 + ], 329 + "engines": { 330 + "node": ">=18" 331 + } 332 + }, 333 + "node_modules/@esbuild/darwin-x64": { 334 + "version": "0.25.3", 335 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", 336 + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", 337 + "cpu": [ 338 + "x64" 339 + ], 340 + "dev": true, 341 + "license": "MIT", 342 + "optional": true, 343 + "os": [ 344 + "darwin" 345 + ], 346 + "engines": { 347 + "node": ">=18" 348 + } 349 + }, 350 + "node_modules/@esbuild/freebsd-arm64": { 351 + "version": "0.25.3", 352 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", 353 + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", 354 + "cpu": [ 355 + "arm64" 356 + ], 357 + "dev": true, 358 + "license": "MIT", 359 + "optional": true, 360 + "os": [ 361 + "freebsd" 362 + ], 363 + "engines": { 364 + "node": ">=18" 365 + } 366 + }, 367 + "node_modules/@esbuild/freebsd-x64": { 368 + "version": "0.25.3", 369 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", 370 + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", 371 + "cpu": [ 372 + "x64" 373 + ], 374 + "dev": true, 375 + "license": "MIT", 376 + "optional": true, 377 + "os": [ 378 + "freebsd" 379 + ], 380 + "engines": { 381 + "node": ">=18" 382 + } 383 + }, 384 + "node_modules/@esbuild/linux-arm": { 385 + "version": "0.25.3", 386 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", 387 + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", 388 + "cpu": [ 389 + "arm" 390 + ], 391 + "dev": true, 392 + "license": "MIT", 393 + "optional": true, 394 + "os": [ 395 + "linux" 396 + ], 397 + "engines": { 398 + "node": ">=18" 399 + } 400 + }, 401 + "node_modules/@esbuild/linux-arm64": { 402 + "version": "0.25.3", 403 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", 404 + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", 405 + "cpu": [ 406 + "arm64" 407 + ], 408 + "dev": true, 409 + "license": "MIT", 410 + "optional": true, 411 + "os": [ 412 + "linux" 413 + ], 414 + "engines": { 415 + "node": ">=18" 416 + } 417 + }, 418 + "node_modules/@esbuild/linux-ia32": { 419 + "version": "0.25.3", 420 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", 421 + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", 422 + "cpu": [ 423 + "ia32" 424 + ], 425 + "dev": true, 426 + "license": "MIT", 427 + "optional": true, 428 + "os": [ 429 + "linux" 430 + ], 431 + "engines": { 432 + "node": ">=18" 433 + } 434 + }, 435 + "node_modules/@esbuild/linux-loong64": { 436 + "version": "0.25.3", 437 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", 438 + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", 439 + "cpu": [ 440 + "loong64" 441 + ], 442 + "dev": true, 443 + "license": "MIT", 444 + "optional": true, 445 + "os": [ 446 + "linux" 447 + ], 448 + "engines": { 449 + "node": ">=18" 450 + } 451 + }, 452 + "node_modules/@esbuild/linux-mips64el": { 453 + "version": "0.25.3", 454 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", 455 + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", 456 + "cpu": [ 457 + "mips64el" 458 + ], 459 + "dev": true, 460 + "license": "MIT", 461 + "optional": true, 462 + "os": [ 463 + "linux" 464 + ], 465 + "engines": { 466 + "node": ">=18" 467 + } 468 + }, 469 + "node_modules/@esbuild/linux-ppc64": { 470 + "version": "0.25.3", 471 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", 472 + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", 473 + "cpu": [ 474 + "ppc64" 475 + ], 476 + "dev": true, 477 + "license": "MIT", 478 + "optional": true, 479 + "os": [ 480 + "linux" 481 + ], 482 + "engines": { 483 + "node": ">=18" 484 + } 485 + }, 486 + "node_modules/@esbuild/linux-riscv64": { 487 + "version": "0.25.3", 488 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", 489 + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", 490 + "cpu": [ 491 + "riscv64" 492 + ], 493 + "dev": true, 494 + "license": "MIT", 495 + "optional": true, 496 + "os": [ 497 + "linux" 498 + ], 499 + "engines": { 500 + "node": ">=18" 501 + } 502 + }, 503 + "node_modules/@esbuild/linux-s390x": { 504 + "version": "0.25.3", 505 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", 506 + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", 507 + "cpu": [ 508 + "s390x" 509 + ], 510 + "dev": true, 511 + "license": "MIT", 512 + "optional": true, 513 + "os": [ 514 + "linux" 515 + ], 516 + "engines": { 517 + "node": ">=18" 518 + } 519 + }, 520 + "node_modules/@esbuild/linux-x64": { 521 + "version": "0.25.3", 522 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", 523 + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", 524 + "cpu": [ 525 + "x64" 526 + ], 527 + "dev": true, 528 + "license": "MIT", 529 + "optional": true, 530 + "os": [ 531 + "linux" 532 + ], 533 + "engines": { 534 + "node": ">=18" 535 + } 536 + }, 537 + "node_modules/@esbuild/netbsd-arm64": { 538 + "version": "0.25.3", 539 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", 540 + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", 541 + "cpu": [ 542 + "arm64" 543 + ], 544 + "dev": true, 545 + "license": "MIT", 546 + "optional": true, 547 + "os": [ 548 + "netbsd" 549 + ], 550 + "engines": { 551 + "node": ">=18" 552 + } 553 + }, 554 + "node_modules/@esbuild/netbsd-x64": { 555 + "version": "0.25.3", 556 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", 557 + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", 558 + "cpu": [ 559 + "x64" 560 + ], 561 + "dev": true, 562 + "license": "MIT", 563 + "optional": true, 564 + "os": [ 565 + "netbsd" 566 + ], 567 + "engines": { 568 + "node": ">=18" 569 + } 570 + }, 571 + "node_modules/@esbuild/openbsd-arm64": { 572 + "version": "0.25.3", 573 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", 574 + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", 575 + "cpu": [ 576 + "arm64" 577 + ], 578 + "dev": true, 579 + "license": "MIT", 580 + "optional": true, 581 + "os": [ 582 + "openbsd" 583 + ], 584 + "engines": { 585 + "node": ">=18" 586 + } 587 + }, 588 + "node_modules/@esbuild/openbsd-x64": { 589 + "version": "0.25.3", 590 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", 591 + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", 592 + "cpu": [ 593 + "x64" 594 + ], 595 + "dev": true, 596 + "license": "MIT", 597 + "optional": true, 598 + "os": [ 599 + "openbsd" 600 + ], 601 + "engines": { 602 + "node": ">=18" 603 + } 604 + }, 605 + "node_modules/@esbuild/sunos-x64": { 606 + "version": "0.25.3", 607 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", 608 + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", 609 + "cpu": [ 610 + "x64" 611 + ], 612 + "dev": true, 613 + "license": "MIT", 614 + "optional": true, 615 + "os": [ 616 + "sunos" 617 + ], 618 + "engines": { 619 + "node": ">=18" 620 + } 621 + }, 622 + "node_modules/@esbuild/win32-arm64": { 623 + "version": "0.25.3", 624 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", 625 + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", 626 + "cpu": [ 627 + "arm64" 628 + ], 629 + "dev": true, 630 + "license": "MIT", 631 + "optional": true, 632 + "os": [ 633 + "win32" 634 + ], 635 + "engines": { 636 + "node": ">=18" 637 + } 638 + }, 639 + "node_modules/@esbuild/win32-ia32": { 640 + "version": "0.25.3", 641 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", 642 + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", 643 + "cpu": [ 644 + "ia32" 645 + ], 646 + "dev": true, 647 + "license": "MIT", 648 + "optional": true, 649 + "os": [ 650 + "win32" 651 + ], 652 + "engines": { 653 + "node": ">=18" 654 + } 655 + }, 656 + "node_modules/@esbuild/win32-x64": { 657 + "version": "0.25.3", 658 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", 659 + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", 660 + "cpu": [ 661 + "x64" 662 + ], 663 + "dev": true, 664 + "license": "MIT", 665 + "optional": true, 666 + "os": [ 667 + "win32" 668 + ], 669 + "engines": { 670 + "node": ">=18" 671 + } 672 + }, 673 + "node_modules/@fastify/busboy": { 674 + "version": "2.1.1", 675 + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 676 + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 677 + "dev": true, 678 + "license": "MIT", 679 + "engines": { 680 + "node": ">=14" 681 + } 682 + }, 683 + "node_modules/@img/sharp-darwin-arm64": { 684 + "version": "0.33.5", 685 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", 686 + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", 687 + "cpu": [ 688 + "arm64" 689 + ], 690 + "dev": true, 691 + "license": "Apache-2.0", 692 + "optional": true, 693 + "os": [ 694 + "darwin" 695 + ], 696 + "engines": { 697 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 698 + }, 699 + "funding": { 700 + "url": "https://opencollective.com/libvips" 701 + }, 702 + "optionalDependencies": { 703 + "@img/sharp-libvips-darwin-arm64": "1.0.4" 704 + } 705 + }, 706 + "node_modules/@img/sharp-darwin-x64": { 707 + "version": "0.33.5", 708 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", 709 + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", 710 + "cpu": [ 711 + "x64" 712 + ], 713 + "dev": true, 714 + "license": "Apache-2.0", 715 + "optional": true, 716 + "os": [ 717 + "darwin" 718 + ], 719 + "engines": { 720 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 721 + }, 722 + "funding": { 723 + "url": "https://opencollective.com/libvips" 724 + }, 725 + "optionalDependencies": { 726 + "@img/sharp-libvips-darwin-x64": "1.0.4" 727 + } 728 + }, 729 + "node_modules/@img/sharp-libvips-darwin-arm64": { 730 + "version": "1.0.4", 731 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", 732 + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", 733 + "cpu": [ 734 + "arm64" 735 + ], 736 + "dev": true, 737 + "license": "LGPL-3.0-or-later", 738 + "optional": true, 739 + "os": [ 740 + "darwin" 741 + ], 742 + "funding": { 743 + "url": "https://opencollective.com/libvips" 744 + } 745 + }, 746 + "node_modules/@img/sharp-libvips-darwin-x64": { 747 + "version": "1.0.4", 748 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", 749 + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", 750 + "cpu": [ 751 + "x64" 752 + ], 753 + "dev": true, 754 + "license": "LGPL-3.0-or-later", 755 + "optional": true, 756 + "os": [ 757 + "darwin" 758 + ], 759 + "funding": { 760 + "url": "https://opencollective.com/libvips" 761 + } 762 + }, 763 + "node_modules/@img/sharp-libvips-linux-arm": { 764 + "version": "1.0.5", 765 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", 766 + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", 767 + "cpu": [ 768 + "arm" 769 + ], 770 + "dev": true, 771 + "license": "LGPL-3.0-or-later", 772 + "optional": true, 773 + "os": [ 774 + "linux" 775 + ], 776 + "funding": { 777 + "url": "https://opencollective.com/libvips" 778 + } 779 + }, 780 + "node_modules/@img/sharp-libvips-linux-arm64": { 781 + "version": "1.0.4", 782 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", 783 + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", 784 + "cpu": [ 785 + "arm64" 786 + ], 787 + "dev": true, 788 + "license": "LGPL-3.0-or-later", 789 + "optional": true, 790 + "os": [ 791 + "linux" 792 + ], 793 + "funding": { 794 + "url": "https://opencollective.com/libvips" 795 + } 796 + }, 797 + "node_modules/@img/sharp-libvips-linux-s390x": { 798 + "version": "1.0.4", 799 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", 800 + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", 801 + "cpu": [ 802 + "s390x" 803 + ], 804 + "dev": true, 805 + "license": "LGPL-3.0-or-later", 806 + "optional": true, 807 + "os": [ 808 + "linux" 809 + ], 810 + "funding": { 811 + "url": "https://opencollective.com/libvips" 812 + } 813 + }, 814 + "node_modules/@img/sharp-libvips-linux-x64": { 815 + "version": "1.0.4", 816 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", 817 + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", 818 + "cpu": [ 819 + "x64" 820 + ], 821 + "dev": true, 822 + "license": "LGPL-3.0-or-later", 823 + "optional": true, 824 + "os": [ 825 + "linux" 826 + ], 827 + "funding": { 828 + "url": "https://opencollective.com/libvips" 829 + } 830 + }, 831 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 832 + "version": "1.0.4", 833 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", 834 + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", 835 + "cpu": [ 836 + "arm64" 837 + ], 838 + "dev": true, 839 + "license": "LGPL-3.0-or-later", 840 + "optional": true, 841 + "os": [ 842 + "linux" 843 + ], 844 + "funding": { 845 + "url": "https://opencollective.com/libvips" 846 + } 847 + }, 848 + "node_modules/@img/sharp-libvips-linuxmusl-x64": { 849 + "version": "1.0.4", 850 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", 851 + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", 852 + "cpu": [ 853 + "x64" 854 + ], 855 + "dev": true, 856 + "license": "LGPL-3.0-or-later", 857 + "optional": true, 858 + "os": [ 859 + "linux" 860 + ], 861 + "funding": { 862 + "url": "https://opencollective.com/libvips" 863 + } 864 + }, 865 + "node_modules/@img/sharp-linux-arm": { 866 + "version": "0.33.5", 867 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", 868 + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", 869 + "cpu": [ 870 + "arm" 871 + ], 872 + "dev": true, 873 + "license": "Apache-2.0", 874 + "optional": true, 875 + "os": [ 876 + "linux" 877 + ], 878 + "engines": { 879 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 880 + }, 881 + "funding": { 882 + "url": "https://opencollective.com/libvips" 883 + }, 884 + "optionalDependencies": { 885 + "@img/sharp-libvips-linux-arm": "1.0.5" 886 + } 887 + }, 888 + "node_modules/@img/sharp-linux-arm64": { 889 + "version": "0.33.5", 890 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", 891 + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", 892 + "cpu": [ 893 + "arm64" 894 + ], 895 + "dev": true, 896 + "license": "Apache-2.0", 897 + "optional": true, 898 + "os": [ 899 + "linux" 900 + ], 901 + "engines": { 902 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 903 + }, 904 + "funding": { 905 + "url": "https://opencollective.com/libvips" 906 + }, 907 + "optionalDependencies": { 908 + "@img/sharp-libvips-linux-arm64": "1.0.4" 909 + } 910 + }, 911 + "node_modules/@img/sharp-linux-s390x": { 912 + "version": "0.33.5", 913 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", 914 + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", 915 + "cpu": [ 916 + "s390x" 917 + ], 918 + "dev": true, 919 + "license": "Apache-2.0", 920 + "optional": true, 921 + "os": [ 922 + "linux" 923 + ], 924 + "engines": { 925 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 926 + }, 927 + "funding": { 928 + "url": "https://opencollective.com/libvips" 929 + }, 930 + "optionalDependencies": { 931 + "@img/sharp-libvips-linux-s390x": "1.0.4" 932 + } 933 + }, 934 + "node_modules/@img/sharp-linux-x64": { 935 + "version": "0.33.5", 936 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", 937 + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", 938 + "cpu": [ 939 + "x64" 940 + ], 941 + "dev": true, 942 + "license": "Apache-2.0", 943 + "optional": true, 944 + "os": [ 945 + "linux" 946 + ], 947 + "engines": { 948 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 949 + }, 950 + "funding": { 951 + "url": "https://opencollective.com/libvips" 952 + }, 953 + "optionalDependencies": { 954 + "@img/sharp-libvips-linux-x64": "1.0.4" 955 + } 956 + }, 957 + "node_modules/@img/sharp-linuxmusl-arm64": { 958 + "version": "0.33.5", 959 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", 960 + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", 961 + "cpu": [ 962 + "arm64" 963 + ], 964 + "dev": true, 965 + "license": "Apache-2.0", 966 + "optional": true, 967 + "os": [ 968 + "linux" 969 + ], 970 + "engines": { 971 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 972 + }, 973 + "funding": { 974 + "url": "https://opencollective.com/libvips" 975 + }, 976 + "optionalDependencies": { 977 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" 978 + } 979 + }, 980 + "node_modules/@img/sharp-linuxmusl-x64": { 981 + "version": "0.33.5", 982 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", 983 + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", 984 + "cpu": [ 985 + "x64" 986 + ], 987 + "dev": true, 988 + "license": "Apache-2.0", 989 + "optional": true, 990 + "os": [ 991 + "linux" 992 + ], 993 + "engines": { 994 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 995 + }, 996 + "funding": { 997 + "url": "https://opencollective.com/libvips" 998 + }, 999 + "optionalDependencies": { 1000 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" 1001 + } 1002 + }, 1003 + "node_modules/@img/sharp-wasm32": { 1004 + "version": "0.33.5", 1005 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", 1006 + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", 1007 + "cpu": [ 1008 + "wasm32" 1009 + ], 1010 + "dev": true, 1011 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 1012 + "optional": true, 1013 + "dependencies": { 1014 + "@emnapi/runtime": "^1.2.0" 1015 + }, 1016 + "engines": { 1017 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1018 + }, 1019 + "funding": { 1020 + "url": "https://opencollective.com/libvips" 1021 + } 1022 + }, 1023 + "node_modules/@img/sharp-win32-ia32": { 1024 + "version": "0.33.5", 1025 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", 1026 + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", 1027 + "cpu": [ 1028 + "ia32" 1029 + ], 1030 + "dev": true, 1031 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 1032 + "optional": true, 1033 + "os": [ 1034 + "win32" 1035 + ], 1036 + "engines": { 1037 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1038 + }, 1039 + "funding": { 1040 + "url": "https://opencollective.com/libvips" 1041 + } 1042 + }, 1043 + "node_modules/@img/sharp-win32-x64": { 1044 + "version": "0.33.5", 1045 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", 1046 + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", 1047 + "cpu": [ 1048 + "x64" 1049 + ], 1050 + "dev": true, 1051 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 1052 + "optional": true, 1053 + "os": [ 1054 + "win32" 1055 + ], 1056 + "engines": { 1057 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1058 + }, 1059 + "funding": { 1060 + "url": "https://opencollective.com/libvips" 1061 + } 1062 + }, 1063 + "node_modules/@jridgewell/resolve-uri": { 1064 + "version": "3.1.2", 1065 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 1066 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 1067 + "dev": true, 1068 + "license": "MIT", 1069 + "engines": { 1070 + "node": ">=6.0.0" 1071 + } 1072 + }, 1073 + "node_modules/@jridgewell/sourcemap-codec": { 1074 + "version": "1.5.0", 1075 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 1076 + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 1077 + "dev": true, 1078 + "license": "MIT" 1079 + }, 1080 + "node_modules/@jridgewell/trace-mapping": { 1081 + "version": "0.3.9", 1082 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 1083 + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 1084 + "dev": true, 1085 + "license": "MIT", 1086 + "dependencies": { 1087 + "@jridgewell/resolve-uri": "^3.0.3", 1088 + "@jridgewell/sourcemap-codec": "^1.4.10" 1089 + } 1090 + }, 1091 + "node_modules/@rollup/rollup-android-arm-eabi": { 1092 + "version": "4.40.1", 1093 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", 1094 + "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", 1095 + "cpu": [ 1096 + "arm" 1097 + ], 1098 + "dev": true, 1099 + "license": "MIT", 1100 + "optional": true, 1101 + "os": [ 1102 + "android" 1103 + ] 1104 + }, 1105 + "node_modules/@rollup/rollup-android-arm64": { 1106 + "version": "4.40.1", 1107 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", 1108 + "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", 1109 + "cpu": [ 1110 + "arm64" 1111 + ], 1112 + "dev": true, 1113 + "license": "MIT", 1114 + "optional": true, 1115 + "os": [ 1116 + "android" 1117 + ] 1118 + }, 1119 + "node_modules/@rollup/rollup-darwin-arm64": { 1120 + "version": "4.40.1", 1121 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", 1122 + "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", 1123 + "cpu": [ 1124 + "arm64" 1125 + ], 1126 + "dev": true, 1127 + "license": "MIT", 1128 + "optional": true, 1129 + "os": [ 1130 + "darwin" 1131 + ] 1132 + }, 1133 + "node_modules/@rollup/rollup-darwin-x64": { 1134 + "version": "4.40.1", 1135 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", 1136 + "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", 1137 + "cpu": [ 1138 + "x64" 1139 + ], 1140 + "dev": true, 1141 + "license": "MIT", 1142 + "optional": true, 1143 + "os": [ 1144 + "darwin" 1145 + ] 1146 + }, 1147 + "node_modules/@rollup/rollup-freebsd-arm64": { 1148 + "version": "4.40.1", 1149 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", 1150 + "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", 1151 + "cpu": [ 1152 + "arm64" 1153 + ], 1154 + "dev": true, 1155 + "license": "MIT", 1156 + "optional": true, 1157 + "os": [ 1158 + "freebsd" 1159 + ] 1160 + }, 1161 + "node_modules/@rollup/rollup-freebsd-x64": { 1162 + "version": "4.40.1", 1163 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", 1164 + "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", 1165 + "cpu": [ 1166 + "x64" 1167 + ], 1168 + "dev": true, 1169 + "license": "MIT", 1170 + "optional": true, 1171 + "os": [ 1172 + "freebsd" 1173 + ] 1174 + }, 1175 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1176 + "version": "4.40.1", 1177 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", 1178 + "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", 1179 + "cpu": [ 1180 + "arm" 1181 + ], 1182 + "dev": true, 1183 + "license": "MIT", 1184 + "optional": true, 1185 + "os": [ 1186 + "linux" 1187 + ] 1188 + }, 1189 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1190 + "version": "4.40.1", 1191 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", 1192 + "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", 1193 + "cpu": [ 1194 + "arm" 1195 + ], 1196 + "dev": true, 1197 + "license": "MIT", 1198 + "optional": true, 1199 + "os": [ 1200 + "linux" 1201 + ] 1202 + }, 1203 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1204 + "version": "4.40.1", 1205 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", 1206 + "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", 1207 + "cpu": [ 1208 + "arm64" 1209 + ], 1210 + "dev": true, 1211 + "license": "MIT", 1212 + "optional": true, 1213 + "os": [ 1214 + "linux" 1215 + ] 1216 + }, 1217 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1218 + "version": "4.40.1", 1219 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", 1220 + "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", 1221 + "cpu": [ 1222 + "arm64" 1223 + ], 1224 + "dev": true, 1225 + "license": "MIT", 1226 + "optional": true, 1227 + "os": [ 1228 + "linux" 1229 + ] 1230 + }, 1231 + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 1232 + "version": "4.40.1", 1233 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", 1234 + "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", 1235 + "cpu": [ 1236 + "loong64" 1237 + ], 1238 + "dev": true, 1239 + "license": "MIT", 1240 + "optional": true, 1241 + "os": [ 1242 + "linux" 1243 + ] 1244 + }, 1245 + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 1246 + "version": "4.40.1", 1247 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", 1248 + "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", 1249 + "cpu": [ 1250 + "ppc64" 1251 + ], 1252 + "dev": true, 1253 + "license": "MIT", 1254 + "optional": true, 1255 + "os": [ 1256 + "linux" 1257 + ] 1258 + }, 1259 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1260 + "version": "4.40.1", 1261 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", 1262 + "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", 1263 + "cpu": [ 1264 + "riscv64" 1265 + ], 1266 + "dev": true, 1267 + "license": "MIT", 1268 + "optional": true, 1269 + "os": [ 1270 + "linux" 1271 + ] 1272 + }, 1273 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1274 + "version": "4.40.1", 1275 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", 1276 + "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", 1277 + "cpu": [ 1278 + "riscv64" 1279 + ], 1280 + "dev": true, 1281 + "license": "MIT", 1282 + "optional": true, 1283 + "os": [ 1284 + "linux" 1285 + ] 1286 + }, 1287 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1288 + "version": "4.40.1", 1289 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", 1290 + "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", 1291 + "cpu": [ 1292 + "s390x" 1293 + ], 1294 + "dev": true, 1295 + "license": "MIT", 1296 + "optional": true, 1297 + "os": [ 1298 + "linux" 1299 + ] 1300 + }, 1301 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1302 + "version": "4.40.1", 1303 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", 1304 + "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", 1305 + "cpu": [ 1306 + "x64" 1307 + ], 1308 + "dev": true, 1309 + "license": "MIT", 1310 + "optional": true, 1311 + "os": [ 1312 + "linux" 1313 + ] 1314 + }, 1315 + "node_modules/@rollup/rollup-linux-x64-musl": { 1316 + "version": "4.40.1", 1317 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", 1318 + "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", 1319 + "cpu": [ 1320 + "x64" 1321 + ], 1322 + "dev": true, 1323 + "license": "MIT", 1324 + "optional": true, 1325 + "os": [ 1326 + "linux" 1327 + ] 1328 + }, 1329 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1330 + "version": "4.40.1", 1331 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", 1332 + "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", 1333 + "cpu": [ 1334 + "arm64" 1335 + ], 1336 + "dev": true, 1337 + "license": "MIT", 1338 + "optional": true, 1339 + "os": [ 1340 + "win32" 1341 + ] 1342 + }, 1343 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1344 + "version": "4.40.1", 1345 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", 1346 + "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", 1347 + "cpu": [ 1348 + "ia32" 1349 + ], 1350 + "dev": true, 1351 + "license": "MIT", 1352 + "optional": true, 1353 + "os": [ 1354 + "win32" 1355 + ] 1356 + }, 1357 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1358 + "version": "4.40.1", 1359 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", 1360 + "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", 1361 + "cpu": [ 1362 + "x64" 1363 + ], 1364 + "dev": true, 1365 + "license": "MIT", 1366 + "optional": true, 1367 + "os": [ 1368 + "win32" 1369 + ] 1370 + }, 1371 + "node_modules/@standard-schema/spec": { 1372 + "version": "1.1.0", 1373 + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", 1374 + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", 1375 + "license": "MIT" 1376 + }, 1377 + "node_modules/@types/estree": { 1378 + "version": "1.0.7", 1379 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 1380 + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 1381 + "dev": true, 1382 + "license": "MIT" 1383 + }, 1384 + "node_modules/@vitest/expect": { 1385 + "version": "3.0.9", 1386 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", 1387 + "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", 1388 + "dev": true, 1389 + "license": "MIT", 1390 + "dependencies": { 1391 + "@vitest/spy": "3.0.9", 1392 + "@vitest/utils": "3.0.9", 1393 + "chai": "^5.2.0", 1394 + "tinyrainbow": "^2.0.0" 1395 + }, 1396 + "funding": { 1397 + "url": "https://opencollective.com/vitest" 1398 + } 1399 + }, 1400 + "node_modules/@vitest/mocker": { 1401 + "version": "3.0.9", 1402 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", 1403 + "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", 1404 + "dev": true, 1405 + "license": "MIT", 1406 + "dependencies": { 1407 + "@vitest/spy": "3.0.9", 1408 + "estree-walker": "^3.0.3", 1409 + "magic-string": "^0.30.17" 1410 + }, 1411 + "funding": { 1412 + "url": "https://opencollective.com/vitest" 1413 + }, 1414 + "peerDependencies": { 1415 + "msw": "^2.4.9", 1416 + "vite": "^5.0.0 || ^6.0.0" 1417 + }, 1418 + "peerDependenciesMeta": { 1419 + "msw": { 1420 + "optional": true 1421 + }, 1422 + "vite": { 1423 + "optional": true 1424 + } 1425 + } 1426 + }, 1427 + "node_modules/@vitest/pretty-format": { 1428 + "version": "3.1.2", 1429 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", 1430 + "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", 1431 + "dev": true, 1432 + "license": "MIT", 1433 + "dependencies": { 1434 + "tinyrainbow": "^2.0.0" 1435 + }, 1436 + "funding": { 1437 + "url": "https://opencollective.com/vitest" 1438 + } 1439 + }, 1440 + "node_modules/@vitest/runner": { 1441 + "version": "3.0.9", 1442 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", 1443 + "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", 1444 + "dev": true, 1445 + "license": "MIT", 1446 + "dependencies": { 1447 + "@vitest/utils": "3.0.9", 1448 + "pathe": "^2.0.3" 1449 + }, 1450 + "funding": { 1451 + "url": "https://opencollective.com/vitest" 1452 + } 1453 + }, 1454 + "node_modules/@vitest/snapshot": { 1455 + "version": "3.0.9", 1456 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", 1457 + "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", 1458 + "dev": true, 1459 + "license": "MIT", 1460 + "dependencies": { 1461 + "@vitest/pretty-format": "3.0.9", 1462 + "magic-string": "^0.30.17", 1463 + "pathe": "^2.0.3" 1464 + }, 1465 + "funding": { 1466 + "url": "https://opencollective.com/vitest" 1467 + } 1468 + }, 1469 + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { 1470 + "version": "3.0.9", 1471 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1472 + "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1473 + "dev": true, 1474 + "license": "MIT", 1475 + "dependencies": { 1476 + "tinyrainbow": "^2.0.0" 1477 + }, 1478 + "funding": { 1479 + "url": "https://opencollective.com/vitest" 1480 + } 1481 + }, 1482 + "node_modules/@vitest/spy": { 1483 + "version": "3.0.9", 1484 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", 1485 + "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", 1486 + "dev": true, 1487 + "license": "MIT", 1488 + "dependencies": { 1489 + "tinyspy": "^3.0.2" 1490 + }, 1491 + "funding": { 1492 + "url": "https://opencollective.com/vitest" 1493 + } 1494 + }, 1495 + "node_modules/@vitest/utils": { 1496 + "version": "3.0.9", 1497 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", 1498 + "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", 1499 + "dev": true, 1500 + "license": "MIT", 1501 + "dependencies": { 1502 + "@vitest/pretty-format": "3.0.9", 1503 + "loupe": "^3.1.3", 1504 + "tinyrainbow": "^2.0.0" 1505 + }, 1506 + "funding": { 1507 + "url": "https://opencollective.com/vitest" 1508 + } 1509 + }, 1510 + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { 1511 + "version": "3.0.9", 1512 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1513 + "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1514 + "dev": true, 1515 + "license": "MIT", 1516 + "dependencies": { 1517 + "tinyrainbow": "^2.0.0" 1518 + }, 1519 + "funding": { 1520 + "url": "https://opencollective.com/vitest" 1521 + } 1522 + }, 1523 + "node_modules/acorn": { 1524 + "version": "8.14.0", 1525 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 1526 + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 1527 + "dev": true, 1528 + "license": "MIT", 1529 + "bin": { 1530 + "acorn": "bin/acorn" 1531 + }, 1532 + "engines": { 1533 + "node": ">=0.4.0" 1534 + } 1535 + }, 1536 + "node_modules/acorn-walk": { 1537 + "version": "8.3.2", 1538 + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", 1539 + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", 1540 + "dev": true, 1541 + "license": "MIT", 1542 + "engines": { 1543 + "node": ">=0.4.0" 1544 + } 1545 + }, 1546 + "node_modules/as-table": { 1547 + "version": "1.0.55", 1548 + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", 1549 + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", 1550 + "dev": true, 1551 + "license": "MIT", 1552 + "dependencies": { 1553 + "printable-characters": "^1.0.42" 1554 + } 1555 + }, 1556 + "node_modules/assertion-error": { 1557 + "version": "2.0.1", 1558 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1559 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1560 + "dev": true, 1561 + "license": "MIT", 1562 + "engines": { 1563 + "node": ">=12" 1564 + } 1565 + }, 1566 + "node_modules/birpc": { 1567 + "version": "0.2.14", 1568 + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", 1569 + "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", 1570 + "dev": true, 1571 + "license": "MIT", 1572 + "funding": { 1573 + "url": "https://github.com/sponsors/antfu" 1574 + } 1575 + }, 1576 + "node_modules/blake3-wasm": { 1577 + "version": "2.1.5", 1578 + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 1579 + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 1580 + "dev": true, 1581 + "license": "MIT" 1582 + }, 1583 + "node_modules/cac": { 1584 + "version": "6.7.14", 1585 + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 1586 + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 1587 + "dev": true, 1588 + "license": "MIT", 1589 + "engines": { 1590 + "node": ">=8" 1591 + } 1592 + }, 1593 + "node_modules/chai": { 1594 + "version": "5.2.0", 1595 + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", 1596 + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", 1597 + "dev": true, 1598 + "license": "MIT", 1599 + "dependencies": { 1600 + "assertion-error": "^2.0.1", 1601 + "check-error": "^2.1.1", 1602 + "deep-eql": "^5.0.1", 1603 + "loupe": "^3.1.0", 1604 + "pathval": "^2.0.0" 1605 + }, 1606 + "engines": { 1607 + "node": ">=12" 1608 + } 1609 + }, 1610 + "node_modules/check-error": { 1611 + "version": "2.1.1", 1612 + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 1613 + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 1614 + "dev": true, 1615 + "license": "MIT", 1616 + "engines": { 1617 + "node": ">= 16" 1618 + } 1619 + }, 1620 + "node_modules/cjs-module-lexer": { 1621 + "version": "1.4.3", 1622 + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", 1623 + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", 1624 + "dev": true, 1625 + "license": "MIT" 1626 + }, 1627 + "node_modules/color": { 1628 + "version": "4.2.3", 1629 + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 1630 + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 1631 + "dev": true, 1632 + "license": "MIT", 1633 + "optional": true, 1634 + "dependencies": { 1635 + "color-convert": "^2.0.1", 1636 + "color-string": "^1.9.0" 1637 + }, 1638 + "engines": { 1639 + "node": ">=12.5.0" 1640 + } 1641 + }, 1642 + "node_modules/color-convert": { 1643 + "version": "2.0.1", 1644 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1645 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1646 + "dev": true, 1647 + "license": "MIT", 1648 + "optional": true, 1649 + "dependencies": { 1650 + "color-name": "~1.1.4" 1651 + }, 1652 + "engines": { 1653 + "node": ">=7.0.0" 1654 + } 1655 + }, 1656 + "node_modules/color-name": { 1657 + "version": "1.1.4", 1658 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1659 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1660 + "dev": true, 1661 + "license": "MIT", 1662 + "optional": true 1663 + }, 1664 + "node_modules/color-string": { 1665 + "version": "1.9.1", 1666 + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1667 + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1668 + "dev": true, 1669 + "license": "MIT", 1670 + "optional": true, 1671 + "dependencies": { 1672 + "color-name": "^1.0.0", 1673 + "simple-swizzle": "^0.2.2" 1674 + } 1675 + }, 1676 + "node_modules/cookie": { 1677 + "version": "0.7.2", 1678 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 1679 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 1680 + "dev": true, 1681 + "license": "MIT", 1682 + "engines": { 1683 + "node": ">= 0.6" 1684 + } 1685 + }, 1686 + "node_modules/data-uri-to-buffer": { 1687 + "version": "2.0.2", 1688 + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", 1689 + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", 1690 + "dev": true, 1691 + "license": "MIT" 1692 + }, 1693 + "node_modules/debug": { 1694 + "version": "4.4.0", 1695 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1696 + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1697 + "dev": true, 1698 + "license": "MIT", 1699 + "dependencies": { 1700 + "ms": "^2.1.3" 1701 + }, 1702 + "engines": { 1703 + "node": ">=6.0" 1704 + }, 1705 + "peerDependenciesMeta": { 1706 + "supports-color": { 1707 + "optional": true 1708 + } 1709 + } 1710 + }, 1711 + "node_modules/deep-eql": { 1712 + "version": "5.0.2", 1713 + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 1714 + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 1715 + "dev": true, 1716 + "license": "MIT", 1717 + "engines": { 1718 + "node": ">=6" 1719 + } 1720 + }, 1721 + "node_modules/defu": { 1722 + "version": "6.1.4", 1723 + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 1724 + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 1725 + "dev": true, 1726 + "license": "MIT" 1727 + }, 1728 + "node_modules/detect-libc": { 1729 + "version": "2.0.4", 1730 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", 1731 + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", 1732 + "dev": true, 1733 + "license": "Apache-2.0", 1734 + "optional": true, 1735 + "engines": { 1736 + "node": ">=8" 1737 + } 1738 + }, 1739 + "node_modules/devalue": { 1740 + "version": "4.3.3", 1741 + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", 1742 + "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", 1743 + "dev": true, 1744 + "license": "MIT" 1745 + }, 1746 + "node_modules/es-module-lexer": { 1747 + "version": "1.7.0", 1748 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1749 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1750 + "dev": true, 1751 + "license": "MIT" 1752 + }, 1753 + "node_modules/esbuild": { 1754 + "version": "0.25.3", 1755 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", 1756 + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", 1757 + "dev": true, 1758 + "hasInstallScript": true, 1759 + "license": "MIT", 1760 + "bin": { 1761 + "esbuild": "bin/esbuild" 1762 + }, 1763 + "engines": { 1764 + "node": ">=18" 1765 + }, 1766 + "optionalDependencies": { 1767 + "@esbuild/aix-ppc64": "0.25.3", 1768 + "@esbuild/android-arm": "0.25.3", 1769 + "@esbuild/android-arm64": "0.25.3", 1770 + "@esbuild/android-x64": "0.25.3", 1771 + "@esbuild/darwin-arm64": "0.25.3", 1772 + "@esbuild/darwin-x64": "0.25.3", 1773 + "@esbuild/freebsd-arm64": "0.25.3", 1774 + "@esbuild/freebsd-x64": "0.25.3", 1775 + "@esbuild/linux-arm": "0.25.3", 1776 + "@esbuild/linux-arm64": "0.25.3", 1777 + "@esbuild/linux-ia32": "0.25.3", 1778 + "@esbuild/linux-loong64": "0.25.3", 1779 + "@esbuild/linux-mips64el": "0.25.3", 1780 + "@esbuild/linux-ppc64": "0.25.3", 1781 + "@esbuild/linux-riscv64": "0.25.3", 1782 + "@esbuild/linux-s390x": "0.25.3", 1783 + "@esbuild/linux-x64": "0.25.3", 1784 + "@esbuild/netbsd-arm64": "0.25.3", 1785 + "@esbuild/netbsd-x64": "0.25.3", 1786 + "@esbuild/openbsd-arm64": "0.25.3", 1787 + "@esbuild/openbsd-x64": "0.25.3", 1788 + "@esbuild/sunos-x64": "0.25.3", 1789 + "@esbuild/win32-arm64": "0.25.3", 1790 + "@esbuild/win32-ia32": "0.25.3", 1791 + "@esbuild/win32-x64": "0.25.3" 1792 + } 1793 + }, 1794 + "node_modules/esm-env": { 1795 + "version": "1.2.2", 1796 + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", 1797 + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", 1798 + "license": "MIT" 1799 + }, 1800 + "node_modules/estree-walker": { 1801 + "version": "3.0.3", 1802 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1803 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1804 + "dev": true, 1805 + "license": "MIT", 1806 + "dependencies": { 1807 + "@types/estree": "^1.0.0" 1808 + } 1809 + }, 1810 + "node_modules/exit-hook": { 1811 + "version": "2.2.1", 1812 + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 1813 + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 1814 + "dev": true, 1815 + "license": "MIT", 1816 + "engines": { 1817 + "node": ">=6" 1818 + }, 1819 + "funding": { 1820 + "url": "https://github.com/sponsors/sindresorhus" 1821 + } 1822 + }, 1823 + "node_modules/expect-type": { 1824 + "version": "1.2.1", 1825 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", 1826 + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", 1827 + "dev": true, 1828 + "license": "Apache-2.0", 1829 + "engines": { 1830 + "node": ">=12.0.0" 1831 + } 1832 + }, 1833 + "node_modules/exsolve": { 1834 + "version": "1.0.5", 1835 + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", 1836 + "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", 1837 + "dev": true, 1838 + "license": "MIT" 1839 + }, 1840 + "node_modules/fdir": { 1841 + "version": "6.4.4", 1842 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", 1843 + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 1844 + "dev": true, 1845 + "license": "MIT", 1846 + "peerDependencies": { 1847 + "picomatch": "^3 || ^4" 1848 + }, 1849 + "peerDependenciesMeta": { 1850 + "picomatch": { 1851 + "optional": true 1852 + } 1853 + } 1854 + }, 1855 + "node_modules/fsevents": { 1856 + "version": "2.3.3", 1857 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1858 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1859 + "dev": true, 1860 + "hasInstallScript": true, 1861 + "license": "MIT", 1862 + "optional": true, 1863 + "os": [ 1864 + "darwin" 1865 + ], 1866 + "engines": { 1867 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1868 + } 1869 + }, 1870 + "node_modules/get-source": { 1871 + "version": "2.0.12", 1872 + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", 1873 + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", 1874 + "dev": true, 1875 + "license": "Unlicense", 1876 + "dependencies": { 1877 + "data-uri-to-buffer": "^2.0.0", 1878 + "source-map": "^0.6.1" 1879 + } 1880 + }, 1881 + "node_modules/glob-to-regexp": { 1882 + "version": "0.4.1", 1883 + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 1884 + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 1885 + "dev": true, 1886 + "license": "BSD-2-Clause" 1887 + }, 1888 + "node_modules/is-arrayish": { 1889 + "version": "0.3.2", 1890 + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 1891 + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 1892 + "dev": true, 1893 + "license": "MIT", 1894 + "optional": true 1895 + }, 1896 + "node_modules/loupe": { 1897 + "version": "3.1.3", 1898 + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", 1899 + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", 1900 + "dev": true, 1901 + "license": "MIT" 1902 + }, 1903 + "node_modules/magic-string": { 1904 + "version": "0.30.17", 1905 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 1906 + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 1907 + "dev": true, 1908 + "license": "MIT", 1909 + "dependencies": { 1910 + "@jridgewell/sourcemap-codec": "^1.5.0" 1911 + } 1912 + }, 1913 + "node_modules/mime": { 1914 + "version": "3.0.0", 1915 + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1916 + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1917 + "dev": true, 1918 + "license": "MIT", 1919 + "bin": { 1920 + "mime": "cli.js" 1921 + }, 1922 + "engines": { 1923 + "node": ">=10.0.0" 1924 + } 1925 + }, 1926 + "node_modules/miniflare": { 1927 + "version": "4.20250428.1", 1928 + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250428.1.tgz", 1929 + "integrity": "sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==", 1930 + "dev": true, 1931 + "license": "MIT", 1932 + "dependencies": { 1933 + "@cspotcode/source-map-support": "0.8.1", 1934 + "acorn": "8.14.0", 1935 + "acorn-walk": "8.3.2", 1936 + "exit-hook": "2.2.1", 1937 + "glob-to-regexp": "0.4.1", 1938 + "stoppable": "1.1.0", 1939 + "undici": "^5.28.5", 1940 + "workerd": "1.20250428.0", 1941 + "ws": "8.18.0", 1942 + "youch": "3.3.4", 1943 + "zod": "3.22.3" 1944 + }, 1945 + "bin": { 1946 + "miniflare": "bootstrap.js" 1947 + }, 1948 + "engines": { 1949 + "node": ">=18.0.0" 1950 + } 1951 + }, 1952 + "node_modules/miniflare/node_modules/zod": { 1953 + "version": "3.22.3", 1954 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", 1955 + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", 1956 + "dev": true, 1957 + "license": "MIT", 1958 + "funding": { 1959 + "url": "https://github.com/sponsors/colinhacks" 1960 + } 1961 + }, 1962 + "node_modules/ms": { 1963 + "version": "2.1.3", 1964 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1965 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1966 + "dev": true, 1967 + "license": "MIT" 1968 + }, 1969 + "node_modules/mustache": { 1970 + "version": "4.2.0", 1971 + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 1972 + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 1973 + "dev": true, 1974 + "license": "MIT", 1975 + "bin": { 1976 + "mustache": "bin/mustache" 1977 + } 1978 + }, 1979 + "node_modules/nanoid": { 1980 + "version": "3.3.11", 1981 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1982 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1983 + "dev": true, 1984 + "funding": [ 1985 + { 1986 + "type": "github", 1987 + "url": "https://github.com/sponsors/ai" 1988 + } 1989 + ], 1990 + "license": "MIT", 1991 + "bin": { 1992 + "nanoid": "bin/nanoid.cjs" 1993 + }, 1994 + "engines": { 1995 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1996 + } 1997 + }, 1998 + "node_modules/ohash": { 1999 + "version": "2.0.11", 2000 + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", 2001 + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", 2002 + "dev": true, 2003 + "license": "MIT" 2004 + }, 2005 + "node_modules/path-to-regexp": { 2006 + "version": "6.3.0", 2007 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 2008 + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 2009 + "dev": true, 2010 + "license": "MIT" 2011 + }, 2012 + "node_modules/pathe": { 2013 + "version": "2.0.3", 2014 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 2015 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 2016 + "dev": true, 2017 + "license": "MIT" 2018 + }, 2019 + "node_modules/pathval": { 2020 + "version": "2.0.0", 2021 + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 2022 + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 2023 + "dev": true, 2024 + "license": "MIT", 2025 + "engines": { 2026 + "node": ">= 14.16" 2027 + } 2028 + }, 2029 + "node_modules/picocolors": { 2030 + "version": "1.1.1", 2031 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 2032 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 2033 + "dev": true, 2034 + "license": "ISC" 2035 + }, 2036 + "node_modules/picomatch": { 2037 + "version": "4.0.2", 2038 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 2039 + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 2040 + "dev": true, 2041 + "license": "MIT", 2042 + "engines": { 2043 + "node": ">=12" 2044 + }, 2045 + "funding": { 2046 + "url": "https://github.com/sponsors/jonschlinkert" 2047 + } 2048 + }, 2049 + "node_modules/postcss": { 2050 + "version": "8.5.3", 2051 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", 2052 + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", 2053 + "dev": true, 2054 + "funding": [ 2055 + { 2056 + "type": "opencollective", 2057 + "url": "https://opencollective.com/postcss/" 2058 + }, 2059 + { 2060 + "type": "tidelift", 2061 + "url": "https://tidelift.com/funding/github/npm/postcss" 2062 + }, 2063 + { 2064 + "type": "github", 2065 + "url": "https://github.com/sponsors/ai" 2066 + } 2067 + ], 2068 + "license": "MIT", 2069 + "dependencies": { 2070 + "nanoid": "^3.3.8", 2071 + "picocolors": "^1.1.1", 2072 + "source-map-js": "^1.2.1" 2073 + }, 2074 + "engines": { 2075 + "node": "^10 || ^12 || >=14" 2076 + } 2077 + }, 2078 + "node_modules/printable-characters": { 2079 + "version": "1.0.42", 2080 + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", 2081 + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", 2082 + "dev": true, 2083 + "license": "Unlicense" 2084 + }, 2085 + "node_modules/rollup": { 2086 + "version": "4.40.1", 2087 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", 2088 + "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", 2089 + "dev": true, 2090 + "license": "MIT", 2091 + "dependencies": { 2092 + "@types/estree": "1.0.7" 2093 + }, 2094 + "bin": { 2095 + "rollup": "dist/bin/rollup" 2096 + }, 2097 + "engines": { 2098 + "node": ">=18.0.0", 2099 + "npm": ">=8.0.0" 2100 + }, 2101 + "optionalDependencies": { 2102 + "@rollup/rollup-android-arm-eabi": "4.40.1", 2103 + "@rollup/rollup-android-arm64": "4.40.1", 2104 + "@rollup/rollup-darwin-arm64": "4.40.1", 2105 + "@rollup/rollup-darwin-x64": "4.40.1", 2106 + "@rollup/rollup-freebsd-arm64": "4.40.1", 2107 + "@rollup/rollup-freebsd-x64": "4.40.1", 2108 + "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", 2109 + "@rollup/rollup-linux-arm-musleabihf": "4.40.1", 2110 + "@rollup/rollup-linux-arm64-gnu": "4.40.1", 2111 + "@rollup/rollup-linux-arm64-musl": "4.40.1", 2112 + "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", 2113 + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", 2114 + "@rollup/rollup-linux-riscv64-gnu": "4.40.1", 2115 + "@rollup/rollup-linux-riscv64-musl": "4.40.1", 2116 + "@rollup/rollup-linux-s390x-gnu": "4.40.1", 2117 + "@rollup/rollup-linux-x64-gnu": "4.40.1", 2118 + "@rollup/rollup-linux-x64-musl": "4.40.1", 2119 + "@rollup/rollup-win32-arm64-msvc": "4.40.1", 2120 + "@rollup/rollup-win32-ia32-msvc": "4.40.1", 2121 + "@rollup/rollup-win32-x64-msvc": "4.40.1", 2122 + "fsevents": "~2.3.2" 2123 + } 2124 + }, 2125 + "node_modules/semver": { 2126 + "version": "7.7.1", 2127 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 2128 + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 2129 + "dev": true, 2130 + "license": "ISC", 2131 + "bin": { 2132 + "semver": "bin/semver.js" 2133 + }, 2134 + "engines": { 2135 + "node": ">=10" 2136 + } 2137 + }, 2138 + "node_modules/sharp": { 2139 + "version": "0.33.5", 2140 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", 2141 + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", 2142 + "dev": true, 2143 + "hasInstallScript": true, 2144 + "license": "Apache-2.0", 2145 + "optional": true, 2146 + "dependencies": { 2147 + "color": "^4.2.3", 2148 + "detect-libc": "^2.0.3", 2149 + "semver": "^7.6.3" 2150 + }, 2151 + "engines": { 2152 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 2153 + }, 2154 + "funding": { 2155 + "url": "https://opencollective.com/libvips" 2156 + }, 2157 + "optionalDependencies": { 2158 + "@img/sharp-darwin-arm64": "0.33.5", 2159 + "@img/sharp-darwin-x64": "0.33.5", 2160 + "@img/sharp-libvips-darwin-arm64": "1.0.4", 2161 + "@img/sharp-libvips-darwin-x64": "1.0.4", 2162 + "@img/sharp-libvips-linux-arm": "1.0.5", 2163 + "@img/sharp-libvips-linux-arm64": "1.0.4", 2164 + "@img/sharp-libvips-linux-s390x": "1.0.4", 2165 + "@img/sharp-libvips-linux-x64": "1.0.4", 2166 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", 2167 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", 2168 + "@img/sharp-linux-arm": "0.33.5", 2169 + "@img/sharp-linux-arm64": "0.33.5", 2170 + "@img/sharp-linux-s390x": "0.33.5", 2171 + "@img/sharp-linux-x64": "0.33.5", 2172 + "@img/sharp-linuxmusl-arm64": "0.33.5", 2173 + "@img/sharp-linuxmusl-x64": "0.33.5", 2174 + "@img/sharp-wasm32": "0.33.5", 2175 + "@img/sharp-win32-ia32": "0.33.5", 2176 + "@img/sharp-win32-x64": "0.33.5" 2177 + } 2178 + }, 2179 + "node_modules/siginfo": { 2180 + "version": "2.0.0", 2181 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 2182 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 2183 + "dev": true, 2184 + "license": "ISC" 2185 + }, 2186 + "node_modules/simple-swizzle": { 2187 + "version": "0.2.2", 2188 + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 2189 + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 2190 + "dev": true, 2191 + "license": "MIT", 2192 + "optional": true, 2193 + "dependencies": { 2194 + "is-arrayish": "^0.3.1" 2195 + } 2196 + }, 2197 + "node_modules/source-map": { 2198 + "version": "0.6.1", 2199 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 2200 + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 2201 + "dev": true, 2202 + "license": "BSD-3-Clause", 2203 + "engines": { 2204 + "node": ">=0.10.0" 2205 + } 2206 + }, 2207 + "node_modules/source-map-js": { 2208 + "version": "1.2.1", 2209 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2210 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2211 + "dev": true, 2212 + "license": "BSD-3-Clause", 2213 + "engines": { 2214 + "node": ">=0.10.0" 2215 + } 2216 + }, 2217 + "node_modules/stackback": { 2218 + "version": "0.0.2", 2219 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 2220 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 2221 + "dev": true, 2222 + "license": "MIT" 2223 + }, 2224 + "node_modules/stacktracey": { 2225 + "version": "2.1.8", 2226 + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", 2227 + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", 2228 + "dev": true, 2229 + "license": "Unlicense", 2230 + "dependencies": { 2231 + "as-table": "^1.0.36", 2232 + "get-source": "^2.0.12" 2233 + } 2234 + }, 2235 + "node_modules/std-env": { 2236 + "version": "3.9.0", 2237 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", 2238 + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", 2239 + "dev": true, 2240 + "license": "MIT" 2241 + }, 2242 + "node_modules/stoppable": { 2243 + "version": "1.1.0", 2244 + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 2245 + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 2246 + "dev": true, 2247 + "license": "MIT", 2248 + "engines": { 2249 + "node": ">=4", 2250 + "npm": ">=6" 2251 + } 2252 + }, 2253 + "node_modules/tinybench": { 2254 + "version": "2.9.0", 2255 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 2256 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 2257 + "dev": true, 2258 + "license": "MIT" 2259 + }, 2260 + "node_modules/tinyexec": { 2261 + "version": "0.3.2", 2262 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 2263 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 2264 + "dev": true, 2265 + "license": "MIT" 2266 + }, 2267 + "node_modules/tinyglobby": { 2268 + "version": "0.2.13", 2269 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", 2270 + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", 2271 + "dev": true, 2272 + "license": "MIT", 2273 + "dependencies": { 2274 + "fdir": "^6.4.4", 2275 + "picomatch": "^4.0.2" 2276 + }, 2277 + "engines": { 2278 + "node": ">=12.0.0" 2279 + }, 2280 + "funding": { 2281 + "url": "https://github.com/sponsors/SuperchupuDev" 2282 + } 2283 + }, 2284 + "node_modules/tinypool": { 2285 + "version": "1.0.2", 2286 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", 2287 + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", 2288 + "dev": true, 2289 + "license": "MIT", 2290 + "engines": { 2291 + "node": "^18.0.0 || >=20.0.0" 2292 + } 2293 + }, 2294 + "node_modules/tinyrainbow": { 2295 + "version": "2.0.0", 2296 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", 2297 + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", 2298 + "dev": true, 2299 + "license": "MIT", 2300 + "engines": { 2301 + "node": ">=14.0.0" 2302 + } 2303 + }, 2304 + "node_modules/tinyspy": { 2305 + "version": "3.0.2", 2306 + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", 2307 + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", 2308 + "dev": true, 2309 + "license": "MIT", 2310 + "engines": { 2311 + "node": ">=14.0.0" 2312 + } 2313 + }, 2314 + "node_modules/tslib": { 2315 + "version": "2.8.1", 2316 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2317 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2318 + "dev": true, 2319 + "license": "0BSD", 2320 + "optional": true 2321 + }, 2322 + "node_modules/ufo": { 2323 + "version": "1.6.1", 2324 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", 2325 + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", 2326 + "dev": true, 2327 + "license": "MIT" 2328 + }, 2329 + "node_modules/undici": { 2330 + "version": "5.29.0", 2331 + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 2332 + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 2333 + "dev": true, 2334 + "license": "MIT", 2335 + "dependencies": { 2336 + "@fastify/busboy": "^2.0.0" 2337 + }, 2338 + "engines": { 2339 + "node": ">=14.0" 2340 + } 2341 + }, 2342 + "node_modules/unenv": { 2343 + "version": "2.0.0-rc.15", 2344 + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz", 2345 + "integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==", 2346 + "dev": true, 2347 + "license": "MIT", 2348 + "dependencies": { 2349 + "defu": "^6.1.4", 2350 + "exsolve": "^1.0.4", 2351 + "ohash": "^2.0.11", 2352 + "pathe": "^2.0.3", 2353 + "ufo": "^1.5.4" 2354 + } 2355 + }, 2356 + "node_modules/unicode-segmenter": { 2357 + "version": "0.14.4", 2358 + "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.4.tgz", 2359 + "integrity": "sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==", 2360 + "license": "MIT" 2361 + }, 2362 + "node_modules/vite": { 2363 + "version": "6.3.4", 2364 + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", 2365 + "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", 2366 + "dev": true, 2367 + "license": "MIT", 2368 + "dependencies": { 2369 + "esbuild": "^0.25.0", 2370 + "fdir": "^6.4.4", 2371 + "picomatch": "^4.0.2", 2372 + "postcss": "^8.5.3", 2373 + "rollup": "^4.34.9", 2374 + "tinyglobby": "^0.2.13" 2375 + }, 2376 + "bin": { 2377 + "vite": "bin/vite.js" 2378 + }, 2379 + "engines": { 2380 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2381 + }, 2382 + "funding": { 2383 + "url": "https://github.com/vitejs/vite?sponsor=1" 2384 + }, 2385 + "optionalDependencies": { 2386 + "fsevents": "~2.3.3" 2387 + }, 2388 + "peerDependencies": { 2389 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2390 + "jiti": ">=1.21.0", 2391 + "less": "*", 2392 + "lightningcss": "^1.21.0", 2393 + "sass": "*", 2394 + "sass-embedded": "*", 2395 + "stylus": "*", 2396 + "sugarss": "*", 2397 + "terser": "^5.16.0", 2398 + "tsx": "^4.8.1", 2399 + "yaml": "^2.4.2" 2400 + }, 2401 + "peerDependenciesMeta": { 2402 + "@types/node": { 2403 + "optional": true 2404 + }, 2405 + "jiti": { 2406 + "optional": true 2407 + }, 2408 + "less": { 2409 + "optional": true 2410 + }, 2411 + "lightningcss": { 2412 + "optional": true 2413 + }, 2414 + "sass": { 2415 + "optional": true 2416 + }, 2417 + "sass-embedded": { 2418 + "optional": true 2419 + }, 2420 + "stylus": { 2421 + "optional": true 2422 + }, 2423 + "sugarss": { 2424 + "optional": true 2425 + }, 2426 + "terser": { 2427 + "optional": true 2428 + }, 2429 + "tsx": { 2430 + "optional": true 2431 + }, 2432 + "yaml": { 2433 + "optional": true 2434 + } 2435 + } 2436 + }, 2437 + "node_modules/vite-node": { 2438 + "version": "3.0.9", 2439 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", 2440 + "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", 2441 + "dev": true, 2442 + "license": "MIT", 2443 + "dependencies": { 2444 + "cac": "^6.7.14", 2445 + "debug": "^4.4.0", 2446 + "es-module-lexer": "^1.6.0", 2447 + "pathe": "^2.0.3", 2448 + "vite": "^5.0.0 || ^6.0.0" 2449 + }, 2450 + "bin": { 2451 + "vite-node": "vite-node.mjs" 2452 + }, 2453 + "engines": { 2454 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2455 + }, 2456 + "funding": { 2457 + "url": "https://opencollective.com/vitest" 2458 + } 2459 + }, 2460 + "node_modules/vitest": { 2461 + "version": "3.0.9", 2462 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", 2463 + "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", 2464 + "dev": true, 2465 + "license": "MIT", 2466 + "dependencies": { 2467 + "@vitest/expect": "3.0.9", 2468 + "@vitest/mocker": "3.0.9", 2469 + "@vitest/pretty-format": "^3.0.9", 2470 + "@vitest/runner": "3.0.9", 2471 + "@vitest/snapshot": "3.0.9", 2472 + "@vitest/spy": "3.0.9", 2473 + "@vitest/utils": "3.0.9", 2474 + "chai": "^5.2.0", 2475 + "debug": "^4.4.0", 2476 + "expect-type": "^1.1.0", 2477 + "magic-string": "^0.30.17", 2478 + "pathe": "^2.0.3", 2479 + "std-env": "^3.8.0", 2480 + "tinybench": "^2.9.0", 2481 + "tinyexec": "^0.3.2", 2482 + "tinypool": "^1.0.2", 2483 + "tinyrainbow": "^2.0.0", 2484 + "vite": "^5.0.0 || ^6.0.0", 2485 + "vite-node": "3.0.9", 2486 + "why-is-node-running": "^2.3.0" 2487 + }, 2488 + "bin": { 2489 + "vitest": "vitest.mjs" 2490 + }, 2491 + "engines": { 2492 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2493 + }, 2494 + "funding": { 2495 + "url": "https://opencollective.com/vitest" 2496 + }, 2497 + "peerDependencies": { 2498 + "@edge-runtime/vm": "*", 2499 + "@types/debug": "^4.1.12", 2500 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2501 + "@vitest/browser": "3.0.9", 2502 + "@vitest/ui": "3.0.9", 2503 + "happy-dom": "*", 2504 + "jsdom": "*" 2505 + }, 2506 + "peerDependenciesMeta": { 2507 + "@edge-runtime/vm": { 2508 + "optional": true 2509 + }, 2510 + "@types/debug": { 2511 + "optional": true 2512 + }, 2513 + "@types/node": { 2514 + "optional": true 2515 + }, 2516 + "@vitest/browser": { 2517 + "optional": true 2518 + }, 2519 + "@vitest/ui": { 2520 + "optional": true 2521 + }, 2522 + "happy-dom": { 2523 + "optional": true 2524 + }, 2525 + "jsdom": { 2526 + "optional": true 2527 + } 2528 + } 2529 + }, 2530 + "node_modules/why-is-node-running": { 2531 + "version": "2.3.0", 2532 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 2533 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 2534 + "dev": true, 2535 + "license": "MIT", 2536 + "dependencies": { 2537 + "siginfo": "^2.0.0", 2538 + "stackback": "0.0.2" 2539 + }, 2540 + "bin": { 2541 + "why-is-node-running": "cli.js" 2542 + }, 2543 + "engines": { 2544 + "node": ">=8" 2545 + } 2546 + }, 2547 + "node_modules/workerd": { 2548 + "version": "1.20250428.0", 2549 + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250428.0.tgz", 2550 + "integrity": "sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==", 2551 + "dev": true, 2552 + "hasInstallScript": true, 2553 + "license": "Apache-2.0", 2554 + "bin": { 2555 + "workerd": "bin/workerd" 2556 + }, 2557 + "engines": { 2558 + "node": ">=16" 2559 + }, 2560 + "optionalDependencies": { 2561 + "@cloudflare/workerd-darwin-64": "1.20250428.0", 2562 + "@cloudflare/workerd-darwin-arm64": "1.20250428.0", 2563 + "@cloudflare/workerd-linux-64": "1.20250428.0", 2564 + "@cloudflare/workerd-linux-arm64": "1.20250428.0", 2565 + "@cloudflare/workerd-windows-64": "1.20250428.0" 2566 + } 2567 + }, 2568 + "node_modules/wrangler": { 2569 + "version": "4.14.1", 2570 + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.14.1.tgz", 2571 + "integrity": "sha512-EU7IThP7i68TBftJJSveogvWZ5k/WRijcJh3UclDWiWWhDZTPbL6LOJEFhHKqFzHOaC4Y2Aewt48rfTz0e7oCw==", 2572 + "dev": true, 2573 + "license": "MIT OR Apache-2.0", 2574 + "dependencies": { 2575 + "@cloudflare/kv-asset-handler": "0.4.0", 2576 + "@cloudflare/unenv-preset": "2.3.1", 2577 + "blake3-wasm": "2.1.5", 2578 + "esbuild": "0.25.2", 2579 + "miniflare": "4.20250428.1", 2580 + "path-to-regexp": "6.3.0", 2581 + "unenv": "2.0.0-rc.15", 2582 + "workerd": "1.20250428.0" 2583 + }, 2584 + "bin": { 2585 + "wrangler": "bin/wrangler.js", 2586 + "wrangler2": "bin/wrangler.js" 2587 + }, 2588 + "engines": { 2589 + "node": ">=18.0.0" 2590 + }, 2591 + "optionalDependencies": { 2592 + "fsevents": "~2.3.2", 2593 + "sharp": "^0.33.5" 2594 + }, 2595 + "peerDependencies": { 2596 + "@cloudflare/workers-types": "^4.20250428.0" 2597 + }, 2598 + "peerDependenciesMeta": { 2599 + "@cloudflare/workers-types": { 2600 + "optional": true 2601 + } 2602 + } 2603 + }, 2604 + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { 2605 + "version": "0.25.2", 2606 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", 2607 + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", 2608 + "cpu": [ 2609 + "ppc64" 2610 + ], 2611 + "dev": true, 2612 + "license": "MIT", 2613 + "optional": true, 2614 + "os": [ 2615 + "aix" 2616 + ], 2617 + "engines": { 2618 + "node": ">=18" 2619 + } 2620 + }, 2621 + "node_modules/wrangler/node_modules/@esbuild/android-arm": { 2622 + "version": "0.25.2", 2623 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", 2624 + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", 2625 + "cpu": [ 2626 + "arm" 2627 + ], 2628 + "dev": true, 2629 + "license": "MIT", 2630 + "optional": true, 2631 + "os": [ 2632 + "android" 2633 + ], 2634 + "engines": { 2635 + "node": ">=18" 2636 + } 2637 + }, 2638 + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { 2639 + "version": "0.25.2", 2640 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", 2641 + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", 2642 + "cpu": [ 2643 + "arm64" 2644 + ], 2645 + "dev": true, 2646 + "license": "MIT", 2647 + "optional": true, 2648 + "os": [ 2649 + "android" 2650 + ], 2651 + "engines": { 2652 + "node": ">=18" 2653 + } 2654 + }, 2655 + "node_modules/wrangler/node_modules/@esbuild/android-x64": { 2656 + "version": "0.25.2", 2657 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", 2658 + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", 2659 + "cpu": [ 2660 + "x64" 2661 + ], 2662 + "dev": true, 2663 + "license": "MIT", 2664 + "optional": true, 2665 + "os": [ 2666 + "android" 2667 + ], 2668 + "engines": { 2669 + "node": ">=18" 2670 + } 2671 + }, 2672 + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { 2673 + "version": "0.25.2", 2674 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", 2675 + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", 2676 + "cpu": [ 2677 + "arm64" 2678 + ], 2679 + "dev": true, 2680 + "license": "MIT", 2681 + "optional": true, 2682 + "os": [ 2683 + "darwin" 2684 + ], 2685 + "engines": { 2686 + "node": ">=18" 2687 + } 2688 + }, 2689 + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { 2690 + "version": "0.25.2", 2691 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", 2692 + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", 2693 + "cpu": [ 2694 + "x64" 2695 + ], 2696 + "dev": true, 2697 + "license": "MIT", 2698 + "optional": true, 2699 + "os": [ 2700 + "darwin" 2701 + ], 2702 + "engines": { 2703 + "node": ">=18" 2704 + } 2705 + }, 2706 + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { 2707 + "version": "0.25.2", 2708 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", 2709 + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", 2710 + "cpu": [ 2711 + "arm64" 2712 + ], 2713 + "dev": true, 2714 + "license": "MIT", 2715 + "optional": true, 2716 + "os": [ 2717 + "freebsd" 2718 + ], 2719 + "engines": { 2720 + "node": ">=18" 2721 + } 2722 + }, 2723 + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { 2724 + "version": "0.25.2", 2725 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", 2726 + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", 2727 + "cpu": [ 2728 + "x64" 2729 + ], 2730 + "dev": true, 2731 + "license": "MIT", 2732 + "optional": true, 2733 + "os": [ 2734 + "freebsd" 2735 + ], 2736 + "engines": { 2737 + "node": ">=18" 2738 + } 2739 + }, 2740 + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { 2741 + "version": "0.25.2", 2742 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", 2743 + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", 2744 + "cpu": [ 2745 + "arm" 2746 + ], 2747 + "dev": true, 2748 + "license": "MIT", 2749 + "optional": true, 2750 + "os": [ 2751 + "linux" 2752 + ], 2753 + "engines": { 2754 + "node": ">=18" 2755 + } 2756 + }, 2757 + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { 2758 + "version": "0.25.2", 2759 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", 2760 + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", 2761 + "cpu": [ 2762 + "arm64" 2763 + ], 2764 + "dev": true, 2765 + "license": "MIT", 2766 + "optional": true, 2767 + "os": [ 2768 + "linux" 2769 + ], 2770 + "engines": { 2771 + "node": ">=18" 2772 + } 2773 + }, 2774 + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { 2775 + "version": "0.25.2", 2776 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", 2777 + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", 2778 + "cpu": [ 2779 + "ia32" 2780 + ], 2781 + "dev": true, 2782 + "license": "MIT", 2783 + "optional": true, 2784 + "os": [ 2785 + "linux" 2786 + ], 2787 + "engines": { 2788 + "node": ">=18" 2789 + } 2790 + }, 2791 + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { 2792 + "version": "0.25.2", 2793 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", 2794 + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", 2795 + "cpu": [ 2796 + "loong64" 2797 + ], 2798 + "dev": true, 2799 + "license": "MIT", 2800 + "optional": true, 2801 + "os": [ 2802 + "linux" 2803 + ], 2804 + "engines": { 2805 + "node": ">=18" 2806 + } 2807 + }, 2808 + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { 2809 + "version": "0.25.2", 2810 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", 2811 + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", 2812 + "cpu": [ 2813 + "mips64el" 2814 + ], 2815 + "dev": true, 2816 + "license": "MIT", 2817 + "optional": true, 2818 + "os": [ 2819 + "linux" 2820 + ], 2821 + "engines": { 2822 + "node": ">=18" 2823 + } 2824 + }, 2825 + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { 2826 + "version": "0.25.2", 2827 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", 2828 + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", 2829 + "cpu": [ 2830 + "ppc64" 2831 + ], 2832 + "dev": true, 2833 + "license": "MIT", 2834 + "optional": true, 2835 + "os": [ 2836 + "linux" 2837 + ], 2838 + "engines": { 2839 + "node": ">=18" 2840 + } 2841 + }, 2842 + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { 2843 + "version": "0.25.2", 2844 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", 2845 + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", 2846 + "cpu": [ 2847 + "riscv64" 2848 + ], 2849 + "dev": true, 2850 + "license": "MIT", 2851 + "optional": true, 2852 + "os": [ 2853 + "linux" 2854 + ], 2855 + "engines": { 2856 + "node": ">=18" 2857 + } 2858 + }, 2859 + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { 2860 + "version": "0.25.2", 2861 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", 2862 + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", 2863 + "cpu": [ 2864 + "s390x" 2865 + ], 2866 + "dev": true, 2867 + "license": "MIT", 2868 + "optional": true, 2869 + "os": [ 2870 + "linux" 2871 + ], 2872 + "engines": { 2873 + "node": ">=18" 2874 + } 2875 + }, 2876 + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { 2877 + "version": "0.25.2", 2878 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", 2879 + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", 2880 + "cpu": [ 2881 + "x64" 2882 + ], 2883 + "dev": true, 2884 + "license": "MIT", 2885 + "optional": true, 2886 + "os": [ 2887 + "linux" 2888 + ], 2889 + "engines": { 2890 + "node": ">=18" 2891 + } 2892 + }, 2893 + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { 2894 + "version": "0.25.2", 2895 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", 2896 + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", 2897 + "cpu": [ 2898 + "arm64" 2899 + ], 2900 + "dev": true, 2901 + "license": "MIT", 2902 + "optional": true, 2903 + "os": [ 2904 + "netbsd" 2905 + ], 2906 + "engines": { 2907 + "node": ">=18" 2908 + } 2909 + }, 2910 + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { 2911 + "version": "0.25.2", 2912 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", 2913 + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", 2914 + "cpu": [ 2915 + "x64" 2916 + ], 2917 + "dev": true, 2918 + "license": "MIT", 2919 + "optional": true, 2920 + "os": [ 2921 + "netbsd" 2922 + ], 2923 + "engines": { 2924 + "node": ">=18" 2925 + } 2926 + }, 2927 + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { 2928 + "version": "0.25.2", 2929 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", 2930 + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", 2931 + "cpu": [ 2932 + "arm64" 2933 + ], 2934 + "dev": true, 2935 + "license": "MIT", 2936 + "optional": true, 2937 + "os": [ 2938 + "openbsd" 2939 + ], 2940 + "engines": { 2941 + "node": ">=18" 2942 + } 2943 + }, 2944 + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { 2945 + "version": "0.25.2", 2946 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", 2947 + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", 2948 + "cpu": [ 2949 + "x64" 2950 + ], 2951 + "dev": true, 2952 + "license": "MIT", 2953 + "optional": true, 2954 + "os": [ 2955 + "openbsd" 2956 + ], 2957 + "engines": { 2958 + "node": ">=18" 2959 + } 2960 + }, 2961 + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { 2962 + "version": "0.25.2", 2963 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", 2964 + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", 2965 + "cpu": [ 2966 + "x64" 2967 + ], 2968 + "dev": true, 2969 + "license": "MIT", 2970 + "optional": true, 2971 + "os": [ 2972 + "sunos" 2973 + ], 2974 + "engines": { 2975 + "node": ">=18" 2976 + } 2977 + }, 2978 + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { 2979 + "version": "0.25.2", 2980 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", 2981 + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", 2982 + "cpu": [ 2983 + "arm64" 2984 + ], 2985 + "dev": true, 2986 + "license": "MIT", 2987 + "optional": true, 2988 + "os": [ 2989 + "win32" 2990 + ], 2991 + "engines": { 2992 + "node": ">=18" 2993 + } 2994 + }, 2995 + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { 2996 + "version": "0.25.2", 2997 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", 2998 + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", 2999 + "cpu": [ 3000 + "ia32" 3001 + ], 3002 + "dev": true, 3003 + "license": "MIT", 3004 + "optional": true, 3005 + "os": [ 3006 + "win32" 3007 + ], 3008 + "engines": { 3009 + "node": ">=18" 3010 + } 3011 + }, 3012 + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { 3013 + "version": "0.25.2", 3014 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", 3015 + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", 3016 + "cpu": [ 3017 + "x64" 3018 + ], 3019 + "dev": true, 3020 + "license": "MIT", 3021 + "optional": true, 3022 + "os": [ 3023 + "win32" 3024 + ], 3025 + "engines": { 3026 + "node": ">=18" 3027 + } 3028 + }, 3029 + "node_modules/wrangler/node_modules/esbuild": { 3030 + "version": "0.25.2", 3031 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", 3032 + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 3033 + "dev": true, 3034 + "hasInstallScript": true, 3035 + "license": "MIT", 3036 + "bin": { 3037 + "esbuild": "bin/esbuild" 3038 + }, 3039 + "engines": { 3040 + "node": ">=18" 3041 + }, 3042 + "optionalDependencies": { 3043 + "@esbuild/aix-ppc64": "0.25.2", 3044 + "@esbuild/android-arm": "0.25.2", 3045 + "@esbuild/android-arm64": "0.25.2", 3046 + "@esbuild/android-x64": "0.25.2", 3047 + "@esbuild/darwin-arm64": "0.25.2", 3048 + "@esbuild/darwin-x64": "0.25.2", 3049 + "@esbuild/freebsd-arm64": "0.25.2", 3050 + "@esbuild/freebsd-x64": "0.25.2", 3051 + "@esbuild/linux-arm": "0.25.2", 3052 + "@esbuild/linux-arm64": "0.25.2", 3053 + "@esbuild/linux-ia32": "0.25.2", 3054 + "@esbuild/linux-loong64": "0.25.2", 3055 + "@esbuild/linux-mips64el": "0.25.2", 3056 + "@esbuild/linux-ppc64": "0.25.2", 3057 + "@esbuild/linux-riscv64": "0.25.2", 3058 + "@esbuild/linux-s390x": "0.25.2", 3059 + "@esbuild/linux-x64": "0.25.2", 3060 + "@esbuild/netbsd-arm64": "0.25.2", 3061 + "@esbuild/netbsd-x64": "0.25.2", 3062 + "@esbuild/openbsd-arm64": "0.25.2", 3063 + "@esbuild/openbsd-x64": "0.25.2", 3064 + "@esbuild/sunos-x64": "0.25.2", 3065 + "@esbuild/win32-arm64": "0.25.2", 3066 + "@esbuild/win32-ia32": "0.25.2", 3067 + "@esbuild/win32-x64": "0.25.2" 3068 + } 3069 + }, 3070 + "node_modules/ws": { 3071 + "version": "8.18.0", 3072 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 3073 + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 3074 + "dev": true, 3075 + "license": "MIT", 3076 + "engines": { 3077 + "node": ">=10.0.0" 3078 + }, 3079 + "peerDependencies": { 3080 + "bufferutil": "^4.0.1", 3081 + "utf-8-validate": ">=5.0.2" 3082 + }, 3083 + "peerDependenciesMeta": { 3084 + "bufferutil": { 3085 + "optional": true 3086 + }, 3087 + "utf-8-validate": { 3088 + "optional": true 3089 + } 3090 + } 3091 + }, 3092 + "node_modules/youch": { 3093 + "version": "3.3.4", 3094 + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", 3095 + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", 3096 + "dev": true, 3097 + "license": "MIT", 3098 + "dependencies": { 3099 + "cookie": "^0.7.1", 3100 + "mustache": "^4.2.0", 3101 + "stacktracey": "^2.1.8" 3102 + } 3103 + }, 3104 + "node_modules/zod": { 3105 + "version": "3.24.3", 3106 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", 3107 + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", 3108 + "dev": true, 3109 + "license": "MIT", 3110 + "funding": { 3111 + "url": "https://github.com/sponsors/colinhacks" 3112 + } 3113 + } 3114 + } 3024 3115 }
+18 -14
avatar/package.json
··· 1 1 { 2 - "name": "avatar", 3 - "version": "0.0.0", 4 - "private": true, 5 - "scripts": { 6 - "deploy": "wrangler deploy", 7 - "dev": "wrangler dev", 8 - "start": "wrangler dev", 9 - "test": "vitest" 10 - }, 11 - "devDependencies": { 12 - "@cloudflare/vitest-pool-workers": "^0.8.19", 13 - "vitest": "~3.0.7", 14 - "wrangler": "^4.14.1" 15 - } 2 + "name": "avatar", 3 + "version": "0.0.0", 4 + "private": true, 5 + "type": "module", 6 + "scripts": { 7 + "deploy": "wrangler deploy", 8 + "dev": "wrangler dev", 9 + "start": "wrangler dev", 10 + "test": "vitest" 11 + }, 12 + "dependencies": { 13 + "@atcute/identity-resolver": "^1.2.2" 14 + }, 15 + "devDependencies": { 16 + "@cloudflare/vitest-pool-workers": "^0.8.19", 17 + "vitest": "~3.0.7", 18 + "wrangler": "^4.14.1" 19 + } 16 20 }
+156 -9
avatar/src/index.js
··· 1 + import { 2 + LocalActorResolver, 3 + CompositeHandleResolver, 4 + DohJsonHandleResolver, 5 + WellKnownHandleResolver, 6 + CompositeDidDocumentResolver, 7 + PlcDidDocumentResolver, 8 + WebDidDocumentResolver, 9 + } from "@atcute/identity-resolver"; 10 + 11 + // Initialize resolvers for Cloudflare Workers 12 + const handleResolver = new CompositeHandleResolver({ 13 + strategy: "race", 14 + methods: { 15 + dns: new DohJsonHandleResolver({ 16 + dohUrl: "https://cloudflare-dns.com/dns-query", 17 + }), 18 + http: new WellKnownHandleResolver(), 19 + }, 20 + }); 21 + 22 + const didDocumentResolver = new CompositeDidDocumentResolver({ 23 + methods: { 24 + plc: new PlcDidDocumentResolver(), 25 + web: new WebDidDocumentResolver(), 26 + }, 27 + }); 28 + 29 + const actorResolver = new LocalActorResolver({ 30 + handleResolver, 31 + didDocumentResolver, 32 + }); 33 + 1 34 export default { 2 35 async fetch(request, env) { 3 36 // Helper function to generate a color from a string ··· 14 47 return color; 15 48 }; 16 49 50 + // Helper function to fetch Tangled profile from PDS 51 + const getTangledAvatarFromPDS = async (actor) => { 52 + try { 53 + // Resolve the identity 54 + const identity = await actorResolver.resolve(actor); 55 + if (!identity) { 56 + console.log({ 57 + level: "debug", 58 + message: "failed to resolve identity", 59 + actor: actor, 60 + }); 61 + return null; 62 + } 63 + 64 + const did = identity.did; 65 + const pdsEndpoint = identity.pds.replace(/\/$/, ""); // Remove trailing slash 66 + 67 + if (!pdsEndpoint) { 68 + console.log({ 69 + level: "debug", 70 + message: "no PDS endpoint found", 71 + actor: actor, 72 + did: did, 73 + }); 74 + return null; 75 + } 76 + 77 + const profileUrl = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=sh.tangled.actor.profile&rkey=self`; 78 + 79 + // Fetch the Tangled profile record from PDS 80 + const profileResponse = await fetch(profileUrl); 81 + 82 + if (!profileResponse.ok) { 83 + console.log({ 84 + level: "debug", 85 + message: "no Tangled profile found on PDS", 86 + actor: actor, 87 + status: profileResponse.status, 88 + }); 89 + return null; 90 + } 91 + 92 + const profileData = await profileResponse.json(); 93 + const avatarBlob = profileData?.value?.avatar; 94 + 95 + if (!avatarBlob) { 96 + console.log({ 97 + level: "debug", 98 + message: "Tangled profile has no avatar", 99 + actor: actor, 100 + }); 101 + return null; 102 + } 103 + 104 + // Extract CID from blob reference object 105 + // The ref might be an object with $link property or a string 106 + let avatarCID; 107 + if (typeof avatarBlob.ref === "string") { 108 + avatarCID = avatarBlob.ref; 109 + } else if (avatarBlob.ref?.$link) { 110 + avatarCID = avatarBlob.ref.$link; 111 + } else if (typeof avatarBlob === "string") { 112 + avatarCID = avatarBlob; 113 + } 114 + 115 + if (!avatarCID || typeof avatarCID !== "string") { 116 + console.log({ 117 + level: "warn", 118 + message: "could not extract valid CID from avatar blob", 119 + actor: actor, 120 + avatarBlob: avatarBlob, 121 + avatarBlobRef: avatarBlob.ref, 122 + }); 123 + return null; 124 + } 125 + 126 + // Construct blob URL (pdsEndpoint already has trailing slash removed) 127 + const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`; 128 + 129 + return blobUrl; 130 + } catch (e) { 131 + console.log({ 132 + level: "warn", 133 + message: "error fetching Tangled avatar from PDS", 134 + actor: actor, 135 + error: e.message, 136 + }); 137 + return null; 138 + } 139 + }; 140 + 17 141 const url = new URL(request.url); 18 142 const { pathname, searchParams } = url; 19 143 20 144 if (!pathname || pathname === "/") { 21 - return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare. 22 - You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`); 145 + return new Response( 146 + `This is Tangled's avatar service. It fetches your pretty avatar from your PDS, Bluesky, or generates a placeholder. 147 + You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`, 148 + ); 23 149 } 24 150 25 151 const size = searchParams.get("size"); ··· 68 194 } 69 195 70 196 try { 71 - const profileResponse = await fetch( 72 - `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, 73 - ); 74 - const profile = await profileResponse.json(); 75 - const avatar = profile.avatar; 197 + let avatarUrl = null; 198 + 199 + // Try to get Tangled avatar from user's PDS first 200 + avatarUrl = await getTangledAvatarFromPDS(actor); 201 + 202 + // If no Tangled avatar, fall back to Bluesky 203 + if (!avatarUrl) { 204 + console.log({ 205 + level: "debug", 206 + message: "no Tangled avatar, falling back to Bluesky", 207 + actor: actor, 208 + }); 209 + 210 + const profileResponse = await fetch( 211 + `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, 212 + ); 76 213 77 - let avatarUrl = profile.avatar; 214 + if (profileResponse.ok) { 215 + const profile = await profileResponse.json(); 216 + avatarUrl = profile.avatar; 217 + } 218 + } 78 219 79 220 if (!avatarUrl) { 80 221 // Generate a random color based on the actor string 222 + console.log({ 223 + level: "debug", 224 + message: "no avatar found, generating placeholder", 225 + actor: actor, 226 + }); 227 + 81 228 const bgColor = stringToColor(actor); 82 229 const size = resizeToTiny ? 32 : 128; 83 230 const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" fill="${bgColor}"/></svg>`; ··· 93 240 return response; 94 241 } 95 242 96 - // Resize if requested 243 + // Fetch and optionally resize the avatar 97 244 let avatarResponse; 98 245 if (resizeToTiny) { 99 246 avatarResponse = await fetch(avatarUrl, {
+13 -13
avatar/wrangler.jsonc
··· 1 1 { 2 - "$schema": "node_modules/wrangler/config-schema.json", 3 - "name": "avatar", 4 - "main": "src/index.js", 5 - "compatibility_date": "2025-05-03", 6 - "observability": { 7 - "enabled": true, 8 - }, 9 - "routes": [ 10 - { 11 - "pattern": "avatar.tangled.sh", 12 - "custom_domain": true, 13 - }, 14 - ], 2 + "$schema": "node_modules/wrangler/config-schema.json", 3 + "name": "avatar", 4 + "main": "src/index.js", 5 + "compatibility_date": "2025-05-03", 6 + "observability": { 7 + "enabled": true, 8 + }, 9 + "routes": [ 10 + { 11 + "pattern": "avatar.tangled.sh", 12 + "custom_domain": true, 13 + }, 14 + ], 15 15 }
+23 -10
cmd/dolly/main.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 + _ "embed" 5 6 "flag" 6 7 "fmt" 7 8 "image" ··· 16 17 "github.com/srwiley/oksvg" 17 18 "github.com/srwiley/rasterx" 18 19 "golang.org/x/image/draw" 19 - "tangled.org/core/appview/pages" 20 20 "tangled.org/core/ico" 21 21 ) 22 22 23 23 func main() { 24 24 var ( 25 - size string 26 - fillColor string 27 - output string 25 + size string 26 + fillColor string 27 + output string 28 + templatePath string 28 29 ) 29 30 31 + flag.StringVar(&templatePath, "template", "", "Path to dolly go-html template") 30 32 flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 31 33 flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 32 34 flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 33 35 flag.Parse() 36 + 37 + if templatePath == "" { 38 + fmt.Fprintf(os.Stderr, "Empty template path") 39 + os.Exit(1) 40 + } 34 41 35 42 width, height, err := parseSize(size) 36 43 if err != nil { ··· 52 59 os.Exit(1) 53 60 } 54 61 55 - svgData, err := dolly(fillColor) 62 + tpl, err := os.ReadFile(templatePath) 63 + if err != nil { 64 + fmt.Fprintf(os.Stderr, "Failed to read template from path %s: %v\n", templatePath, err) 65 + os.Exit(1) 66 + } 67 + 68 + svgData, err := dolly(string(tpl), fillColor) 56 69 if err != nil { 57 70 fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 58 71 os.Exit(1) ··· 84 97 fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 85 98 } 86 99 87 - func dolly(hexColor string) ([]byte, error) { 88 - tpl, err := template.New("dolly"). 89 - ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 100 + func dolly(tplString, hexColor string) ([]byte, error) { 101 + tpl, err := template.New("dolly").Parse(tplString) 90 102 if err != nil { 91 103 return nil, err 92 104 } 93 105 94 106 var svgData bytes.Buffer 95 - if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", pages.DollyParams{ 96 - FillColor: hexColor, 107 + if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", map[string]any{ 108 + "FillColor": hexColor, 109 + "Classes": "", 97 110 }); err != nil { 98 111 return nil, err 99 112 }
+36 -3
docs/DOCS.md
··· 375 375 KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 376 376 ``` 377 377 378 - If you run a Linux distribution that uses systemd, you can use the provided 379 - service file to run the server. Copy 380 - [`knotserver.service`](/systemd/knotserver.service) 378 + If you run a Linux distribution that uses systemd, you can 379 + use the provided service file to run the server. Copy 380 + [`knotserver.service`](https://tangled.org/tangled.org/core/blob/master/systemd/knotserver.service) 381 381 to `/etc/systemd/system/`. Then, run: 382 382 383 383 ``` ··· 692 692 NODE_ENV: "production" 693 693 MY_ENV_VAR: "MY_ENV_VALUE" 694 694 ``` 695 + 696 + By default, the following environment variables set: 697 + 698 + - `CI` - Always set to `true` to indicate a CI environment 699 + - `TANGLED_PIPELINE_ID` - The AT URI of the current pipeline 700 + - `TANGLED_REPO_KNOT` - The repository's knot hostname 701 + - `TANGLED_REPO_DID` - The DID of the repository owner 702 + - `TANGLED_REPO_NAME` - The name of the repository 703 + - `TANGLED_REPO_DEFAULT_BRANCH` - The default branch of the 704 + repository 705 + - `TANGLED_REPO_URL` - The full URL to the repository 706 + 707 + These variables are only available when the pipeline is 708 + triggered by a push: 709 + 710 + - `TANGLED_REF` - The full git reference (e.g., 711 + `refs/heads/main` or `refs/tags/v1.0.0`) 712 + - `TANGLED_REF_NAME` - The short name of the reference 713 + (e.g., `main` or `v1.0.0`) 714 + - `TANGLED_REF_TYPE` - The type of reference, either 715 + `branch` or `tag` 716 + - `TANGLED_SHA` - The commit SHA that triggered the pipeline 717 + - `TANGLED_COMMIT_SHA` - Alias for `TANGLED_SHA` 718 + 719 + These variables are only available when the pipeline is 720 + triggered by a pull request: 721 + 722 + - `TANGLED_PR_SOURCE_BRANCH` - The source branch of the pull 723 + request 724 + - `TANGLED_PR_TARGET_BRANCH` - The target branch of the pull 725 + request 726 + - `TANGLED_PR_SOURCE_SHA` - The commit SHA of the source 727 + branch 695 728 696 729 ### Steps 697 730
+3
flake.nix
··· 284 284 rm -f api/tangled/* 285 285 lexgen --build-file lexicon-build-config.json lexicons 286 286 sed -i.bak 's/\tutil/\/\/\tutil/' api/tangled/* 287 + # lexgen generates incomplete Marshaler/Unmarshaler for union types 288 + find api/tangled/*.go -not -name "cbor_gen.go" -exec \ 289 + sed -i '/^func.*\(MarshalCBOR\|UnmarshalCBOR\)/,/^}/ s/^/\/\/ /' {} + 287 290 ${pkgs.gotools}/bin/goimports -w api/tangled/* 288 291 go run ./cmd/cborgen/ 289 292 lexgen --build-file lexicon-build-config.json lexicons
+3 -3
go.mod
··· 5 5 require ( 6 6 github.com/Blank-Xu/sql-adapter v1.1.1 7 7 github.com/alecthomas/assert/v2 v2.11.0 8 - github.com/alecthomas/chroma/v2 v2.15.0 8 + github.com/alecthomas/chroma/v2 v2.23.1 9 9 github.com/avast/retry-go/v4 v4.6.1 10 10 github.com/blevesearch/bleve/v2 v2.5.3 11 11 github.com/bluekeyes/go-gitdiff v0.8.1 ··· 61 61 github.com/Microsoft/go-winio v0.6.2 // indirect 62 62 github.com/ProtonMail/go-crypto v1.3.0 // indirect 63 63 github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect 64 - github.com/alecthomas/repr v0.4.0 // indirect 64 + github.com/alecthomas/repr v0.5.2 // indirect 65 65 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 66 66 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 67 67 github.com/aymerick/douceur v0.2.0 // indirect ··· 224 224 225 225 replace github.com/bluekeyes/go-gitdiff => tangled.sh/oppi.li/go-gitdiff v0.8.2 226 226 227 - replace github.com/alecthomas/chroma/v2 => github.com/oppiliappan/chroma/v2 v2.19.0 227 + replace github.com/alecthomas/chroma/v2 => github.com/oppiliappan/chroma/v2 v2.24.2 228 228 229 229 // from bluesky-social/indigo 230 230 replace github.com/gocql/gocql => github.com/scylladb/gocql v1.14.4
+4 -3
go.sum
··· 13 13 github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= 14 14 github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 15 15 github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 16 - github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 17 16 github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 17 + github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= 18 + github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 18 19 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 19 20 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 20 21 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= ··· 412 413 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 413 414 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= 414 415 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= 415 - github.com/oppiliappan/chroma/v2 v2.19.0 h1:PN7/pb+6JRKCva30NPTtRJMlrOyzgpPpIroNzy4ekHU= 416 - github.com/oppiliappan/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 416 + github.com/oppiliappan/chroma/v2 v2.24.2 h1:lHB9tWQxDoHa6sYEDdFep8SX6FPMmAF+ocGUffFwujE= 417 + github.com/oppiliappan/chroma/v2 v2.24.2/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= 417 418 github.com/oppiliappan/go-git/v5 v5.17.0 h1:CuJnpcIDxr0oiNaSHMconovSWnowHznVDG+AhjGuSEo= 418 419 github.com/oppiliappan/go-git/v5 v5.17.0/go.mod h1:q/FE8C3SPMoRN7LoH9vRFiBzidAOBWJPS1CqVS8DN+w= 419 420 github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
+15 -2
input.css
··· 93 93 @apply block text-gray-900 text-sm font-bold py-2 uppercase dark:text-gray-100; 94 94 } 95 95 input { 96 - @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 96 + @apply p-3 border border-gray-100 block rounded bg-gray-50 focus:outline-none focus:ring-1 focus:ring-gray-200 p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400;; 97 97 } 98 98 textarea { 99 - @apply border border-gray-400 block rounded bg-gray-50 focus:ring-black p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 99 + @apply border border-gray-100 block rounded bg-gray-50 focus:outline-none focus:ring-1 focus:ring-gray-200 p-3 dark:bg-gray-800 dark:border-gray-600 dark:text-white dark:focus:ring-gray-400; 100 100 } 101 101 details summary::-webkit-details-marker { 102 102 display: none; ··· 119 119 hover:before:bg-gray-50 120 120 dark:hover:before:bg-gray-700 121 121 active:before:shadow-[inset_0_2px_2px_0_rgba(0,0,0,0.1)] 122 + focus:outline-none focus-visible:before:outline focus-visible:before:outline-2 focus-visible:before:outline-gray-400 123 + disabled:cursor-not-allowed disabled:opacity-50 124 + dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700; 125 + } 126 + 127 + .btn-flat { 128 + @apply relative z-10 inline-flex min-h-[30px] cursor-pointer items-center justify-center 129 + bg-transparent px-2 pb-[0.2rem] text-sm text-gray-900 130 + before:absolute before:inset-0 before:-z-10 before:block before:rounded 131 + before:border before:border-gray-200 before:bg-white 132 + before:content-[''] before:transition-all before:duration-150 before:ease-in-out 133 + hover:before:bg-gray-50 134 + dark:hover:before:bg-gray-700 122 135 focus:outline-none focus-visible:before:outline focus-visible:before:outline-2 focus-visible:before:outline-gray-400 123 136 disabled:cursor-not-allowed disabled:opacity-50 124 137 dark:text-gray-100 dark:before:bg-gray-800 dark:before:border-gray-700;
+3 -8
knotserver/git/diff.go
··· 64 64 65 65 for _, tf := range d.TextFragments { 66 66 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 67 - for _, l := range tf.Lines { 68 - switch l.Op { 69 - case gitdiff.OpAdd: 70 - nd.Stat.Insertions += 1 71 - case gitdiff.OpDelete: 72 - nd.Stat.Deletions += 1 73 - } 74 - } 67 + nd.Stat.Insertions += tf.LinesAdded 68 + nd.Stat.Deletions += tf.LinesDeleted 75 69 } 76 70 77 71 nd.Diff = append(nd.Diff, ndiff) 78 72 } 79 73 74 + nd.Stat.FilesChanged += len(diffs) 80 75 nd.Commit.FromGoGitCommit(c) 81 76 82 77 return &nd, nil
+4
knotserver/git/git.go
··· 76 76 return &g, nil 77 77 } 78 78 79 + func (g *GitRepo) Hash() plumbing.Hash { 80 + return g.h 81 + } 82 + 79 83 // re-open a repository and update references 80 84 func (g *GitRepo) Refresh() error { 81 85 refreshed, err := PlainOpen(g.path)
+35
knotserver/xrpc/repo_archive.go
··· 4 4 "compress/gzip" 5 5 "fmt" 6 6 "net/http" 7 + "net/url" 7 8 "strings" 8 9 9 10 "github.com/go-git/go-git/v5/plumbing" 10 11 12 + "tangled.org/core/api/tangled" 11 13 "tangled.org/core/knotserver/git" 12 14 xrpcerr "tangled.org/core/xrpc/errors" 13 15 ) ··· 47 49 repoParts := strings.Split(repo, "/") 48 50 repoName := repoParts[len(repoParts)-1] 49 51 52 + immutableLink, err := x.buildImmutableLink(repo, format, gr.Hash().String(), prefix) 53 + if err != nil { 54 + x.Logger.Error( 55 + "failed to build immutable link", 56 + "err", err.Error(), 57 + "repo", repo, 58 + "format", format, 59 + "ref", gr.Hash().String(), 60 + "prefix", prefix, 61 + ) 62 + } 63 + 50 64 safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 51 65 52 66 var archivePrefix string ··· 59 73 filename := fmt.Sprintf("%s-%s.tar.gz", repoName, safeRefFilename) 60 74 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 61 75 w.Header().Set("Content-Type", "application/gzip") 76 + w.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"immutable\"", immutableLink)) 62 77 63 78 gw := gzip.NewWriter(w) 64 79 defer gw.Close() ··· 79 94 return 80 95 } 81 96 } 97 + 98 + func (x *Xrpc) buildImmutableLink(repo string, format string, ref string, prefix string) (string, error) { 99 + scheme := "https" 100 + if x.Config.Server.Dev { 101 + scheme = "http" 102 + } 103 + 104 + u, err := url.Parse(scheme + "://" + x.Config.Server.Hostname + "/xrpc/" + tangled.RepoArchiveNSID) 105 + if err != nil { 106 + return "", err 107 + } 108 + 109 + params := url.Values{} 110 + params.Set("repo", repo) 111 + params.Set("format", format) 112 + params.Set("ref", ref) 113 + params.Set("prefix", prefix) 114 + 115 + return fmt.Sprintf("%s?%s", u.String(), params.Encode()), nil 116 + }
+7 -3
lexicons/actor/profile.json
··· 8 8 "key": "literal:self", 9 9 "record": { 10 10 "type": "object", 11 - "required": [ 12 - "bluesky" 13 - ], 11 + "required": ["bluesky"], 14 12 "properties": { 13 + "avatar": { 14 + "type": "blob", 15 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'", 16 + "accept": ["image/png", "image/jpeg"], 17 + "maxSize": 1000000 18 + }, 15 19 "description": { 16 20 "type": "string", 17 21 "description": "Free-form profile description text.",
+4 -4
nix/gomod2nix.toml
··· 20 20 version = "v2.11.0" 21 21 hash = "sha256-tDJCDKZ0R4qNA7hgMKWrpDyogt1802LCJDBCExxdqaU=" 22 22 [mod."github.com/alecthomas/chroma/v2"] 23 - version = "v2.19.0" 24 - hash = "sha256-dxsu43a+PvHg2jYR0Tfys6a8x6IVR+9oCGAh+fvL3SM=" 23 + version = "v2.24.2" 24 + hash = "sha256-Xz4DLZpn98rwaLmNNztK3PJu9MVxDLSrhJI82ZzyFZo=" 25 25 replaced = "github.com/oppiliappan/chroma/v2" 26 26 [mod."github.com/alecthomas/repr"] 27 - version = "v0.4.0" 28 - hash = "sha256-CyAzMSTfLGHDtfGXi91y7XMVpPUDNOKjsznb+osl9dU=" 27 + version = "v0.5.2" 28 + hash = "sha256-PfIeyHh7xTbDN0g2otuDyUOQqbgS4KftVC1JKZ+6sdM=" 29 29 [mod."github.com/anmitsu/go-shlex"] 30 30 version = "v0.0.0-20200514113438-38f4b401e2be" 31 31 hash = "sha256-L3Ak4X2z7WXq7vMKuiHCOJ29nlpajUQ08Sfb9T0yP54="
+25 -18
nix/pkgs/dolly.nix
··· 1 1 { 2 + lib, 2 3 buildGoApplication, 3 4 modules, 4 - src, 5 - }: 6 - buildGoApplication { 7 - pname = "dolly"; 8 - version = "0.1.0"; 9 - inherit src modules; 10 - 11 - # patch the static dir 12 - postUnpack = '' 13 - pushd source 14 - mkdir -p appview/pages/static 15 - touch appview/pages/static/x 16 - popd 17 - ''; 18 - 19 - doCheck = false; 20 - subPackages = ["cmd/dolly"]; 21 - } 5 + writeShellScriptBin, 6 + }: let 7 + src = lib.fileset.toSource { 8 + root = ../..; 9 + fileset = lib.fileset.unions [ 10 + ../../go.mod 11 + ../../ico 12 + ../../cmd/dolly/main.go 13 + ../../appview/pages/templates/fragments/dolly/logo.html 14 + ]; 15 + }; 16 + dolly-unwrapped = buildGoApplication { 17 + pname = "dolly-unwrapped"; 18 + version = "0.1.0"; 19 + inherit src modules; 20 + doCheck = false; 21 + subPackages = ["cmd/dolly"]; 22 + }; 23 + in 24 + writeShellScriptBin "dolly" '' 25 + exec ${dolly-unwrapped}/bin/dolly \ 26 + -template ${src}/appview/pages/templates/fragments/dolly/logo.html \ 27 + "$@" 28 + ''
+66 -10
patchutil/interdiff.go
··· 5 5 "strings" 6 6 7 7 "github.com/bluekeyes/go-gitdiff/gitdiff" 8 + "tangled.org/core/appview/filetree" 8 9 "tangled.org/core/types" 9 10 ) 10 11 ··· 12 13 Files []*InterdiffFile 13 14 } 14 15 15 - func (i *InterdiffResult) AffectedFiles() []string { 16 - files := make([]string, len(i.Files)) 17 - for _, f := range i.Files { 18 - files = append(files, f.Name) 16 + func (i *InterdiffResult) Stats() types.DiffStat { 17 + var ins, del int64 18 + for _, s := range i.ChangedFiles() { 19 + stat := s.Stats() 20 + ins += stat.Insertions 21 + del += stat.Deletions 22 + } 23 + return types.DiffStat{ 24 + Insertions: ins, 25 + Deletions: del, 26 + FilesChanged: len(i.Files), 19 27 } 20 - return files 28 + } 29 + 30 + func (i *InterdiffResult) ChangedFiles() []types.DiffFileRenderer { 31 + drs := make([]types.DiffFileRenderer, len(i.Files)) 32 + for i, s := range i.Files { 33 + drs[i] = s 34 + } 35 + return drs 36 + } 37 + 38 + func (i *InterdiffResult) FileTree() *filetree.FileTreeNode { 39 + fs := make([]string, len(i.Files)) 40 + for i, s := range i.Files { 41 + fs[i] = s.Name 42 + } 43 + return filetree.FileTree(fs) 21 44 } 22 45 23 46 func (i *InterdiffResult) String() string { ··· 36 59 Status InterdiffFileStatus 37 60 } 38 61 39 - func (s *InterdiffFile) Split() *types.SplitDiff { 62 + func (s *InterdiffFile) Id() string { 63 + return s.Name 64 + } 65 + 66 + func (s *InterdiffFile) Split() types.SplitDiff { 40 67 fragments := make([]types.SplitFragment, len(s.TextFragments)) 41 68 42 69 for i, fragment := range s.TextFragments { ··· 49 76 } 50 77 } 51 78 52 - return &types.SplitDiff{ 79 + return types.SplitDiff{ 53 80 Name: s.Id(), 54 81 TextFragments: fragments, 55 82 } 56 83 } 57 84 58 - // used by html elements as a unique ID for hrefs 59 - func (s *InterdiffFile) Id() string { 60 - return s.Name 85 + func (s *InterdiffFile) CanRender() string { 86 + if s.Status.IsUnchanged() { 87 + return "This file has not been changed." 88 + } else if s.Status.IsRebased() { 89 + return "This patch was likely rebased, as context lines do not match." 90 + } else if s.Status.IsError() { 91 + return "Failed to calculate interdiff for this file." 92 + } else { 93 + return "" 94 + } 95 + } 96 + 97 + func (s *InterdiffFile) Names() types.DiffFileName { 98 + var n types.DiffFileName 99 + n.New = s.Name 100 + return n 101 + } 102 + 103 + func (s *InterdiffFile) Stats() types.DiffFileStat { 104 + var ins, del int64 105 + 106 + if s.File != nil { 107 + for _, f := range s.TextFragments { 108 + ins += f.LinesAdded 109 + del += f.LinesDeleted 110 + } 111 + } 112 + 113 + return types.DiffFileStat{ 114 + Insertions: ins, 115 + Deletions: del, 116 + } 61 117 } 62 118 63 119 func (s *InterdiffFile) String() string {
+9
patchutil/patchutil_test.go
··· 4 4 "errors" 5 5 "reflect" 6 6 "testing" 7 + 8 + "tangled.org/core/types" 7 9 ) 8 10 9 11 func TestIsPatchValid(t *testing.T) { ··· 323 325 }) 324 326 } 325 327 } 328 + 329 + func TestImplsInterfaces(t *testing.T) { 330 + id := &InterdiffResult{} 331 + _ = isDiffsRenderer(id) 332 + } 333 + 334 + func isDiffsRenderer[S types.DiffRenderer](S) bool { return true }
+2 -2
spindle/models/models.go
··· 53 53 StatusKindRunning, 54 54 } 55 55 FinishStates [4]StatusKind = [4]StatusKind{ 56 - StatusKindCancelled, 57 56 StatusKindFailed, 58 - StatusKindSuccess, 59 57 StatusKindTimeout, 58 + StatusKindCancelled, 59 + StatusKindSuccess, 60 60 } 61 61 ) 62 62
+78 -30
types/diff.go
··· 1 1 package types 2 2 3 3 import ( 4 + "net/url" 5 + 4 6 "github.com/bluekeyes/go-gitdiff/gitdiff" 7 + "tangled.org/core/appview/filetree" 5 8 ) 6 9 7 10 type DiffOpts struct { 8 11 Split bool `json:"split"` 9 12 } 10 13 11 - type TextFragment struct { 12 - Header string `json:"comment"` 13 - Lines []gitdiff.Line `json:"lines"` 14 + func (d DiffOpts) Encode() string { 15 + values := make(url.Values) 16 + if d.Split { 17 + values.Set("diff", "split") 18 + } else { 19 + values.Set("diff", "unified") 20 + } 21 + return values.Encode() 22 + } 23 + 24 + // A nicer git diff representation. 25 + type NiceDiff struct { 26 + Commit Commit `json:"commit"` 27 + Stat DiffStat `json:"stat"` 28 + Diff []Diff `json:"diff"` 14 29 } 15 30 16 31 type Diff struct { ··· 26 41 IsRename bool `json:"is_rename"` 27 42 } 28 43 29 - type DiffStat struct { 30 - Insertions int64 31 - Deletions int64 32 - } 33 - 34 - func (d *Diff) Stats() DiffStat { 35 - var stats DiffStat 44 + func (d Diff) Stats() DiffFileStat { 45 + var stats DiffFileStat 36 46 for _, f := range d.TextFragments { 37 47 stats.Insertions += f.LinesAdded 38 48 stats.Deletions += f.LinesDeleted ··· 40 50 return stats 41 51 } 42 52 43 - // A nicer git diff representation. 44 - type NiceDiff struct { 45 - Commit Commit `json:"commit"` 46 - Stat struct { 47 - FilesChanged int `json:"files_changed"` 48 - Insertions int `json:"insertions"` 49 - Deletions int `json:"deletions"` 50 - } `json:"stat"` 51 - Diff []Diff `json:"diff"` 53 + type DiffStat struct { 54 + Insertions int64 `json:"insertions"` 55 + Deletions int64 `json:"deletions"` 56 + FilesChanged int `json:"files_changed"` 57 + } 58 + 59 + type DiffFileStat struct { 60 + Insertions int64 61 + Deletions int64 52 62 } 53 63 54 64 type DiffTree struct { ··· 58 68 Diff []*gitdiff.File `json:"diff"` 59 69 } 60 70 61 - func (d *NiceDiff) ChangedFiles() []string { 62 - files := make([]string, len(d.Diff)) 71 + type DiffFileName struct { 72 + Old string 73 + New string 74 + } 63 75 64 - for i, f := range d.Diff { 65 - if f.IsDelete { 66 - files[i] = f.Name.Old 76 + func (d NiceDiff) ChangedFiles() []DiffFileRenderer { 77 + drs := make([]DiffFileRenderer, len(d.Diff)) 78 + for i, s := range d.Diff { 79 + drs[i] = s 80 + } 81 + return drs 82 + } 83 + 84 + func (d NiceDiff) FileTree() *filetree.FileTreeNode { 85 + fs := make([]string, len(d.Diff)) 86 + for i, s := range d.Diff { 87 + n := s.Names() 88 + if n.New == "" { 89 + fs[i] = n.Old 67 90 } else { 68 - files[i] = f.Name.New 91 + fs[i] = n.New 69 92 } 70 93 } 94 + return filetree.FileTree(fs) 95 + } 71 96 72 - return files 97 + func (d NiceDiff) Stats() DiffStat { 98 + return d.Stat 73 99 } 74 100 75 - // used by html elements as a unique ID for hrefs 76 - func (d *Diff) Id() string { 101 + func (d Diff) Id() string { 77 102 if d.IsDelete { 78 103 return d.Name.Old 79 104 } 80 105 return d.Name.New 81 106 } 82 107 83 - func (d *Diff) Split() *SplitDiff { 108 + func (d Diff) Names() DiffFileName { 109 + var n DiffFileName 110 + if d.IsDelete { 111 + n.Old = d.Name.Old 112 + return n 113 + } else if d.IsCopy || d.IsRename { 114 + n.Old = d.Name.Old 115 + n.New = d.Name.New 116 + return n 117 + } else { 118 + n.New = d.Name.New 119 + return n 120 + } 121 + } 122 + 123 + func (d Diff) CanRender() string { 124 + if d.IsBinary { 125 + return "This is a binary file and will not be displayed." 126 + } 127 + 128 + return "" 129 + } 130 + 131 + func (d Diff) Split() SplitDiff { 84 132 fragments := make([]SplitFragment, len(d.TextFragments)) 85 133 for i, fragment := range d.TextFragments { 86 134 leftLines, rightLines := SeparateLines(&fragment) ··· 91 139 } 92 140 } 93 141 94 - return &SplitDiff{ 142 + return SplitDiff{ 95 143 Name: d.Id(), 96 144 TextFragments: fragments, 97 145 }
+31
types/diff_renderer.go
··· 1 + package types 2 + 3 + import "tangled.org/core/appview/filetree" 4 + 5 + type DiffRenderer interface { 6 + // list of file affected by these diffs 7 + ChangedFiles() []DiffFileRenderer 8 + 9 + // filetree 10 + FileTree() *filetree.FileTreeNode 11 + 12 + Stats() DiffStat 13 + } 14 + 15 + type DiffFileRenderer interface { 16 + // html ID for each file in the diff 17 + Id() string 18 + 19 + // produce a splitdiff 20 + Split() SplitDiff 21 + 22 + // stats for this single file 23 + Stats() DiffFileStat 24 + 25 + // old and new name of file 26 + Names() DiffFileName 27 + 28 + // whether this diff can be displayed, 29 + // returns a reason if not, and the empty string if it can 30 + CanRender() string 31 + }
+11 -2
types/diff_test.go
··· 1 1 package types 2 2 3 - import "testing" 3 + import ( 4 + "testing" 5 + ) 4 6 5 7 func TestDiffId(t *testing.T) { 6 8 tests := []struct { ··· 105 107 } 106 108 107 109 for i, diff := range nd.Diff { 108 - if changedFiles[i] != diff.Id() { 110 + if changedFiles[i].Id() != diff.Id() { 109 111 t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 110 112 } 111 113 } 112 114 } 115 + 116 + func TestImplsInterfaces(t *testing.T) { 117 + nd := NiceDiff{} 118 + _ = isDiffsRenderer(nd) 119 + } 120 + 121 + func isDiffsRenderer[S DiffRenderer](S) bool { return true }
+1 -2
types/split.go
··· 22 22 TextFragments []SplitFragment `json:"fragments"` 23 23 } 24 24 25 - // used by html elements as a unique ID for hrefs 26 - func (d *SplitDiff) Id() string { 25 + func (d SplitDiff) Id() string { 27 26 return d.Name 28 27 } 29 28