Monorepo for Tangled tangled.org

Compare changes

Choose any two refs to compare.

Changed files
+7189 -5266
api
appview
avatar
cmd
dolly
docs
hook
ico
knotserver
lexicons
actor
pulls
nix
sets
spindle
types
-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"` 23 21 // bluesky: Include link to this account on Bluesky. 24 22 Bluesky bool `json:"bluesky" cborgen:"bluesky"` 25 23 // description: Free-form profile description text.
+80 -64
api/tangled/cbor_gen.go
··· 26 26 } 27 27 28 28 cw := cbg.NewCborWriter(w) 29 - fieldCount := 9 30 - 31 - if t.Avatar == nil { 32 - fieldCount-- 33 - } 29 + fieldCount := 8 34 30 35 31 if t.Description == nil { 36 32 fieldCount-- ··· 148 144 return err 149 145 } 150 146 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 170 147 } 171 148 } 172 149 ··· 451 428 } 452 429 453 430 } 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 - 474 431 } 475 432 // t.Bluesky (bool) (bool) 476 433 case "bluesky": ··· 7977 7934 } 7978 7935 7979 7936 cw := cbg.NewCborWriter(w) 7980 - fieldCount := 9 7937 + fieldCount := 10 7981 7938 7982 7939 if t.Body == nil { 7983 7940 fieldCount-- 7984 7941 } 7985 7942 7986 7943 if t.Mentions == nil { 7944 + fieldCount-- 7945 + } 7946 + 7947 + if t.Patch == nil { 7987 7948 fieldCount-- 7988 7949 } 7989 7950 ··· 8051 8012 } 8052 8013 8053 8014 // t.Patch (string) (string) 8054 - if len("patch") > 1000000 { 8055 - return xerrors.Errorf("Value in field \"patch\" was too long") 8056 - } 8015 + if t.Patch != nil { 8057 8016 8058 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8059 - return err 8060 - } 8061 - if _, err := cw.WriteString(string("patch")); err != nil { 8062 - return err 8063 - } 8017 + if len("patch") > 1000000 { 8018 + return xerrors.Errorf("Value in field \"patch\" was too long") 8019 + } 8064 8020 8065 - if len(t.Patch) > 1000000 { 8066 - return xerrors.Errorf("Value in field t.Patch was too long") 8067 - } 8021 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patch"))); err != nil { 8022 + return err 8023 + } 8024 + if _, err := cw.WriteString(string("patch")); err != nil { 8025 + return err 8026 + } 8027 + 8028 + if t.Patch == nil { 8029 + if _, err := cw.Write(cbg.CborNull); err != nil { 8030 + return err 8031 + } 8032 + } else { 8033 + if len(*t.Patch) > 1000000 { 8034 + return xerrors.Errorf("Value in field t.Patch was too long") 8035 + } 8068 8036 8069 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Patch))); err != nil { 8070 - return err 8071 - } 8072 - if _, err := cw.WriteString(string(t.Patch)); err != nil { 8073 - return err 8037 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Patch))); err != nil { 8038 + return err 8039 + } 8040 + if _, err := cw.WriteString(string(*t.Patch)); err != nil { 8041 + return err 8042 + } 8043 + } 8074 8044 } 8075 8045 8076 8046 // t.Title (string) (string) ··· 8190 8160 return err 8191 8161 } 8192 8162 8163 + // t.PatchBlob (util.LexBlob) (struct) 8164 + if len("patchBlob") > 1000000 { 8165 + return xerrors.Errorf("Value in field \"patchBlob\" was too long") 8166 + } 8167 + 8168 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("patchBlob"))); err != nil { 8169 + return err 8170 + } 8171 + if _, err := cw.WriteString(string("patchBlob")); err != nil { 8172 + return err 8173 + } 8174 + 8175 + if err := t.PatchBlob.MarshalCBOR(cw); err != nil { 8176 + return err 8177 + } 8178 + 8193 8179 // t.References ([]string) (slice) 8194 8180 if t.References != nil { 8195 8181 ··· 8305 8291 case "patch": 8306 8292 8307 8293 { 8308 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 8294 + b, err := cr.ReadByte() 8309 8295 if err != nil { 8310 8296 return err 8311 8297 } 8298 + if b != cbg.CborNull[0] { 8299 + if err := cr.UnreadByte(); err != nil { 8300 + return err 8301 + } 8312 8302 8313 - t.Patch = string(sval) 8303 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 8304 + if err != nil { 8305 + return err 8306 + } 8307 + 8308 + t.Patch = (*string)(&sval) 8309 + } 8314 8310 } 8315 8311 // t.Title (string) (string) 8316 8312 case "title": ··· 8413 8409 } 8414 8410 8415 8411 t.CreatedAt = string(sval) 8412 + } 8413 + // t.PatchBlob (util.LexBlob) (struct) 8414 + case "patchBlob": 8415 + 8416 + { 8417 + 8418 + b, err := cr.ReadByte() 8419 + if err != nil { 8420 + return err 8421 + } 8422 + if b != cbg.CborNull[0] { 8423 + if err := cr.UnreadByte(); err != nil { 8424 + return err 8425 + } 8426 + t.PatchBlob = new(util.LexBlob) 8427 + if err := t.PatchBlob.UnmarshalCBOR(cr); err != nil { 8428 + return xerrors.Errorf("unmarshaling t.PatchBlob pointer: %w", err) 8429 + } 8430 + } 8431 + 8416 8432 } 8417 8433 // t.References ([]string) (slice) 8418 8434 case "references":
+12 -9
api/tangled/repopull.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoPull 19 19 type RepoPull struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 - Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 - Patch string `json:"patch" cborgen:"patch"` 25 - References []string `json:"references,omitempty" cborgen:"references,omitempty"` 26 - Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 27 - Target *RepoPull_Target `json:"target" cborgen:"target"` 28 - Title string `json:"title" cborgen:"title"` 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 + Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"` 24 + // patch: (deprecated) use patchBlob instead 25 + Patch *string `json:"patch,omitempty" cborgen:"patch,omitempty"` 26 + // patchBlob: patch content 27 + PatchBlob *util.LexBlob `json:"patchBlob" cborgen:"patchBlob"` 28 + References []string `json:"references,omitempty" cborgen:"references,omitempty"` 29 + Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 30 + Target *RepoPull_Target `json:"target" cborgen:"target"` 31 + Title string `json:"title" cborgen:"title"` 29 32 } 30 33 31 34 // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
+1 -9
appview/db/db.go
··· 260 260 did text not null, 261 261 262 262 -- data 263 - avatar text, 264 263 description text not null, 265 264 include_bluesky integer not null default 0, 266 265 location text, ··· 1079 1078 // transfer data, constructing pull_at from pulls table 1080 1079 _, err = tx.Exec(` 1081 1080 insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1082 - select 1081 + select 1083 1082 ps.id, 1084 1083 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1085 1084 ps.round_number, ··· 1170 1169 1171 1170 create index if not exists idx_stars_created on stars(created); 1172 1171 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; 1180 1172 `) 1181 1173 return err 1182 1174 })
+2
appview/db/follow.go
··· 167 167 if err != nil { 168 168 return nil, err 169 169 } 170 + defer rows.Close() 171 + 170 172 for rows.Next() { 171 173 var follow models.Follow 172 174 var followedAt string
+1
appview/db/issues.go
··· 452 452 if err != nil { 453 453 return nil, err 454 454 } 455 + defer rows.Close() 455 456 456 457 for rows.Next() { 457 458 var comment models.IssueComment
+1 -1
appview/db/language.go
··· 28 28 whereClause, 29 29 ) 30 30 rows, err := e.Query(query, args...) 31 - 32 31 if err != nil { 33 32 return nil, fmt.Errorf("failed to execute query: %w ", err) 34 33 } 34 + defer rows.Close() 35 35 36 36 var langs []models.RepoLanguage 37 37 for rows.Next() {
+26 -21
appview/db/profile.go
··· 20 20 timeline := models.ProfileTimeline{ 21 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 22 } 23 - currentMonth := time.Now().Month() 23 + now := time.Now() 24 24 timeframe := fmt.Sprintf("-%d months", TimeframeMonths) 25 25 26 26 pulls, err := GetPullsByOwnerDid(e, forDid, timeframe) ··· 30 30 31 31 // group pulls by month 32 32 for _, pull := range pulls { 33 - pullMonth := pull.Created.Month() 33 + monthsAgo := monthsBetween(pull.Created, now) 34 34 35 - if currentMonth-pullMonth >= TimeframeMonths { 35 + if monthsAgo >= TimeframeMonths { 36 36 // shouldn't happen; but times are weird 37 37 continue 38 38 } 39 39 40 - idx := currentMonth - pullMonth 40 + idx := monthsAgo 41 41 items := &timeline.ByMonth[idx].PullEvents.Items 42 42 43 43 *items = append(*items, &pull) ··· 53 53 } 54 54 55 55 for _, issue := range issues { 56 - issueMonth := issue.Created.Month() 56 + monthsAgo := monthsBetween(issue.Created, now) 57 57 58 - if currentMonth-issueMonth >= TimeframeMonths { 58 + if monthsAgo >= TimeframeMonths { 59 59 // shouldn't happen; but times are weird 60 60 continue 61 61 } 62 62 63 - idx := currentMonth - issueMonth 63 + idx := monthsAgo 64 64 items := &timeline.ByMonth[idx].IssueEvents.Items 65 65 66 66 *items = append(*items, &issue) ··· 77 77 if repo.Source != "" { 78 78 sourceRepo, err = GetRepoByAtUri(e, repo.Source) 79 79 if err != nil { 80 - return nil, err 80 + // the source repo was not found, skip this bit 81 + log.Println("profile", "err", err) 81 82 } 82 83 } 83 84 84 - repoMonth := repo.Created.Month() 85 + monthsAgo := monthsBetween(repo.Created, now) 85 86 86 - if currentMonth-repoMonth >= TimeframeMonths { 87 + if monthsAgo >= TimeframeMonths { 87 88 // shouldn't happen; but times are weird 88 89 continue 89 90 } 90 91 91 - idx := currentMonth - repoMonth 92 + idx := monthsAgo 92 93 93 94 items := &timeline.ByMonth[idx].RepoEvents 94 95 *items = append(*items, models.RepoEvent{ ··· 100 101 return &timeline, nil 101 102 } 102 103 104 + func monthsBetween(from, to time.Time) int { 105 + years := to.Year() - from.Year() 106 + months := int(to.Month() - from.Month()) 107 + return years*12 + months 108 + } 109 + 103 110 func UpsertProfile(tx *sql.Tx, profile *models.Profile) error { 104 111 defer tx.Rollback() 105 112 ··· 128 135 _, err = tx.Exec( 129 136 `insert or replace into profile ( 130 137 did, 131 - avatar, 132 138 description, 133 139 include_bluesky, 134 140 location, 135 141 pronouns 136 142 ) 137 - values (?, ?, ?, ?, ?, ?)`, 143 + values (?, ?, ?, ?, ?)`, 138 144 profile.Did, 139 - profile.Avatar, 140 145 profile.Description, 141 146 includeBskyValue, 142 147 profile.Location, ··· 232 237 if err != nil { 233 238 return nil, err 234 239 } 240 + defer rows.Close() 235 241 236 242 profileMap := make(map[string]*models.Profile) 237 243 for rows.Next() { ··· 272 278 if err != nil { 273 279 return nil, err 274 280 } 281 + defer rows.Close() 282 + 275 283 idxs := make(map[string]int) 276 284 for did := range profileMap { 277 285 idxs[did] = 0 ··· 292 300 if err != nil { 293 301 return nil, err 294 302 } 303 + defer rows.Close() 304 + 295 305 idxs = make(map[string]int) 296 306 for did := range profileMap { 297 307 idxs[did] = 0 ··· 314 324 func GetProfile(e Execer, did string) (*models.Profile, error) { 315 325 var profile models.Profile 316 326 var pronouns sql.Null[string] 317 - var avatar sql.Null[string] 318 327 319 328 profile.Did = did 320 329 321 330 includeBluesky := 0 322 331 323 332 err := e.QueryRow( 324 - `select avatar, description, include_bluesky, location, pronouns from profile where did = ?`, 333 + `select description, include_bluesky, location, pronouns from profile where did = ?`, 325 334 did, 326 - ).Scan(&avatar, &profile.Description, &includeBluesky, &profile.Location, &pronouns) 335 + ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns) 327 336 if err == sql.ErrNoRows { 328 337 profile := models.Profile{} 329 338 profile.Did = did ··· 340 349 341 350 if pronouns.Valid { 342 351 profile.Pronouns = pronouns.V 343 - } 344 - 345 - if avatar.Valid { 346 - profile.Avatar = avatar.V 347 352 } 348 353 349 354 rows, err := e.Query(`select link from profile_links where did = ?`, did)
+1 -1
appview/db/punchcard.go
··· 78 78 punch.Count = int(count.Int64) 79 79 } 80 80 81 - punchcard.Punches[punch.Date.YearDay()] = punch 81 + punchcard.Punches[punch.Date.YearDay()-1] = punch 82 82 punchcard.Total += punch.Count 83 83 } 84 84
+1
appview/db/registration.go
··· 38 38 if err != nil { 39 39 return nil, err 40 40 } 41 + defer rows.Close() 41 42 42 43 for rows.Next() { 43 44 var createdAt string
+12 -1
appview/db/repos.go
··· 56 56 limitClause, 57 57 ) 58 58 rows, err := e.Query(repoQuery, args...) 59 - 60 59 if err != nil { 61 60 return nil, fmt.Errorf("failed to execute repo query: %w ", err) 62 61 } 62 + defer rows.Close() 63 63 64 64 for rows.Next() { 65 65 var repo models.Repo ··· 128 128 if err != nil { 129 129 return nil, fmt.Errorf("failed to execute labels query: %w ", err) 130 130 } 131 + defer rows.Close() 132 + 131 133 for rows.Next() { 132 134 var repoat, labelat string 133 135 if err := rows.Scan(&repoat, &labelat); err != nil { ··· 156 158 from repo_languages 157 159 where repo_at in (%s) 158 160 and is_default_ref = 1 161 + and language <> '' 159 162 ) 160 163 where rn = 1 161 164 `, ··· 165 168 if err != nil { 166 169 return nil, fmt.Errorf("failed to execute lang query: %w ", err) 167 170 } 171 + defer rows.Close() 172 + 168 173 for rows.Next() { 169 174 var repoat, lang string 170 175 if err := rows.Scan(&repoat, &lang); err != nil { ··· 191 196 if err != nil { 192 197 return nil, fmt.Errorf("failed to execute star-count query: %w ", err) 193 198 } 199 + defer rows.Close() 200 + 194 201 for rows.Next() { 195 202 var repoat string 196 203 var count int ··· 220 227 if err != nil { 221 228 return nil, fmt.Errorf("failed to execute issue-count query: %w ", err) 222 229 } 230 + defer rows.Close() 231 + 223 232 for rows.Next() { 224 233 var repoat string 225 234 var open, closed int ··· 261 270 if err != nil { 262 271 return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err) 263 272 } 273 + defer rows.Close() 274 + 264 275 for rows.Next() { 265 276 var repoat string 266 277 var open, merged, closed, deleted int
+1
appview/db/star.go
··· 165 165 if err != nil { 166 166 return nil, err 167 167 } 168 + defer rows.Close() 168 169 169 170 starMap := make(map[string][]models.Star) 170 171 for rows.Next() {
-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 - 293 288 description := "" 294 289 if record.Description != nil { 295 290 description = *record.Description ··· 330 325 331 326 profile := models.Profile{ 332 327 Did: did, 333 - Avatar: avatar, 334 328 Description: description, 335 329 IncludeBluesky: includeBluesky, 336 330 Location: location,
+2 -2
appview/issues/opengraph.go
··· 193 193 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 194 194 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 195 195 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 196 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 196 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 197 197 if err != nil { 198 - log.Printf("dolly silhouette not available (this is ok): %v", err) 198 + log.Printf("dolly not available (this is ok): %v", err) 199 199 } 200 200 201 201 // Draw "opened by @author" and date at the bottom with more spacing
-5
appview/knots/knots.go
··· 666 666 k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 667 667 return 668 668 } 669 - if memberId.Handle.IsInvalidHandle() { 670 - l.Error("failed to resolve member identity to handle") 671 - k.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 672 - return 673 - } 674 669 675 670 // remove from enforcer 676 671 err = k.Enforcer.RemoveKnotMember(domain, memberId.DID.String())
+4
appview/middleware/middleware.go
··· 223 223 ) 224 224 if err != nil { 225 225 log.Println("failed to resolve repo", "err", err) 226 + w.WriteHeader(http.StatusNotFound) 226 227 mw.pages.ErrorKnot404(w) 227 228 return 228 229 } ··· 240 241 f, err := mw.repoResolver.Resolve(r) 241 242 if err != nil { 242 243 log.Println("failed to fully resolve repo", err) 244 + w.WriteHeader(http.StatusNotFound) 243 245 mw.pages.ErrorKnot404(w) 244 246 return 245 247 } ··· 288 290 f, err := mw.repoResolver.Resolve(r) 289 291 if err != nil { 290 292 log.Println("failed to fully resolve repo", err) 293 + w.WriteHeader(http.StatusNotFound) 291 294 mw.pages.ErrorKnot404(w) 292 295 return 293 296 } ··· 324 327 f, err := mw.repoResolver.Resolve(r) 325 328 if err != nil { 326 329 log.Println("failed to fully resolve repo", err) 330 + w.WriteHeader(http.StatusNotFound) 327 331 mw.pages.ErrorKnot404(w) 328 332 return 329 333 }
-1
appview/models/profile.go
··· 13 13 Did string 14 14 15 15 // data 16 - Avatar string // CID of the avatar blob 17 16 Description string 18 17 IncludeBluesky bool 19 18 Location string
+1 -1
appview/models/pull.go
··· 83 83 Repo *Repo 84 84 } 85 85 86 + // NOTE: This method does not include patch blob in returned atproto record 86 87 func (p Pull) AsRecord() tangled.RepoPull { 87 88 var source *tangled.RepoPull_Source 88 89 if p.PullSource != nil { ··· 113 114 Repo: p.RepoAt.String(), 114 115 Branch: p.TargetBranch, 115 116 }, 116 - Patch: p.LatestPatch(), 117 117 Source: source, 118 118 } 119 119 return record
+67 -57
appview/notify/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "log" 6 - "maps" 7 6 "slices" 8 7 9 8 "github.com/bluesky-social/indigo/atproto/syntax" ··· 13 12 "tangled.org/core/appview/notify" 14 13 "tangled.org/core/idresolver" 15 14 "tangled.org/core/orm" 15 + "tangled.org/core/sets" 16 16 ) 17 17 18 18 const ( 19 - maxMentions = 5 19 + maxMentions = 8 20 20 ) 21 21 22 22 type databaseNotifier struct { ··· 50 50 } 51 51 52 52 actorDid := syntax.DID(star.Did) 53 - recipients := []syntax.DID{syntax.DID(repo.Did)} 53 + recipients := sets.Singleton(syntax.DID(repo.Did)) 54 54 eventType := models.NotificationTypeRepoStarred 55 55 entityType := "repo" 56 56 entityId := star.RepoAt.String() ··· 75 75 } 76 76 77 77 func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) { 78 - 79 - // build the recipients list 80 - // - owner of the repo 81 - // - collaborators in the repo 82 - var recipients []syntax.DID 83 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 84 78 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 85 79 if err != nil { 86 80 log.Printf("failed to fetch collaborators: %v", err) 87 81 return 88 82 } 83 + 84 + // build the recipients list 85 + // - owner of the repo 86 + // - collaborators in the repo 87 + // - remove users already mentioned 88 + recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 89 89 for _, c := range collaborators { 90 - recipients = append(recipients, c.SubjectDid) 90 + recipients.Insert(c.SubjectDid) 91 + } 92 + for _, m := range mentions { 93 + recipients.Remove(m) 91 94 } 92 95 93 96 actorDid := syntax.DID(issue.Did) ··· 109 112 ) 110 113 n.notifyEvent( 111 114 actorDid, 112 - mentions, 115 + sets.Collect(slices.Values(mentions)), 113 116 models.NotificationTypeUserMentioned, 114 117 entityType, 115 118 entityId, ··· 131 134 } 132 135 issue := issues[0] 133 136 134 - var recipients []syntax.DID 135 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 137 + // built the recipients list: 138 + // - the owner of the repo 139 + // - | if the comment is a reply -> everybody on that thread 140 + // | if the comment is a top level -> just the issue owner 141 + // - remove mentioned users from the recipients list 142 + recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 136 143 137 144 if comment.IsReply() { 138 145 // if this comment is a reply, then notify everybody in that thread 139 146 parentAtUri := *comment.ReplyTo 140 - allThreads := issue.CommentList() 141 147 142 148 // find the parent thread, and add all DIDs from here to the recipient list 143 - for _, t := range allThreads { 149 + for _, t := range issue.CommentList() { 144 150 if t.Self.AtUri().String() == parentAtUri { 145 - recipients = append(recipients, t.Participants()...) 151 + for _, p := range t.Participants() { 152 + recipients.Insert(p) 153 + } 146 154 } 147 155 } 148 156 } else { 149 157 // not a reply, notify just the issue author 150 - recipients = append(recipients, syntax.DID(issue.Did)) 158 + recipients.Insert(syntax.DID(issue.Did)) 159 + } 160 + 161 + for _, m := range mentions { 162 + recipients.Remove(m) 151 163 } 152 164 153 165 actorDid := syntax.DID(comment.Did) ··· 169 181 ) 170 182 n.notifyEvent( 171 183 actorDid, 172 - mentions, 184 + sets.Collect(slices.Values(mentions)), 173 185 models.NotificationTypeUserMentioned, 174 186 entityType, 175 187 entityId, ··· 185 197 186 198 func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) { 187 199 actorDid := syntax.DID(follow.UserDid) 188 - recipients := []syntax.DID{syntax.DID(follow.SubjectDid)} 200 + recipients := sets.Singleton(syntax.DID(follow.SubjectDid)) 189 201 eventType := models.NotificationTypeFollowed 190 202 entityType := "follow" 191 203 entityId := follow.UserDid ··· 213 225 log.Printf("NewPull: failed to get repos: %v", err) 214 226 return 215 227 } 216 - 217 - // build the recipients list 218 - // - owner of the repo 219 - // - collaborators in the repo 220 - var recipients []syntax.DID 221 - recipients = append(recipients, syntax.DID(repo.Did)) 222 228 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 223 229 if err != nil { 224 230 log.Printf("failed to fetch collaborators: %v", err) 225 231 return 226 232 } 233 + 234 + // build the recipients list 235 + // - owner of the repo 236 + // - collaborators in the repo 237 + recipients := sets.Singleton(syntax.DID(repo.Did)) 227 238 for _, c := range collaborators { 228 - recipients = append(recipients, c.SubjectDid) 239 + recipients.Insert(c.SubjectDid) 229 240 } 230 241 231 242 actorDid := syntax.DID(pull.OwnerDid) ··· 268 279 // build up the recipients list: 269 280 // - repo owner 270 281 // - all pull participants 271 - var recipients []syntax.DID 272 - recipients = append(recipients, syntax.DID(repo.Did)) 282 + // - remove those already mentioned 283 + recipients := sets.Singleton(syntax.DID(repo.Did)) 273 284 for _, p := range pull.Participants() { 274 - recipients = append(recipients, syntax.DID(p)) 285 + recipients.Insert(syntax.DID(p)) 286 + } 287 + for _, m := range mentions { 288 + recipients.Remove(m) 275 289 } 276 290 277 291 actorDid := syntax.DID(comment.OwnerDid) ··· 295 309 ) 296 310 n.notifyEvent( 297 311 actorDid, 298 - mentions, 312 + sets.Collect(slices.Values(mentions)), 299 313 models.NotificationTypeUserMentioned, 300 314 entityType, 301 315 entityId, ··· 322 336 } 323 337 324 338 func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) { 325 - // build up the recipients list: 326 - // - repo owner 327 - // - repo collaborators 328 - // - all issue participants 329 - var recipients []syntax.DID 330 - recipients = append(recipients, syntax.DID(issue.Repo.Did)) 331 339 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt())) 332 340 if err != nil { 333 341 log.Printf("failed to fetch collaborators: %v", err) 334 342 return 335 343 } 344 + 345 + // build up the recipients list: 346 + // - repo owner 347 + // - repo collaborators 348 + // - all issue participants 349 + recipients := sets.Singleton(syntax.DID(issue.Repo.Did)) 336 350 for _, c := range collaborators { 337 - recipients = append(recipients, c.SubjectDid) 351 + recipients.Insert(c.SubjectDid) 338 352 } 339 353 for _, p := range issue.Participants() { 340 - recipients = append(recipients, syntax.DID(p)) 354 + recipients.Insert(syntax.DID(p)) 341 355 } 342 356 343 357 entityType := "pull" ··· 373 387 return 374 388 } 375 389 376 - // build up the recipients list: 377 - // - repo owner 378 - // - all pull participants 379 - var recipients []syntax.DID 380 - recipients = append(recipients, syntax.DID(repo.Did)) 381 390 collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt())) 382 391 if err != nil { 383 392 log.Printf("failed to fetch collaborators: %v", err) 384 393 return 385 394 } 395 + 396 + // build up the recipients list: 397 + // - repo owner 398 + // - all pull participants 399 + recipients := sets.Singleton(syntax.DID(repo.Did)) 386 400 for _, c := range collaborators { 387 - recipients = append(recipients, c.SubjectDid) 401 + recipients.Insert(c.SubjectDid) 388 402 } 389 403 for _, p := range pull.Participants() { 390 - recipients = append(recipients, syntax.DID(p)) 404 + recipients.Insert(syntax.DID(p)) 391 405 } 392 406 393 407 entityType := "pull" ··· 423 437 424 438 func (n *databaseNotifier) notifyEvent( 425 439 actorDid syntax.DID, 426 - recipients []syntax.DID, 440 + recipients sets.Set[syntax.DID], 427 441 eventType models.NotificationType, 428 442 entityType string, 429 443 entityId string, ··· 431 445 issueId *int64, 432 446 pullId *int64, 433 447 ) { 434 - if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions { 435 - recipients = recipients[:maxMentions] 448 + // if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody 449 + if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions { 450 + return 436 451 } 437 - recipientSet := make(map[syntax.DID]struct{}) 438 - for _, did := range recipients { 439 - // everybody except actor themselves 440 - if did != actorDid { 441 - recipientSet[did] = struct{}{} 442 - } 443 - } 452 + 453 + recipients.Remove(actorDid) 444 454 445 455 prefMap, err := db.GetNotificationPreferences( 446 456 n.db, 447 - orm.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))), 457 + orm.FilterIn("user_did", slices.Collect(recipients.All())), 448 458 ) 449 459 if err != nil { 450 460 // failed to get prefs for users ··· 460 470 defer tx.Rollback() 461 471 462 472 // filter based on preferences 463 - for recipientDid := range recipientSet { 473 + for recipientDid := range recipients.All() { 464 474 prefs, ok := prefMap[recipientDid] 465 475 if !ok { 466 476 prefs = models.DefaultNotificationPreferences(recipientDid)
-1
appview/notify/merged_notifier.go
··· 39 39 v.Call(in) 40 40 }(n) 41 41 } 42 - wg.Wait() 43 42 } 44 43 45 44 func (m *mergedNotifier) NewRepo(ctx context.Context, repo *models.Repo) {
+9 -9
appview/ogcard/card.go
··· 334 334 return nil 335 335 } 336 336 337 - func (c *Card) DrawDollySilhouette(x, y, size int, iconColor color.Color) error { 337 + func (c *Card) DrawDolly(x, y, size int, iconColor color.Color) error { 338 338 tpl, err := template.New("dolly"). 339 - ParseFS(pages.Files, "templates/fragments/dolly/silhouette.html") 339 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 340 340 if err != nil { 341 - return fmt.Errorf("failed to read dolly silhouette template: %w", err) 341 + return fmt.Errorf("failed to read dolly template: %w", err) 342 342 } 343 343 344 344 var svgData bytes.Buffer 345 - if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/silhouette", nil); err != nil { 346 - return fmt.Errorf("failed to execute dolly silhouette template: %w", err) 345 + if err = tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", nil); err != nil { 346 + return fmt.Errorf("failed to execute dolly template: %w", err) 347 347 } 348 348 349 349 icon, err := BuildSVGIconFromData(svgData.Bytes(), iconColor) ··· 453 453 454 454 // Handle SVG separately 455 455 if contentType == "image/svg+xml" || strings.HasSuffix(url, ".svg") { 456 - return c.convertSVGToPNG(bodyBytes) 456 + return convertSVGToPNG(bodyBytes) 457 457 } 458 458 459 459 // Support content types are in-sync with the allowed custom avatar file types ··· 493 493 } 494 494 495 495 // convertSVGToPNG converts SVG data to a PNG image 496 - func (c *Card) convertSVGToPNG(svgData []byte) (image.Image, bool) { 496 + func convertSVGToPNG(svgData []byte) (image.Image, bool) { 497 497 // Parse the SVG 498 498 icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 499 499 if err != nil { ··· 547 547 draw.CatmullRom.Scale(scaledImg, scaledImg.Bounds(), img, srcBounds, draw.Src, nil) 548 548 549 549 // Draw the image with circular clipping 550 - for cy := 0; cy < size; cy++ { 551 - for cx := 0; cx < size; cx++ { 550 + for cy := range size { 551 + for cx := range size { 552 552 // Calculate distance from center 553 553 dx := float64(cx - center) 554 554 dy := float64(cy - center)
+6 -23
appview/pages/funcmap.go
··· 25 25 "github.com/dustin/go-humanize" 26 26 "github.com/go-enry/go-enry/v2" 27 27 "github.com/yuin/goldmark" 28 + emoji "github.com/yuin/goldmark-emoji" 28 29 "tangled.org/core/appview/filetree" 29 30 "tangled.org/core/appview/models" 30 31 "tangled.org/core/appview/pages/markup" ··· 261 262 }, 262 263 "description": func(text string) template.HTML { 263 264 p.rctx.RendererType = markup.RendererTypeDefault 264 - htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New()) 265 + htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New( 266 + goldmark.WithExtensions( 267 + emoji.Emoji, 268 + ), 269 + )) 265 270 sanitized := p.rctx.SanitizeDescription(htmlString) 266 271 return template.HTML(sanitized) 267 272 }, ··· 359 364 }, 360 365 "fullAvatar": func(handle string) string { 361 366 return p.AvatarUrl(handle, "") 362 - }, 363 - "placeholderAvatar": func(size string) template.HTML { 364 - sizeClass := "size-6" 365 - iconSize := "size-4" 366 - if size == "tiny" { 367 - sizeClass = "size-6" 368 - iconSize = "size-4" 369 - } else if size == "small" { 370 - sizeClass = "size-8" 371 - iconSize = "size-5" 372 - } else { 373 - sizeClass = "size-12" 374 - iconSize = "size-8" 375 - } 376 - icon, _ := p.icon("user-round", []string{iconSize, "text-gray-400", "dark:text-gray-500"}) 377 - 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)) 378 - }, 379 - "profileAvatarUrl": func(profile *models.Profile, size string) string { 380 - if profile != nil { 381 - return p.AvatarUrl(profile.Did, size) 382 - } 383 - return "" 384 367 }, 385 368 "langColor": enry.GetColor, 386 369 "layoutSide": func() string {
+13 -3
appview/pages/markup/extension/atlink.go
··· 35 35 return KindAt 36 36 } 37 37 38 - var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9.-]+)(\b)`) 38 + var atRegexp = regexp.MustCompile(`(^|\s|\()(@)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\b)`) 39 + var markdownLinkRegexp = regexp.MustCompile(`(?ms)\[.*\]\(.*\)`) 39 40 40 41 type atParser struct{} 41 42 ··· 55 56 if m == nil { 56 57 return nil 57 58 } 59 + 60 + // Check for all links in the markdown to see if the handle found is inside one 61 + linksIndexes := markdownLinkRegexp.FindAllIndex(block.Source(), -1) 62 + for _, linkMatch := range linksIndexes { 63 + if linkMatch[0] < segment.Start && segment.Start < linkMatch[1] { 64 + return nil 65 + } 66 + } 67 + 58 68 atSegment := text.NewSegment(segment.Start, segment.Start+m[1]) 59 69 block.Advance(m[1]) 60 70 node := &AtNode{} ··· 87 97 88 98 func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 89 99 if entering { 90 - w.WriteString(`<a href="/@`) 100 + w.WriteString(`<a href="/`) 91 101 w.WriteString(n.(*AtNode).Handle) 92 - w.WriteString(`" class="mention font-bold">`) 102 + w.WriteString(`" class="mention">`) 93 103 } else { 94 104 w.WriteString("</a>") 95 105 }
+2
appview/pages/markup/markdown.go
··· 13 13 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 14 14 "github.com/alecthomas/chroma/v2/styles" 15 15 "github.com/yuin/goldmark" 16 + "github.com/yuin/goldmark-emoji" 16 17 highlighting "github.com/yuin/goldmark-highlighting/v2" 17 18 "github.com/yuin/goldmark/ast" 18 19 "github.com/yuin/goldmark/extension" ··· 66 67 ), 67 68 callout.CalloutExtention, 68 69 textension.AtExt, 70 + emoji.Emoji, 69 71 ), 70 72 goldmark.WithParserOptions( 71 73 parser.WithAutoHeadingID(),
+121
appview/pages/markup/markdown_test.go
··· 1 + package markup 2 + 3 + import ( 4 + "bytes" 5 + "testing" 6 + ) 7 + 8 + func TestAtExtension_Rendering(t *testing.T) { 9 + tests := []struct { 10 + name string 11 + markdown string 12 + expected string 13 + }{ 14 + { 15 + name: "renders simple at mention", 16 + markdown: "Hello @user.tngl.sh!", 17 + expected: `<p>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>!</p>`, 18 + }, 19 + { 20 + name: "renders multiple at mentions", 21 + markdown: "Hi @alice.tngl.sh and @bob.example.com", 22 + expected: `<p>Hi <a href="/alice.tngl.sh" class="mention">@alice.tngl.sh</a> and <a href="/bob.example.com" class="mention">@bob.example.com</a></p>`, 23 + }, 24 + { 25 + name: "renders at mention in parentheses", 26 + markdown: "Check this out (@user.tngl.sh)", 27 + expected: `<p>Check this out (<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>)</p>`, 28 + }, 29 + { 30 + name: "does not render email", 31 + markdown: "Contact me at test@example.com", 32 + expected: `<p>Contact me at <a href="mailto:test@example.com">test@example.com</a></p>`, 33 + }, 34 + { 35 + name: "renders at mention with hyphen", 36 + markdown: "Follow @user-name.tngl.sh", 37 + expected: `<p>Follow <a href="/user-name.tngl.sh" class="mention">@user-name.tngl.sh</a></p>`, 38 + }, 39 + { 40 + name: "renders at mention with numbers", 41 + markdown: "@user123.test456.social", 42 + expected: `<p><a href="/user123.test456.social" class="mention">@user123.test456.social</a></p>`, 43 + }, 44 + { 45 + name: "at mention at start of line", 46 + markdown: "@user.tngl.sh is cool", 47 + expected: `<p><a href="/user.tngl.sh" class="mention">@user.tngl.sh</a> is cool</p>`, 48 + }, 49 + } 50 + 51 + for _, tt := range tests { 52 + t.Run(tt.name, func(t *testing.T) { 53 + md := NewMarkdown() 54 + 55 + var buf bytes.Buffer 56 + if err := md.Convert([]byte(tt.markdown), &buf); err != nil { 57 + t.Fatalf("failed to convert markdown: %v", err) 58 + } 59 + 60 + result := buf.String() 61 + if result != tt.expected+"\n" { 62 + t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, result) 63 + } 64 + }) 65 + } 66 + } 67 + 68 + func TestAtExtension_WithOtherMarkdown(t *testing.T) { 69 + tests := []struct { 70 + name string 71 + markdown string 72 + contains string 73 + }{ 74 + { 75 + name: "at mention with bold", 76 + markdown: "**Hello @user.tngl.sh**", 77 + contains: `<strong>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></strong>`, 78 + }, 79 + { 80 + name: "at mention with italic", 81 + markdown: "*Check @user.tngl.sh*", 82 + contains: `<em>Check <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></em>`, 83 + }, 84 + { 85 + name: "at mention in list", 86 + markdown: "- Item 1\n- @user.tngl.sh\n- Item 3", 87 + contains: `<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>`, 88 + }, 89 + { 90 + name: "at mention in link", 91 + markdown: "[@regnault.dev](https://regnault.dev)", 92 + contains: `<a href="https://regnault.dev">@regnault.dev</a>`, 93 + }, 94 + { 95 + name: "at mention in link again", 96 + markdown: "[check out @regnault.dev](https://regnault.dev)", 97 + contains: `<a href="https://regnault.dev">check out @regnault.dev</a>`, 98 + }, 99 + { 100 + name: "at mention in link again, multiline", 101 + markdown: "[\ncheck out @regnault.dev](https://regnault.dev)", 102 + contains: "<a href=\"https://regnault.dev\">\ncheck out @regnault.dev</a>", 103 + }, 104 + } 105 + 106 + for _, tt := range tests { 107 + t.Run(tt.name, func(t *testing.T) { 108 + md := NewMarkdown() 109 + 110 + var buf bytes.Buffer 111 + if err := md.Convert([]byte(tt.markdown), &buf); err != nil { 112 + t.Fatalf("failed to convert markdown: %v", err) 113 + } 114 + 115 + result := buf.String() 116 + if !bytes.Contains([]byte(result), []byte(tt.contains)) { 117 + t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result) 118 + } 119 + }) 120 + } 121 + }
+13 -2
appview/pages/pages.go
··· 210 210 return tpl.ExecuteTemplate(w, "layouts/base", params) 211 211 } 212 212 213 + type DollyParams struct { 214 + Classes string 215 + FillColor string 216 + } 217 + 218 + func (p *Pages) Dolly(w io.Writer, params DollyParams) error { 219 + return p.executePlain("fragments/dolly/logo", w, params) 220 + } 221 + 213 222 func (p *Pages) Favicon(w io.Writer) error { 214 - return p.executePlain("fragments/dolly/silhouette", w, nil) 223 + return p.Dolly(w, DollyParams{ 224 + Classes: "text-black dark:text-white", 225 + }) 215 226 } 216 227 217 228 type LoginParams struct { ··· 640 651 } 641 652 642 653 func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error { 643 - return p.executePlain("fragments/starBtn", w, params) 654 + return p.executePlain("fragments/starBtn-oob", w, params) 644 655 } 645 656 646 657 type RepoIndexParams struct {
+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://tangled.org/@tangled.org/core/tree/master/docs/migrations.md"> 33 + href="https://docs.tangled.org/migrating-knots-spindles.html#migrating-knots-spindles"> 34 34 Click to read the upgrade guide</a>. 35 35 </div> 36 36 </details>
+9 -29
appview/pages/templates/brand/brand.html
··· 4 4 <div class="grid grid-cols-10"> 5 5 <header class="col-span-full md:col-span-10 px-6 py-2 mb-4"> 6 6 <h1 class="text-2xl font-bold dark:text-white mb-1">Brand</h1> 7 - <p class="text-gray-600 dark:text-gray-400 mb-1"> 7 + <p class="text-gray-500 dark:text-gray-300 mb-1"> 8 8 Assets and guidelines for using Tangled's logo and brand elements. 9 9 </p> 10 10 </header> ··· 14 14 15 15 <!-- Introduction Section --> 16 16 <section> 17 - <p class="text-gray-600 dark:text-gray-400 mb-2"> 17 + <p class="text-gray-500 dark:text-gray-300 mb-2"> 18 18 Tangled's logo and mascot is <strong>Dolly</strong>, the first ever <em>cloned</em> mammal. Please 19 19 follow the below guidelines when using Dolly and the logotype. 20 20 </p> 21 - <p class="text-gray-600 dark:text-gray-400 mb-2"> 21 + <p class="text-gray-500 dark:text-gray-300 mb-2"> 22 22 All assets are served as SVGs, and can be downloaded by right-clicking and clicking "Save image as". 23 23 </p> 24 24 </section> ··· 34 34 </div> 35 35 <div class="order-1 lg:order-2"> 36 36 <h2 class="text-xl font-semibold dark:text-white mb-3">Black logotype</h2> 37 - <p class="text-gray-600 dark:text-gray-400 mb-4">For use on light-colored backgrounds.</p> 37 + <p class="text-gray-500 dark:text-gray-300 mb-4">For use on light-colored backgrounds.</p> 38 38 <p class="text-gray-700 dark:text-gray-300"> 39 39 This is the preferred version of the logotype, featuring dark text and elements, ideal for light 40 40 backgrounds and designs. ··· 53 53 </div> 54 54 <div class="order-1 lg:order-2"> 55 55 <h2 class="text-xl font-semibold dark:text-white mb-3">White logotype</h2> 56 - <p class="text-gray-600 dark:text-gray-400 mb-4">For use on dark-colored backgrounds.</p> 56 + <p class="text-gray-500 dark:text-gray-300 mb-4">For use on dark-colored backgrounds.</p> 57 57 <p class="text-gray-700 dark:text-gray-300"> 58 58 This version features white text and elements, ideal for dark backgrounds 59 59 and inverted designs. ··· 81 81 </div> 82 82 <div class="order-1 lg:order-2"> 83 83 <h2 class="text-xl font-semibold dark:text-white mb-3">Mark only</h2> 84 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 84 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 85 85 When a smaller 1:1 logo or icon is needed, Dolly's face may be used on its own. 86 86 </p> 87 87 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 123 123 </div> 124 124 <div class="order-1 lg:order-2"> 125 125 <h2 class="text-xl font-semibold dark:text-white mb-3">Colored backgrounds</h2> 126 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 126 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 127 127 White logo mark on colored backgrounds. 128 128 </p> 129 129 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 165 165 </div> 166 166 <div class="order-1 lg:order-2"> 167 167 <h2 class="text-xl font-semibold dark:text-white mb-3">Lighter backgrounds</h2> 168 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 168 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 169 169 Dark logo mark on lighter, pastel backgrounds. 170 170 </p> 171 171 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 186 186 </div> 187 187 <div class="order-1 lg:order-2"> 188 188 <h2 class="text-xl font-semibold dark:text-white mb-3">Recoloring</h2> 189 - <p class="text-gray-600 dark:text-gray-400 mb-4"> 189 + <p class="text-gray-500 dark:text-gray-300 mb-4"> 190 190 Custom coloring of the logotype is permitted. 191 191 </p> 192 192 <p class="text-gray-700 dark:text-gray-300 mb-4"> ··· 194 194 </p> 195 195 <p class="text-gray-700 dark:text-gray-300 text-sm"> 196 196 <strong>Example:</strong> Gray/sand colored logotype on a light yellow/tan background. 197 - </p> 198 - </div> 199 - </section> 200 - 201 - <!-- Silhouette Section --> 202 - <section class="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center"> 203 - <div class="order-2 lg:order-1"> 204 - <div class="border border-gray-200 dark:border-gray-700 p-8 sm:p-16 bg-gray-50 dark:bg-gray-100 rounded"> 205 - <img src="https://assets.tangled.network/tangled_dolly_silhouette.svg" 206 - alt="Dolly silhouette" 207 - class="w-full max-w-32 mx-auto" /> 208 - </div> 209 - </div> 210 - <div class="order-1 lg:order-2"> 211 - <h2 class="text-xl font-semibold dark:text-white mb-3">Dolly silhouette</h2> 212 - <p class="text-gray-600 dark:text-gray-400 mb-4">A minimalist version of Dolly.</p> 213 - <p class="text-gray-700 dark:text-gray-300"> 214 - The silhouette can be used where a subtle brand presence is needed, 215 - or as a background element. Works on any background color with proper contrast. 216 - For example, we use this as the site's favicon. 217 197 </p> 218 198 </div> 219 199 </section>
+14 -2
appview/pages/templates/fragments/dolly/logo.html
··· 2 2 <svg 3 3 version="1.1" 4 4 id="svg1" 5 - class="{{ . }}" 5 + class="{{ .Classes }}" 6 6 width="25" 7 7 height="25" 8 8 viewBox="0 0 25 25" ··· 17 17 xmlns:svg="http://www.w3.org/2000/svg" 18 18 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 19 19 xmlns:cc="http://creativecommons.org/ns#"> 20 + <style> 21 + .dolly { 22 + color: #000000; 23 + } 24 + 25 + @media (prefers-color-scheme: dark) { 26 + .dolly { 27 + color: #ffffff; 28 + } 29 + } 30 + </style> 20 31 <sodipodi:namedview 21 32 id="namedview1" 22 33 pagecolor="#ffffff" ··· 51 62 id="g1" 52 63 transform="translate(-0.42924038,-0.87777209)"> 53 64 <path 54 - fill="currentColor" 65 + class="dolly" 66 + fill="{{ or .FillColor "currentColor" }}" 55 67 style="stroke-width:0.111183;" 56 68 d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z m 0.686342,-3.497495 c -0.643126,-0.394168 -0.33365,-1.249599 -0.359402,-1.870938 0.064,-0.749774 0.115321,-1.538054 0.452402,-2.221125 0.356724,-0.487008 1.226721,-0.299139 1.265134,0.325689 -0.02558,0.628509 -0.314101,1.25416 -0.279646,1.9057 -0.07482,0.544043 0.05418,1.155133 -0.186476,1.652391 -0.197455,0.275121 -0.599638,0.355105 -0.892012,0.208283 z m -2.808766,-0.358124 c -0.605767,-0.328664 -0.4133176,-1.155655 -0.5083256,-1.73063 0.078762,-0.66567 0.013203,-1.510085 0.5705316,-1.976886 0.545037,-0.380109 1.286917,0.270803 1.029164,0.868384 -0.274913,0.755214 -0.09475,1.580345 -0.08893,2.34609 -0.104009,0.451702 -0.587146,0.691508 -1.002445,0.493042 z" 57 69 id="path4"
-95
appview/pages/templates/fragments/dolly/silhouette.html
··· 1 - {{ define "fragments/dolly/silhouette" }} 2 - <svg 3 - version="1.1" 4 - id="svg1" 5 - width="25" 6 - height="25" 7 - viewBox="0 0 25 25" 8 - sodipodi:docname="tangled_dolly_face_only_black_on_trans.svg" 9 - inkscape:export-filename="tangled_dolly_silhouette_black_on_trans.svg" 10 - inkscape:export-xdpi="96" 11 - inkscape:export-ydpi="96" 12 - inkscape:version="1.4 (e7c3feb100, 2024-10-09)" 13 - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 14 - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 15 - xmlns="http://www.w3.org/2000/svg" 16 - xmlns:svg="http://www.w3.org/2000/svg" 17 - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 18 - xmlns:cc="http://creativecommons.org/ns#"> 19 - <style> 20 - .dolly { 21 - color: #000000; 22 - } 23 - 24 - @media (prefers-color-scheme: dark) { 25 - .dolly { 26 - color: #ffffff; 27 - } 28 - } 29 - </style> 30 - <sodipodi:namedview 31 - id="namedview1" 32 - pagecolor="#ffffff" 33 - bordercolor="#000000" 34 - borderopacity="0.25" 35 - inkscape:showpageshadow="2" 36 - inkscape:pageopacity="0.0" 37 - inkscape:pagecheckerboard="true" 38 - inkscape:deskcolor="#d5d5d5" 39 - inkscape:zoom="64" 40 - inkscape:cx="4.96875" 41 - inkscape:cy="13.429688" 42 - inkscape:window-width="3840" 43 - inkscape:window-height="2160" 44 - inkscape:window-x="0" 45 - inkscape:window-y="0" 46 - inkscape:window-maximized="0" 47 - inkscape:current-layer="g1" 48 - borderlayer="true"> 49 - <inkscape:page 50 - x="0" 51 - y="0" 52 - width="25" 53 - height="25" 54 - id="page2" 55 - margin="0" 56 - bleed="0" /> 57 - </sodipodi:namedview> 58 - <g 59 - inkscape:groupmode="layer" 60 - inkscape:label="Image" 61 - id="g1" 62 - transform="translate(-0.42924038,-0.87777209)"> 63 - <path 64 - class="dolly" 65 - fill="currentColor" 66 - style="stroke-width:0.111183" 67 - d="m 16.775491,24.987061 c -0.78517,-0.0064 -1.384202,-0.234614 -2.033994,-0.631295 -0.931792,-0.490188 -1.643475,-1.31368 -2.152014,-2.221647 C 11.781409,23.136647 10.701392,23.744942 9.4922931,24.0886 8.9774725,24.238111 8.0757679,24.389777 6.5811304,23.84827 4.4270703,23.124679 2.8580086,20.883331 3.0363279,18.599583 3.0037061,17.652919 3.3488675,16.723769 3.8381157,15.925061 2.5329485,15.224503 1.4686756,14.048584 1.0611184,12.606459 0.81344502,11.816973 0.82385989,10.966486 0.91519098,10.154906 1.2422711,8.2387903 2.6795811,6.5725716 4.5299585,5.9732484 5.2685364,4.290122 6.8802592,3.0349975 8.706276,2.7794663 c 1.2124148,-0.1688264 2.46744,0.084987 3.52811,0.7011837 1.545426,-1.7139736 4.237779,-2.2205077 6.293579,-1.1676231 1.568222,0.7488935 2.689625,2.3113526 2.961888,4.0151464 1.492195,0.5977882 2.749007,1.8168898 3.242225,3.3644951 0.329805,0.9581836 0.340709,2.0135956 0.127128,2.9974286 -0.381606,1.535184 -1.465322,2.842146 -2.868035,3.556463 0.0034,0.273204 0.901506,2.243045 0.751284,3.729647 -0.03281,1.858525 -1.211631,3.619894 -2.846433,4.475452 -0.953967,0.556812 -2.084452,0.546309 -3.120531,0.535398 z m -4.470079,-5.349839 c 1.322246,-0.147248 2.189053,-1.300106 2.862307,-2.338363 0.318287,-0.472954 0.561404,-1.002348 0.803,-1.505815 0.313265,0.287151 0.578698,0.828085 1.074141,0.956909 0.521892,0.162542 1.133743,0.03052 1.45325,-0.443554 0.611414,-1.140449 0.31004,-2.516537 -0.04602,-3.698347 C 18.232844,11.92927 17.945151,11.232927 17.397785,10.751793 17.514522,9.9283111 17.026575,9.0919791 16.332883,8.6609491 15.741721,9.1323278 14.842258,9.1294949 14.271975,8.6252369 13.178927,9.7400102 12.177239,9.7029996 11.209704,8.8195135 10.992255,8.6209543 10.577326,10.031484 9.1211947,9.2324497 8.2846288,9.9333947 7.6359672,10.607693 7.0611981,11.578553 6.5026891,12.62523 5.9177873,13.554793 5.867393,14.69141 c -0.024234,0.66432 0.4948601,1.360337 1.1982269,1.306329 0.702996,0.06277 1.1815208,-0.629091 1.7138087,-0.916491 0.079382,0.927141 0.1688108,1.923227 0.4821259,2.828358 0.3596254,1.171275 1.6262605,1.915695 2.8251855,1.745211 0.08481,-0.0066 0.218672,-0.01769 0.218672,-0.0176 z" 68 - id="path7" 69 - sodipodi:nodetypes="sccccccccccccccccccsscccccccccscccccccsc" /> 70 - </g> 71 - <metadata 72 - id="metadata1"> 73 - <rdf:RDF> 74 - <cc:Work 75 - rdf:about=""> 76 - <cc:license 77 - rdf:resource="http://creativecommons.org/licenses/by/4.0/" /> 78 - </cc:Work> 79 - <cc:License 80 - rdf:about="http://creativecommons.org/licenses/by/4.0/"> 81 - <cc:permits 82 - rdf:resource="http://creativecommons.org/ns#Reproduction" /> 83 - <cc:permits 84 - rdf:resource="http://creativecommons.org/ns#Distribution" /> 85 - <cc:requires 86 - rdf:resource="http://creativecommons.org/ns#Notice" /> 87 - <cc:requires 88 - rdf:resource="http://creativecommons.org/ns#Attribution" /> 89 - <cc:permits 90 - rdf:resource="http://creativecommons.org/ns#DerivativeWorks" /> 91 - </cc:License> 92 - </rdf:RDF> 93 - </metadata> 94 - </svg> 95 - {{ end }}
+1 -1
appview/pages/templates/fragments/logotype.html
··· 1 1 {{ define "fragments/logotype" }} 2 2 <span class="flex items-center gap-2"> 3 - {{ template "fragments/dolly/logo" "size-16 text-black dark:text-white" }} 3 + {{ template "fragments/dolly/logo" (dict "Classes" "size-16 text-black dark:text-white") }} 4 4 <span class="font-bold text-4xl not-italic">tangled</span> 5 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 6 alpha
+1 -1
appview/pages/templates/fragments/logotypeSmall.html
··· 1 1 {{ define "fragments/logotypeSmall" }} 2 2 <span class="flex items-center gap-2"> 3 - {{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }} 3 + {{ template "fragments/dolly/logo" (dict "Classes" "size-8 text-black dark:text-white")}} 4 4 <span class="font-bold text-xl not-italic">tangled</span> 5 5 <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1"> 6 6 alpha
+5
appview/pages/templates/fragments/starBtn-oob.html
··· 1 + {{ define "fragments/starBtn-oob" }} 2 + <div hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'> 3 + {{ template "fragments/starBtn" . }} 4 + </div> 5 + {{ end }}
+1 -3
appview/pages/templates/fragments/starBtn.html
··· 1 1 {{ define "fragments/starBtn" }} 2 + {{/* NOTE: this fragment is always replaced with hx-swap-oob */}} 2 3 <button 3 4 id="starBtn" 4 5 class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" ··· 10 11 {{ end }} 11 12 12 13 hx-trigger="click" 13 - hx-target="this" 14 - hx-swap="outerHTML" 15 - hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]' 16 14 hx-disabled-elt="#starBtn" 17 15 > 18 16 {{ if .IsStarred }}
+1 -1
appview/pages/templates/knots/index.html
··· 105 105 {{ define "docsButton" }} 106 106 <a 107 107 class="btn flex items-center gap-2" 108 - href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 108 + href="https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide"> 109 109 {{ i "book" "size-4" }} 110 110 docs 111 111 </a>
+4
appview/pages/templates/layouts/base.html
··· 11 11 <script defer src="/static/htmx-ext-ws.min.js"></script> 12 12 <script defer src="/static/actor-typeahead.js" type="module"></script> 13 13 14 + <link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/> 15 + <link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/> 16 + <link rel="apple-touch-icon" href="/static/logos/dolly.png"/> 17 + 14 18 <!-- preconnect to image cdn --> 15 19 <link rel="preconnect" href="https://avatar.tangled.sh" /> 16 20 <link rel="preconnect" href="https://camo.tangled.sh" />
+2 -2
appview/pages/templates/layouts/fragments/footer.html
··· 26 26 <div class="flex flex-col gap-1"> 27 27 <div class="{{ $headerStyle }}">resources</div> 28 28 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 29 - <a href="https://tangled.org/@tangled.org/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 29 + <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 30 30 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 31 31 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 32 32 </div> ··· 73 73 <div class="flex flex-col gap-1"> 74 74 <div class="{{ $headerStyle }}">resources</div> 75 75 <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a> 76 - <a href="https://tangled.org/@tangled.org/core/tree/master/docs" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 76 + <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a> 77 77 <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a> 78 78 <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a> 79 79 </div>
+1 -5
appview/pages/templates/layouts/fragments/topbar.html
··· 3 3 <div class="flex justify-between p-0 items-center"> 4 4 <div id="left-items"> 5 5 <a href="/" hx-boost="true" class="text-2xl no-underline hover:no-underline flex items-center gap-2"> 6 - {{ template "fragments/dolly/logo" "size-8 text-black dark:text-white" }} 7 - <span class="font-bold text-xl not-italic hidden md:inline">tangled</span> 8 - <span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1 hidden md:inline"> 9 - alpha 10 - </span> 6 + {{ template "fragments/logotypeSmall" }} 11 7 </a> 12 8 </div> 13 9
+1 -1
appview/pages/templates/layouts/profilebase.html
··· 2 2 3 3 {{ define "extrameta" }} 4 4 {{ $handle := resolve .Card.UserDid }} 5 - {{ $avatarUrl := profileAvatarUrl .Card.Profile "" }} 5 + {{ $avatarUrl := fullAvatar $handle }} 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/repo/empty.html
··· 26 26 {{ else if (and .LoggedInUser (eq .LoggedInUser.Did .RepoInfo.OwnerDid)) }} 27 27 {{ $knot := .RepoInfo.Knot }} 28 28 {{ if eq $knot "knot1.tangled.sh" }} 29 - {{ $knot = "tangled.sh" }} 29 + {{ $knot = "tangled.org" }} 30 30 {{ end }} 31 31 <div class="w-full flex place-content-center"> 32 32 <div class="py-6 w-fit flex flex-col gap-4">
+6 -6
appview/pages/templates/repo/fragments/backlinks.html
··· 14 14 <div class="flex gap-2 items-center"> 15 15 {{ if .State.IsClosed }} 16 16 <span class="text-gray-500 dark:text-gray-400"> 17 - {{ i "ban" "w-4 h-4" }} 17 + {{ i "ban" "size-3" }} 18 18 </span> 19 19 {{ else if eq .Kind.String "issues" }} 20 20 <span class="text-green-600 dark:text-green-500"> 21 - {{ i "circle-dot" "w-4 h-4" }} 21 + {{ i "circle-dot" "size-3" }} 22 22 </span> 23 23 {{ else if .State.IsOpen }} 24 24 <span class="text-green-600 dark:text-green-500"> 25 - {{ i "git-pull-request" "w-4 h-4" }} 25 + {{ i "git-pull-request" "size-3" }} 26 26 </span> 27 27 {{ else if .State.IsMerged }} 28 28 <span class="text-purple-600 dark:text-purple-500"> 29 - {{ i "git-merge" "w-4 h-4" }} 29 + {{ i "git-merge" "size-3" }} 30 30 </span> 31 31 {{ else }} 32 32 <span class="text-gray-600 dark:text-gray-300"> 33 - {{ i "git-pull-request-closed" "w-4 h-4" }} 33 + {{ i "git-pull-request-closed" "size-3" }} 34 34 </span> 35 35 {{ end }} 36 - <a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a> 36 + <a href="{{ . }}" class="line-clamp-1 text-sm"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a> 37 37 </div> 38 38 {{ if not (eq $.RepoInfo.FullName $repoUrl) }} 39 39 <div>
+1 -1
appview/pages/templates/repo/fragments/diff.html
··· 17 17 {{ else }} 18 18 {{ range $idx, $hunk := $diff }} 19 19 {{ with $hunk }} 20 - <details open id="file-{{ .Name.New }}" 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 }}"> 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 21 <summary class="list-none cursor-pointer sticky top-0"> 22 22 <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 23 23 <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto">
+35 -35
appview/pages/templates/repo/fragments/splitDiff.html
··· 3 3 {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} 4 4 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 5 5 {{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 6 - {{- $containerStyle := "flex min-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:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 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 " -}} 10 10 {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} 11 11 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 12 12 <div class="grid grid-cols-2 divide-x divide-gray-200 dark:divide-gray-700"> 13 - <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 13 + <div class="overflow-x-auto col-span-1 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> 14 14 {{- range .LeftLines -}} 15 15 {{- if .IsEmpty -}} 16 - <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 17 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 18 - <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 19 - <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 20 - </div> 16 + <span class="{{ $emptyStyle }} {{ $containerStyle }}"> 17 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span> 18 + <span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span> 19 + <span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span> 20 + </span> 21 21 {{- else if eq .Op.String "-" -}} 22 - <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 23 - <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 24 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 25 - <div class="px-2">{{ .Content }}</div> 26 - </div> 22 + <span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 23 + <span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span> 24 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 25 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 26 + </span> 27 27 {{- else if eq .Op.String " " -}} 28 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 29 - <div class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></div> 30 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 31 - <div class="px-2">{{ .Content }}</div> 32 - </div> 28 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{.LineNumber}}"> 29 + <span class="{{ $lineNrStyle }} {{ $lineNrSepStyle }}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{.LineNumber}}">{{ .LineNumber }}</a></span> 30 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 31 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 32 + </span> 33 33 {{- end -}} 34 34 {{- end -}} 35 - {{- end -}}</div></div></pre> 35 + {{- end -}}</div></div></div> 36 36 37 - <pre class="overflow-x-auto col-span-1"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 37 + <div class="overflow-x-auto col-span-1 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> 38 38 {{- range .RightLines -}} 39 39 {{- if .IsEmpty -}} 40 - <div class="{{ $emptyStyle }} {{ $containerStyle }}"> 41 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></div> 42 - <div class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></div> 43 - <div class="px-2 invisible" aria-hidden="true">{{ .Content }}</div> 44 - </div> 40 + <span class="{{ $emptyStyle }} {{ $containerStyle }}"> 41 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><span aria-hidden="true" class="invisible">{{.LineNumber}}</span></span> 42 + <span class="{{ $opStyle }}"><span aria-hidden="true" class="invisible">{{ .Op.String }}</span></span> 43 + <span class="px-2 invisible" aria-hidden="true">{{ .Content }}</span> 44 + </span> 45 45 {{- else if eq .Op.String "+" -}} 46 - <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 47 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 48 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 49 - <div class="px-2" >{{ .Content }}</div> 50 - </div> 46 + <span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 47 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></span> 48 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 49 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 50 + </span> 51 51 {{- else if eq .Op.String " " -}} 52 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 53 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a></div> 54 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 55 - <div class="px-2">{{ .Content }}</div> 56 - </div> 52 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-N{{.LineNumber}}"> 53 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{.LineNumber}}">{{ .LineNumber }}</a> </span> 54 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 55 + <span class="px-2 whitespace-pre">{{ .Content }}</span> 56 + </span> 57 57 {{- end -}} 58 58 {{- end -}} 59 - {{- end -}}</div></div></pre> 59 + {{- end -}}</div></div></div> 60 60 </div> 61 61 {{ end }}
+21 -22
appview/pages/templates/repo/fragments/unifiedDiff.html
··· 1 1 {{ define "repo/fragments/unifiedDiff" }} 2 2 {{ $name := .Id }} 3 - <pre class="overflow-x-auto"><div class="overflow-x-auto"><div class="min-w-full inline-block">{{- range .TextFragments -}}<div class="bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 select-none text-center">&middot;&middot;&middot;</div> 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 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 7 {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} 8 8 {{- $lineNrSepStyle1 := "" -}} 9 9 {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} 10 - {{- $containerStyle := "flex min-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:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} 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" -}} 14 14 {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} 15 15 {{- range .Lines -}} 16 16 {{- if eq .Op.String "+" -}} 17 - <div class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}"> 18 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></div> 19 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></div> 20 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 21 - <div class="px-2">{{ .Line }}</div> 22 - </div> 17 + <span class="{{ $addStyle }} {{ $containerStyle }}" id="{{$name}}-N{{$newStart}}"> 18 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><span aria-hidden="true" class="invisible">{{$newStart}}</span></span> 19 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-N{{$newStart}}">{{ $newStart }}</a></span> 20 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 21 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 22 + </span> 23 23 {{- $newStart = add64 $newStart 1 -}} 24 24 {{- end -}} 25 25 {{- if eq .Op.String "-" -}} 26 - <div class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}"> 27 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></div> 28 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></div> 29 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 30 - <div class="px-2">{{ .Line }}</div> 31 - </div> 26 + <span class="{{ $delStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}"> 27 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}">{{ $oldStart }}</a></span> 28 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><span aria-hidden="true" class="invisible">{{$oldStart}}</span></span> 29 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 30 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 31 + </span> 32 32 {{- $oldStart = add64 $oldStart 1 -}} 33 33 {{- end -}} 34 34 {{- if eq .Op.String " " -}} 35 - <div class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}"> 36 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></div> 37 - <div class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></div> 38 - <div class="{{ $opStyle }}">{{ .Op.String }}</div> 39 - <div class="px-2">{{ .Line }}</div> 40 - </div> 35 + <span class="{{ $ctxStyle }} {{ $containerStyle }}" id="{{$name}}-O{{$oldStart}}-N{{$newStart}}"> 36 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle1}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $oldStart }}</a></span> 37 + <span class="{{$lineNrStyle}} {{$lineNrSepStyle2}}"><a class="{{$linkStyle}}" href="#{{$name}}-O{{$oldStart}}-N{{$newStart}}">{{ $newStart }}</a></span> 38 + <span class="{{ $opStyle }}">{{ .Op.String }}</span> 39 + <span class="px-2 whitespace-pre">{{ .Line }}</span> 40 + </span> 41 41 {{- $newStart = add64 $newStart 1 -}} 42 42 {{- $oldStart = add64 $oldStart 1 -}} 43 43 {{- end -}} 44 44 {{- end -}} 45 - {{- end -}}</div></div></pre> 45 + {{- end -}}</div></div></div> 46 46 {{ end }} 47 -
+1 -1
appview/pages/templates/repo/pipelines/pipelines.html
··· 23 23 </p> 24 24 <p> 25 25 <span class="{{ $bullet }}">2</span>Configure your CI/CD 26 - <a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/pipeline.md" class="underline">pipeline</a>. 26 + <a href="https://docs.tangled.org/spindles.html#pipelines" class="underline">pipeline</a>. 27 27 </p> 28 28 <p><span class="{{ $bullet }}">3</span>Trigger a workflow with a push or a pull-request!</p> 29 29 </div>
+1 -1
appview/pages/templates/repo/settings/pipelines.html
··· 22 22 <p class="text-gray-500 dark:text-gray-400"> 23 23 Choose a spindle to execute your workflows on. Only repository owners 24 24 can configure spindles. Spindles can be selfhosted, 25 - <a class="text-gray-500 dark:text-gray-400 underline" href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 25 + <a class="text-gray-500 dark:text-gray-400 underline" href="https://docs.tangled.org/spindles.html#self-hosting-guide"> 26 26 click to learn more. 27 27 </a> 28 28 </p>
+1 -1
appview/pages/templates/spindles/index.html
··· 102 102 {{ define "docsButton" }} 103 103 <a 104 104 class="btn flex items-center gap-2" 105 - href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md"> 105 + href="https://docs.tangled.org/spindles.html#self-hosting-guide"> 106 106 {{ i "book" "size-4" }} 107 107 docs 108 108 </a>
+1 -1
appview/pages/templates/strings/string.html
··· 17 17 <span class="select-none">/</span> 18 18 <a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a> 19 19 </div> 20 - <div class="flex gap-2 text-base"> 20 + <div class="flex gap-2 items-stretch text-base"> 21 21 {{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }} 22 22 <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 23 23 hx-boost="true"
-44
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 avatar 10 - </label> 11 - <p class="text-sm text-gray-500 dark:text-gray-400">Select an image (PNG or JPEG, max 1MB)</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 class="flex gap-2 pt-2"> 26 - <button 27 - id="cancel-avatar-btn" 28 - type="button" 29 - popovertarget="avatar-upload-modal" 30 - popovertargetaction="hide" 31 - class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"> 32 - {{ i "x" "size-4" }} 33 - cancel 34 - </button> 35 - <button type="submit" class="btn w-1/2 flex items-center"> 36 - <span class="inline-flex gap-2 items-center">{{ i "upload" "size-4" }} upload</span> 37 - <span id="spinner" class="group"> 38 - {{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 39 - </span> 40 - </button> 41 - </div> 42 - <div id="avatar-error" class="text-red-500 dark:text-red-400"></div> 43 - </form> 44 - {{ end }}
+2 -2
appview/pages/templates/user/fragments/followCard.html
··· 6 6 <img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" alt="{{ $userIdent }}" /> 7 7 </div> 8 8 9 - <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full"> 9 + <div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0"> 10 10 <div class="flex-1 min-h-0 justify-around flex flex-col"> 11 11 <a href="/{{ $userIdent }}"> 12 12 <span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span> 13 13 </a> 14 14 {{ with .Profile }} 15 - <p class="text-sm pb-2 md:pb-2">{{.Description}}</p> 15 + <p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p> 16 16 {{ end }} 17 17 <div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 18 18 <span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
+3 -17
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="{{ 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 }} 6 + <img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" /> 16 7 </div> 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 8 </div> 24 9 <div class="col-span-2"> 25 10 <div class="flex items-center flex-row flex-nowrap gap-2"> ··· 51 36 {{ block "followerFollowing" (list $ $userIdent) }} {{ end }} 52 37 </div> 53 38 54 - <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 39 + <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 55 40 {{ if .Location }} 56 41 <div class="flex items-center gap-2"> 57 42 <span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span> ··· 126 111 </div> 127 112 {{ end }} 128 113 {{ end }} 114 +
+2 -4
appview/pages/templates/user/settings/emails.html
··· 62 62 hx-swap="none" 63 63 class="flex flex-col gap-2" 64 64 > 65 - <label for="email-address" class="uppercase p-0"> 66 - add email 67 - </label> 65 + <p class="uppercase p-0">ADD EMAIL</p> 68 66 <p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p> 69 67 <input 70 68 type="email" ··· 93 91 <div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div> 94 92 <div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div> 95 93 </form> 96 - {{ end }} 94 + {{ end }}
+2 -4
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 - <label for="key-name" class="uppercase p-0"> 67 - add ssh key 68 - </label> 66 + <p class="uppercase p-0">ADD SSH KEY</p> 69 67 <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> 70 68 <input 71 69 type="text"
+9 -6
appview/pages/templates/user/signup.html
··· 43 43 page to complete your registration. 44 44 </span> 45 45 <div class="w-full mt-4 text-center"> 46 - <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div> 46 + <div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div> 47 47 </div> 48 48 <button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" > 49 49 <span>join now</span> 50 50 </button> 51 + <p class="text-sm text-gray-500"> 52 + Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>. 53 + </p> 54 + 55 + <p id="signup-msg" class="error w-full"></p> 56 + <p class="text-sm text-gray-500 pt-4"> 57 + By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>. 58 + </p> 51 59 </form> 52 - <p class="text-sm text-gray-500"> 53 - Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>. 54 - </p> 55 - 56 - <p id="signup-msg" class="error w-full"></p> 57 60 </main> 58 61 </body> 59 62 </html>
+1 -1
appview/pulls/opengraph.go
··· 242 242 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 243 243 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 244 244 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 245 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 245 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 246 246 if err != nil { 247 247 log.Printf("dolly silhouette not available (this is ok): %v", err) 248 248 }
+56 -36
appview/pulls/pulls.go
··· 1241 1241 return 1242 1242 } 1243 1243 1244 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1245 + if err != nil { 1246 + log.Println("failed to upload patch", err) 1247 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1248 + return 1249 + } 1250 + 1244 1251 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1245 1252 Collection: tangled.RepoPullNSID, 1246 1253 Repo: user.Did, ··· 1252 1259 Repo: string(repo.RepoAt()), 1253 1260 Branch: targetBranch, 1254 1261 }, 1255 - Patch: patch, 1262 + PatchBlob: blob.Blob, 1256 1263 Source: recordPullSource, 1257 1264 CreatedAt: time.Now().Format(time.RFC3339), 1258 1265 }, ··· 1328 1335 // apply all record creations at once 1329 1336 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1330 1337 for _, p := range stack { 1338 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(p.LatestPatch())) 1339 + if err != nil { 1340 + log.Println("failed to upload patch blob", err) 1341 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1342 + return 1343 + } 1344 + 1331 1345 record := p.AsRecord() 1332 - write := comatproto.RepoApplyWrites_Input_Writes_Elem{ 1346 + record.PatchBlob = blob.Blob 1347 + writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 1333 1348 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 1334 1349 Collection: tangled.RepoPullNSID, 1335 1350 Rkey: &p.Rkey, ··· 1337 1352 Val: &record, 1338 1353 }, 1339 1354 }, 1340 - } 1341 - writes = append(writes, &write) 1355 + }) 1342 1356 } 1343 1357 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{ 1344 1358 Repo: user.Did, ··· 1366 1380 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1367 1381 return 1368 1382 } 1383 + 1369 1384 } 1370 1385 1371 1386 if err = tx.Commit(); err != nil { 1372 1387 log.Println("failed to create pull request", err) 1373 1388 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 1374 1389 return 1390 + } 1391 + 1392 + // notify about each pull 1393 + // 1394 + // this is performed after tx.Commit, because it could result in a locked DB otherwise 1395 + for _, p := range stack { 1396 + s.notifier.NewPull(r.Context(), p) 1375 1397 } 1376 1398 1377 1399 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo) ··· 1863 1885 return 1864 1886 } 1865 1887 1866 - var recordPullSource *tangled.RepoPull_Source 1867 - if pull.IsBranchBased() { 1868 - recordPullSource = &tangled.RepoPull_Source{ 1869 - Branch: pull.PullSource.Branch, 1870 - Sha: sourceRev, 1871 - } 1888 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 1889 + if err != nil { 1890 + log.Println("failed to upload patch blob", err) 1891 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 1892 + return 1872 1893 } 1873 - if pull.IsForkBased() { 1874 - repoAt := pull.PullSource.RepoAt.String() 1875 - recordPullSource = &tangled.RepoPull_Source{ 1876 - Branch: pull.PullSource.Branch, 1877 - Repo: &repoAt, 1878 - Sha: sourceRev, 1879 - } 1880 - } 1894 + record := pull.AsRecord() 1895 + record.PatchBlob = blob.Blob 1896 + record.CreatedAt = time.Now().Format(time.RFC3339) 1881 1897 1882 1898 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1883 1899 Collection: tangled.RepoPullNSID, ··· 1885 1901 Rkey: pull.Rkey, 1886 1902 SwapRecord: ex.Cid, 1887 1903 Record: &lexutil.LexiconTypeDecoder{ 1888 - Val: &tangled.RepoPull{ 1889 - Title: pull.Title, 1890 - Target: &tangled.RepoPull_Target{ 1891 - Repo: string(repo.RepoAt()), 1892 - Branch: pull.TargetBranch, 1893 - }, 1894 - Patch: patch, // new patch 1895 - Source: recordPullSource, 1896 - CreatedAt: time.Now().Format(time.RFC3339), 1897 - }, 1904 + Val: &record, 1898 1905 }, 1899 1906 }) 1900 1907 if err != nil { ··· 1980 1987 } 1981 1988 defer tx.Rollback() 1982 1989 1990 + client, err := s.oauth.AuthorizedClient(r) 1991 + if err != nil { 1992 + log.Println("failed to authorize client") 1993 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1994 + return 1995 + } 1996 + 1983 1997 // pds updates to make 1984 1998 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem 1985 1999 ··· 2013 2027 return 2014 2028 } 2015 2029 2030 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2031 + if err != nil { 2032 + log.Println("failed to upload patch blob", err) 2033 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2034 + return 2035 + } 2016 2036 record := p.AsRecord() 2037 + record.PatchBlob = blob.Blob 2017 2038 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2018 2039 RepoApplyWrites_Create: &comatproto.RepoApplyWrites_Create{ 2019 2040 Collection: tangled.RepoPullNSID, ··· 2048 2069 return 2049 2070 } 2050 2071 2072 + blob, err := comatproto.RepoUploadBlob(r.Context(), client, strings.NewReader(patch)) 2073 + if err != nil { 2074 + log.Println("failed to upload patch blob", err) 2075 + s.pages.Notice(w, "resubmit-error", "Failed to update pull request on the PDS. Try again later.") 2076 + return 2077 + } 2051 2078 record := np.AsRecord() 2052 - 2079 + record.PatchBlob = blob.Blob 2053 2080 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{ 2054 2081 RepoApplyWrites_Update: &comatproto.RepoApplyWrites_Update{ 2055 2082 Collection: tangled.RepoPullNSID, ··· 2083 2110 if err != nil { 2084 2111 log.Println("failed to resubmit pull", err) 2085 2112 s.pages.Notice(w, "pull-resubmit-error", "Failed to resubmit pull request. Try again later.") 2086 - return 2087 - } 2088 - 2089 - client, err := s.oauth.AuthorizedClient(r) 2090 - if err != nil { 2091 - log.Println("failed to authorize client") 2092 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 2093 2113 return 2094 2114 } 2095 2115
+1
appview/repo/archive.go
··· 18 18 l := rp.logger.With("handler", "DownloadArchive") 19 19 ref := chi.URLParam(r, "ref") 20 20 ref, _ = url.PathUnescape(ref) 21 + ref = strings.TrimSuffix(ref, ".tar.gz") 21 22 f, err := rp.repoResolver.Resolve(r) 22 23 if err != nil { 23 24 l.Error("failed to get repo and knot", "err", err)
+1 -1
appview/repo/opengraph.go
··· 237 237 dollyX := dollyBounds.Min.X + (dollyBounds.Dx() / 2) - (dollySize / 2) 238 238 dollyY := statsY + iconBaselineOffset - dollySize/2 + 25 239 239 dollyColor := color.RGBA{180, 180, 180, 255} // light gray 240 - err = dollyArea.DrawDollySilhouette(dollyX, dollyY, dollySize, dollyColor) 240 + err = dollyArea.DrawDolly(dollyX, dollyY, dollySize, dollyColor) 241 241 if err != nil { 242 242 log.Printf("dolly silhouette not available (this is ok): %v", err) 243 243 }
+26 -1
appview/reporesolver/resolver.go
··· 63 63 } 64 64 65 65 // get dir/ref 66 - currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath())) 66 + currentDir := extractCurrentDir(r.URL.EscapedPath()) 67 67 ref := chi.URLParam(r, "ref") 68 68 69 69 repoAt := repo.RepoAt() ··· 130 130 } 131 131 132 132 return repoInfo 133 + } 134 + 135 + // extractCurrentDir gets the current directory for markdown link resolution. 136 + // for blob paths, returns the parent dir. for tree paths, returns the path itself. 137 + // 138 + // /@user/repo/blob/main/docs/README.md => docs 139 + // /@user/repo/tree/main/docs => docs 140 + func extractCurrentDir(fullPath string) string { 141 + fullPath = strings.TrimPrefix(fullPath, "/") 142 + 143 + blobPattern := regexp.MustCompile(`blob/[^/]+/(.*)$`) 144 + if matches := blobPattern.FindStringSubmatch(fullPath); len(matches) > 1 { 145 + return path.Dir(matches[1]) 146 + } 147 + 148 + treePattern := regexp.MustCompile(`tree/[^/]+/(.*)$`) 149 + if matches := treePattern.FindStringSubmatch(fullPath); len(matches) > 1 { 150 + dir := strings.TrimSuffix(matches[1], "/") 151 + if dir == "" { 152 + return "." 153 + } 154 + return dir 155 + } 156 + 157 + return "." 133 158 } 134 159 135 160 // extractPathAfterRef gets the actual repository path
+22
appview/reporesolver/resolver_test.go
··· 1 + package reporesolver 2 + 3 + import "testing" 4 + 5 + func TestExtractCurrentDir(t *testing.T) { 6 + tests := []struct { 7 + path string 8 + want string 9 + }{ 10 + {"/@user/repo/blob/main/docs/README.md", "docs"}, 11 + {"/@user/repo/blob/main/README.md", "."}, 12 + {"/@user/repo/tree/main/docs", "docs"}, 13 + {"/@user/repo/tree/main/docs/", "docs"}, 14 + {"/@user/repo/tree/main", "."}, 15 + } 16 + 17 + for _, tt := range tests { 18 + if got := extractCurrentDir(tt.path); got != tt.want { 19 + t.Errorf("extractCurrentDir(%q) = %q, want %q", tt.path, got, tt.want) 20 + } 21 + } 22 + }
-5
appview/spindles/spindles.go
··· 653 653 s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 654 654 return 655 655 } 656 - if memberId.Handle.IsInvalidHandle() { 657 - l.Error("failed to resolve member identity to handle") 658 - s.Pages.Notice(w, noticeId, "Failed to remove member, identity resolution failed.") 659 - return 660 - } 661 656 662 657 tx, err := s.Db.Begin() 663 658 if err != nil {
+17
appview/state/git_http.go
··· 25 25 26 26 } 27 27 28 + func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) { 29 + user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 + if !ok { 31 + http.Error(w, "failed to resolve user", http.StatusInternalServerError) 32 + return 33 + } 34 + repo := r.Context().Value("repo").(*models.Repo) 35 + 36 + scheme := "https" 37 + if s.config.Core.Dev { 38 + scheme = "http" 39 + } 40 + 41 + targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery) 42 + s.proxyRequest(w, r, targetURL) 43 + } 44 + 28 45 func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) { 29 46 user, ok := r.Context().Value("resolvedId").(identity.Identity) 30 47 if !ok {
+29
appview/state/manifest.go
··· 1 + package state 2 + 3 + import ( 4 + "encoding/json" 5 + "net/http" 6 + ) 7 + 8 + // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 9 + // https://www.w3.org/TR/appmanifest/ 10 + var manifestData = map[string]any{ 11 + "name": "tangled", 12 + "description": "tightly-knit social coding.", 13 + "icons": []map[string]string{ 14 + { 15 + "src": "/static/logos/dolly.svg", 16 + "sizes": "144x144", 17 + }, 18 + }, 19 + "start_url": "/", 20 + "id": "https://tangled.org", 21 + "display": "standalone", 22 + "background_color": "#111827", 23 + "theme_color": "#111827", 24 + } 25 + 26 + func (p *State) WebAppManifest(w http.ResponseWriter, r *http.Request) { 27 + w.Header().Set("Content-Type", "application/manifest+json") 28 + json.NewEncoder(w).Encode(manifestData) 29 + }
+6 -128
appview/state/profile.go
··· 163 163 } 164 164 165 165 // populate commit counts in the timeline, using the punchcard 166 - currentMonth := time.Now().Month() 166 + now := time.Now() 167 167 for _, p := range profile.Punchcard.Punches { 168 - idx := currentMonth - p.Date.Month() 169 - if int(idx) < len(timeline.ByMonth) { 170 - timeline.ByMonth[idx].Commits += p.Count 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 171 173 } 172 174 } 173 175 ··· 737 739 AllRepos: allRepos, 738 740 }) 739 741 } 740 - 741 - func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) { 742 - l := s.logger.With("handler", "UploadProfileAvatar") 743 - user := s.oauth.GetUser(r) 744 - l = l.With("did", user.Did) 745 - 746 - // Parse multipart form (10MB max) 747 - if err := r.ParseMultipartForm(10 << 20); err != nil { 748 - l.Error("failed to parse form", "err", err) 749 - w.WriteHeader(http.StatusBadRequest) 750 - fmt.Fprintf(w, "Failed to parse form") 751 - return 752 - } 753 - 754 - file, handler, err := r.FormFile("avatar") 755 - if err != nil { 756 - l.Error("failed to read avatar file", "err", err) 757 - w.WriteHeader(http.StatusBadRequest) 758 - fmt.Fprintf(w, "Failed to read avatar file") 759 - return 760 - } 761 - defer file.Close() 762 - 763 - if handler.Size > 1000000 { 764 - l.Warn("avatar file too large", "size", handler.Size) 765 - w.WriteHeader(http.StatusBadRequest) 766 - fmt.Fprintf(w, "Avatar file too large (max 1MB)") 767 - return 768 - } 769 - 770 - contentType := handler.Header.Get("Content-Type") 771 - if contentType != "image/png" && contentType != "image/jpeg" { 772 - l.Warn("invalid image type", "contentType", contentType) 773 - w.WriteHeader(http.StatusBadRequest) 774 - fmt.Fprintf(w, "Invalid image type (only PNG and JPEG allowed)") 775 - return 776 - } 777 - 778 - client, err := s.oauth.AuthorizedClient(r) 779 - if err != nil { 780 - l.Error("failed to get PDS client", "err", err) 781 - w.WriteHeader(http.StatusInternalServerError) 782 - fmt.Fprintf(w, "Failed to connect to your PDS") 783 - return 784 - } 785 - 786 - uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file) 787 - if err != nil { 788 - l.Error("failed to upload avatar blob", "err", err) 789 - w.WriteHeader(http.StatusInternalServerError) 790 - fmt.Fprintf(w, "Failed to upload avatar to your PDS") 791 - return 792 - } 793 - 794 - l.Info("uploaded avatar blob", "cid", uploadBlobResp.Blob.Ref.String()) 795 - 796 - // get current profile record from PDS to get its CID for swap 797 - getRecordResp, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self") 798 - if err != nil { 799 - l.Error("failed to get current profile record", "err", err) 800 - w.WriteHeader(http.StatusInternalServerError) 801 - fmt.Fprintf(w, "Failed to get current profile from your PDS") 802 - return 803 - } 804 - 805 - var profileRecord *tangled.ActorProfile 806 - if getRecordResp.Value != nil { 807 - if val, ok := getRecordResp.Value.Val.(*tangled.ActorProfile); ok { 808 - profileRecord = val 809 - } else { 810 - l.Warn("profile record type assertion failed, creating new record") 811 - profileRecord = &tangled.ActorProfile{} 812 - } 813 - } else { 814 - l.Warn("no existing profile record, creating new record") 815 - profileRecord = &tangled.ActorProfile{} 816 - } 817 - 818 - profileRecord.Avatar = uploadBlobResp.Blob 819 - 820 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 821 - Collection: tangled.ActorProfileNSID, 822 - Repo: user.Did, 823 - Rkey: "self", 824 - Record: &lexutil.LexiconTypeDecoder{Val: profileRecord}, 825 - SwapRecord: getRecordResp.Cid, 826 - }) 827 - 828 - if err != nil { 829 - l.Error("failed to update profile record", "err", err) 830 - w.WriteHeader(http.StatusInternalServerError) 831 - fmt.Fprintf(w, "Failed to update profile on your PDS") 832 - return 833 - } 834 - 835 - l.Info("successfully updated profile with avatar") 836 - 837 - profile, err := db.GetProfile(s.db, user.Did) 838 - if err != nil { 839 - l.Warn("getting profile data from DB", "err", err) 840 - profile = &models.Profile{Did: user.Did} 841 - } 842 - profile.Avatar = uploadBlobResp.Blob.Ref.String() 843 - 844 - tx, err := s.db.BeginTx(r.Context(), nil) 845 - if err != nil { 846 - l.Error("failed to start transaction", "err", err) 847 - s.pages.HxRefresh(w) 848 - w.WriteHeader(http.StatusOK) 849 - return 850 - } 851 - 852 - err = db.UpsertProfile(tx, profile) 853 - if err != nil { 854 - l.Error("failed to update profile in DB", "err", err) 855 - tx.Rollback() 856 - s.pages.HxRefresh(w) 857 - w.WriteHeader(http.StatusOK) 858 - return 859 - } 860 - 861 - w.Header().Set("HX-Redirect", r.Header.Get("Referer")) 862 - w.WriteHeader(http.StatusOK) 863 - }
+4 -4
appview/state/router.go
··· 32 32 s.pages, 33 33 ) 34 34 35 - router.Get("/favicon.svg", s.Favicon) 36 - router.Get("/favicon.ico", s.Favicon) 37 - router.Get("/pwa-manifest.json", s.PWAManifest) 35 + router.Get("/pwa-manifest.json", s.WebAppManifest) 38 36 router.Get("/robots.txt", s.RobotsTxt) 39 37 40 38 userRouter := s.UserRouter(&middleware) ··· 101 99 102 100 // These routes get proxied to the knot 103 101 r.Get("/info/refs", s.InfoRefs) 102 + r.Post("/git-upload-archive", s.UploadArchive) 104 103 r.Post("/git-upload-pack", s.UploadPack) 105 104 r.Post("/git-receive-pack", s.ReceivePack) 106 105 ··· 108 107 }) 109 108 110 109 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 110 + w.WriteHeader(http.StatusNotFound) 111 111 s.pages.Error404(w) 112 112 }) 113 113 ··· 162 162 r.Get("/edit-pins", s.EditPinsFragment) 163 163 r.Post("/bio", s.UpdateProfileBio) 164 164 r.Post("/pins", s.UpdateProfilePins) 165 - r.Post("/avatar", s.UploadProfileAvatar) 166 165 }) 167 166 168 167 r.Mount("/settings", s.SettingsRouter()) ··· 182 181 r.Get("/brand", s.Brand) 183 182 184 183 r.NotFound(func(w http.ResponseWriter, r *http.Request) { 184 + w.WriteHeader(http.StatusNotFound) 185 185 s.pages.Error404(w) 186 186 }) 187 187 return r
-36
appview/state/state.go
··· 202 202 return s.db.Close() 203 203 } 204 204 205 - func (s *State) Favicon(w http.ResponseWriter, r *http.Request) { 206 - w.Header().Set("Content-Type", "image/svg+xml") 207 - w.Header().Set("Cache-Control", "public, max-age=31536000") // one year 208 - w.Header().Set("ETag", `"favicon-svg-v1"`) 209 - 210 - if match := r.Header.Get("If-None-Match"); match == `"favicon-svg-v1"` { 211 - w.WriteHeader(http.StatusNotModified) 212 - return 213 - } 214 - 215 - s.pages.Favicon(w) 216 - } 217 - 218 205 func (s *State) RobotsTxt(w http.ResponseWriter, r *http.Request) { 219 206 w.Header().Set("Content-Type", "text/plain") 220 207 w.Header().Set("Cache-Control", "public, max-age=86400") // one day ··· 223 210 Allow: / 224 211 ` 225 212 w.Write([]byte(robotsTxt)) 226 - } 227 - 228 - // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 229 - const manifestJson = `{ 230 - "name": "tangled", 231 - "description": "tightly-knit social coding.", 232 - "icons": [ 233 - { 234 - "src": "/favicon.svg", 235 - "sizes": "144x144" 236 - } 237 - ], 238 - "start_url": "/", 239 - "id": "org.tangled", 240 - 241 - "display": "standalone", 242 - "background_color": "#111827", 243 - "theme_color": "#111827" 244 - }` 245 - 246 - func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) { 247 - w.Header().Set("Content-Type", "application/json") 248 - w.Write([]byte(manifestJson)) 249 213 } 250 214 251 215 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
+3022 -3137
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 - "dependencies": { 11 - "@atproto/identity": "^0.4.1" 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/@atproto/common-web": { 20 - "version": "0.4.7", 21 - "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.7.tgz", 22 - "integrity": "sha512-vjw2+81KPo2/SAbbARGn64Ln+6JTI0FTI4xk8if0ebBfDxFRmHb2oSN1y77hzNq/ybGHqA2mecfhS03pxC5+lg==", 23 - "license": "MIT", 24 - "dependencies": { 25 - "@atproto/lex-data": "0.0.3", 26 - "@atproto/lex-json": "0.0.3", 27 - "zod": "^3.23.8" 28 - } 29 - }, 30 - "node_modules/@atproto/crypto": { 31 - "version": "0.4.5", 32 - "resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.5.tgz", 33 - "integrity": "sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw==", 34 - "license": "MIT", 35 - "dependencies": { 36 - "@noble/curves": "^1.7.0", 37 - "@noble/hashes": "^1.6.1", 38 - "uint8arrays": "3.0.0" 39 - }, 40 - "engines": { 41 - "node": ">=18.7.0" 42 - } 43 - }, 44 - "node_modules/@atproto/identity": { 45 - "version": "0.4.10", 46 - "resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.10.tgz", 47 - "integrity": "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ==", 48 - "license": "MIT", 49 - "dependencies": { 50 - "@atproto/common-web": "^0.4.4", 51 - "@atproto/crypto": "^0.4.4" 52 - }, 53 - "engines": { 54 - "node": ">=18.7.0" 55 - } 56 - }, 57 - "node_modules/@atproto/lex-data": { 58 - "version": "0.0.3", 59 - "resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.3.tgz", 60 - "integrity": "sha512-ivo1IpY/EX+RIpxPgCf4cPhQo5bfu4nrpa1vJCt8hCm9SfoonJkDFGa0n4SMw4JnXZoUcGcrJ46L+D8bH6GI2g==", 61 - "license": "MIT", 62 - "dependencies": { 63 - "@atproto/syntax": "0.4.2", 64 - "multiformats": "^9.9.0", 65 - "tslib": "^2.8.1", 66 - "uint8arrays": "3.0.0", 67 - "unicode-segmenter": "^0.14.0" 68 - } 69 - }, 70 - "node_modules/@atproto/lex-json": { 71 - "version": "0.0.3", 72 - "resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.3.tgz", 73 - "integrity": "sha512-ZVcY7XlRfdPYvQQ2WroKUepee0+NCovrSXgXURM3Xv+n5jflJCoczguROeRr8sN0xvT0ZbzMrDNHCUYKNnxcjw==", 74 - "license": "MIT", 75 - "dependencies": { 76 - "@atproto/lex-data": "0.0.3", 77 - "tslib": "^2.8.1" 78 - } 79 - }, 80 - "node_modules/@atproto/syntax": { 81 - "version": "0.4.2", 82 - "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz", 83 - "integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==", 84 - "license": "MIT" 85 - }, 86 - "node_modules/@cloudflare/kv-asset-handler": { 87 - "version": "0.4.0", 88 - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", 89 - "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", 90 - "dev": true, 91 - "license": "MIT OR Apache-2.0", 92 - "dependencies": { 93 - "mime": "^3.0.0" 94 - }, 95 - "engines": { 96 - "node": ">=18.0.0" 97 - } 98 - }, 99 - "node_modules/@cloudflare/unenv-preset": { 100 - "version": "2.3.1", 101 - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz", 102 - "integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==", 103 - "dev": true, 104 - "license": "MIT OR Apache-2.0", 105 - "peerDependencies": { 106 - "unenv": "2.0.0-rc.15", 107 - "workerd": "^1.20250320.0" 108 - }, 109 - "peerDependenciesMeta": { 110 - "workerd": { 111 - "optional": true 112 - } 113 - } 114 - }, 115 - "node_modules/@cloudflare/vitest-pool-workers": { 116 - "version": "0.8.24", 117 - "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.24.tgz", 118 - "integrity": "sha512-wT2PABJQ9YLYWrVu4CRZOjvmjHkdbMyLTZPU9n/7JEMM3pgG8dY41F1Rj31UsXRQaXX39A/CTPGlk58dcMUysA==", 119 - "dev": true, 120 - "license": "MIT", 121 - "dependencies": { 122 - "birpc": "0.2.14", 123 - "cjs-module-lexer": "^1.2.3", 124 - "devalue": "^4.3.0", 125 - "miniflare": "4.20250428.1", 126 - "semver": "^7.7.1", 127 - "wrangler": "4.14.1", 128 - "zod": "^3.22.3" 129 - }, 130 - "peerDependencies": { 131 - "@vitest/runner": "2.0.x - 3.1.x", 132 - "@vitest/snapshot": "2.0.x - 3.1.x", 133 - "vitest": "2.0.x - 3.1.x" 134 - } 135 - }, 136 - "node_modules/@cloudflare/workerd-darwin-64": { 137 - "version": "1.20250428.0", 138 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250428.0.tgz", 139 - "integrity": "sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==", 140 - "cpu": [ 141 - "x64" 142 - ], 143 - "dev": true, 144 - "license": "Apache-2.0", 145 - "optional": true, 146 - "os": [ 147 - "darwin" 148 - ], 149 - "engines": { 150 - "node": ">=16" 151 - } 152 - }, 153 - "node_modules/@cloudflare/workerd-darwin-arm64": { 154 - "version": "1.20250428.0", 155 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250428.0.tgz", 156 - "integrity": "sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==", 157 - "cpu": [ 158 - "arm64" 159 - ], 160 - "dev": true, 161 - "license": "Apache-2.0", 162 - "optional": true, 163 - "os": [ 164 - "darwin" 165 - ], 166 - "engines": { 167 - "node": ">=16" 168 - } 169 - }, 170 - "node_modules/@cloudflare/workerd-linux-64": { 171 - "version": "1.20250428.0", 172 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250428.0.tgz", 173 - "integrity": "sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==", 174 - "cpu": [ 175 - "x64" 176 - ], 177 - "dev": true, 178 - "license": "Apache-2.0", 179 - "optional": true, 180 - "os": [ 181 - "linux" 182 - ], 183 - "engines": { 184 - "node": ">=16" 185 - } 186 - }, 187 - "node_modules/@cloudflare/workerd-linux-arm64": { 188 - "version": "1.20250428.0", 189 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250428.0.tgz", 190 - "integrity": "sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==", 191 - "cpu": [ 192 - "arm64" 193 - ], 194 - "dev": true, 195 - "license": "Apache-2.0", 196 - "optional": true, 197 - "os": [ 198 - "linux" 199 - ], 200 - "engines": { 201 - "node": ">=16" 202 - } 203 - }, 204 - "node_modules/@cloudflare/workerd-windows-64": { 205 - "version": "1.20250428.0", 206 - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250428.0.tgz", 207 - "integrity": "sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==", 208 - "cpu": [ 209 - "x64" 210 - ], 211 - "dev": true, 212 - "license": "Apache-2.0", 213 - "optional": true, 214 - "os": [ 215 - "win32" 216 - ], 217 - "engines": { 218 - "node": ">=16" 219 - } 220 - }, 221 - "node_modules/@cspotcode/source-map-support": { 222 - "version": "0.8.1", 223 - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 224 - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 225 - "dev": true, 226 - "license": "MIT", 227 - "dependencies": { 228 - "@jridgewell/trace-mapping": "0.3.9" 229 - }, 230 - "engines": { 231 - "node": ">=12" 232 - } 233 - }, 234 - "node_modules/@emnapi/runtime": { 235 - "version": "1.4.3", 236 - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", 237 - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", 238 - "dev": true, 239 - "license": "MIT", 240 - "optional": true, 241 - "dependencies": { 242 - "tslib": "^2.4.0" 243 - } 244 - }, 245 - "node_modules/@esbuild/aix-ppc64": { 246 - "version": "0.25.3", 247 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", 248 - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", 249 - "cpu": [ 250 - "ppc64" 251 - ], 252 - "dev": true, 253 - "license": "MIT", 254 - "optional": true, 255 - "os": [ 256 - "aix" 257 - ], 258 - "engines": { 259 - "node": ">=18" 260 - } 261 - }, 262 - "node_modules/@esbuild/android-arm": { 263 - "version": "0.25.3", 264 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", 265 - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", 266 - "cpu": [ 267 - "arm" 268 - ], 269 - "dev": true, 270 - "license": "MIT", 271 - "optional": true, 272 - "os": [ 273 - "android" 274 - ], 275 - "engines": { 276 - "node": ">=18" 277 - } 278 - }, 279 - "node_modules/@esbuild/android-arm64": { 280 - "version": "0.25.3", 281 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", 282 - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", 283 - "cpu": [ 284 - "arm64" 285 - ], 286 - "dev": true, 287 - "license": "MIT", 288 - "optional": true, 289 - "os": [ 290 - "android" 291 - ], 292 - "engines": { 293 - "node": ">=18" 294 - } 295 - }, 296 - "node_modules/@esbuild/android-x64": { 297 - "version": "0.25.3", 298 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", 299 - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", 300 - "cpu": [ 301 - "x64" 302 - ], 303 - "dev": true, 304 - "license": "MIT", 305 - "optional": true, 306 - "os": [ 307 - "android" 308 - ], 309 - "engines": { 310 - "node": ">=18" 311 - } 312 - }, 313 - "node_modules/@esbuild/darwin-arm64": { 314 - "version": "0.25.3", 315 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", 316 - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", 317 - "cpu": [ 318 - "arm64" 319 - ], 320 - "dev": true, 321 - "license": "MIT", 322 - "optional": true, 323 - "os": [ 324 - "darwin" 325 - ], 326 - "engines": { 327 - "node": ">=18" 328 - } 329 - }, 330 - "node_modules/@esbuild/darwin-x64": { 331 - "version": "0.25.3", 332 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", 333 - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", 334 - "cpu": [ 335 - "x64" 336 - ], 337 - "dev": true, 338 - "license": "MIT", 339 - "optional": true, 340 - "os": [ 341 - "darwin" 342 - ], 343 - "engines": { 344 - "node": ">=18" 345 - } 346 - }, 347 - "node_modules/@esbuild/freebsd-arm64": { 348 - "version": "0.25.3", 349 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", 350 - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", 351 - "cpu": [ 352 - "arm64" 353 - ], 354 - "dev": true, 355 - "license": "MIT", 356 - "optional": true, 357 - "os": [ 358 - "freebsd" 359 - ], 360 - "engines": { 361 - "node": ">=18" 362 - } 363 - }, 364 - "node_modules/@esbuild/freebsd-x64": { 365 - "version": "0.25.3", 366 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", 367 - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", 368 - "cpu": [ 369 - "x64" 370 - ], 371 - "dev": true, 372 - "license": "MIT", 373 - "optional": true, 374 - "os": [ 375 - "freebsd" 376 - ], 377 - "engines": { 378 - "node": ">=18" 379 - } 380 - }, 381 - "node_modules/@esbuild/linux-arm": { 382 - "version": "0.25.3", 383 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", 384 - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", 385 - "cpu": [ 386 - "arm" 387 - ], 388 - "dev": true, 389 - "license": "MIT", 390 - "optional": true, 391 - "os": [ 392 - "linux" 393 - ], 394 - "engines": { 395 - "node": ">=18" 396 - } 397 - }, 398 - "node_modules/@esbuild/linux-arm64": { 399 - "version": "0.25.3", 400 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", 401 - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", 402 - "cpu": [ 403 - "arm64" 404 - ], 405 - "dev": true, 406 - "license": "MIT", 407 - "optional": true, 408 - "os": [ 409 - "linux" 410 - ], 411 - "engines": { 412 - "node": ">=18" 413 - } 414 - }, 415 - "node_modules/@esbuild/linux-ia32": { 416 - "version": "0.25.3", 417 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", 418 - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", 419 - "cpu": [ 420 - "ia32" 421 - ], 422 - "dev": true, 423 - "license": "MIT", 424 - "optional": true, 425 - "os": [ 426 - "linux" 427 - ], 428 - "engines": { 429 - "node": ">=18" 430 - } 431 - }, 432 - "node_modules/@esbuild/linux-loong64": { 433 - "version": "0.25.3", 434 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", 435 - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", 436 - "cpu": [ 437 - "loong64" 438 - ], 439 - "dev": true, 440 - "license": "MIT", 441 - "optional": true, 442 - "os": [ 443 - "linux" 444 - ], 445 - "engines": { 446 - "node": ">=18" 447 - } 448 - }, 449 - "node_modules/@esbuild/linux-mips64el": { 450 - "version": "0.25.3", 451 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", 452 - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", 453 - "cpu": [ 454 - "mips64el" 455 - ], 456 - "dev": true, 457 - "license": "MIT", 458 - "optional": true, 459 - "os": [ 460 - "linux" 461 - ], 462 - "engines": { 463 - "node": ">=18" 464 - } 465 - }, 466 - "node_modules/@esbuild/linux-ppc64": { 467 - "version": "0.25.3", 468 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", 469 - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", 470 - "cpu": [ 471 - "ppc64" 472 - ], 473 - "dev": true, 474 - "license": "MIT", 475 - "optional": true, 476 - "os": [ 477 - "linux" 478 - ], 479 - "engines": { 480 - "node": ">=18" 481 - } 482 - }, 483 - "node_modules/@esbuild/linux-riscv64": { 484 - "version": "0.25.3", 485 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", 486 - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", 487 - "cpu": [ 488 - "riscv64" 489 - ], 490 - "dev": true, 491 - "license": "MIT", 492 - "optional": true, 493 - "os": [ 494 - "linux" 495 - ], 496 - "engines": { 497 - "node": ">=18" 498 - } 499 - }, 500 - "node_modules/@esbuild/linux-s390x": { 501 - "version": "0.25.3", 502 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", 503 - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", 504 - "cpu": [ 505 - "s390x" 506 - ], 507 - "dev": true, 508 - "license": "MIT", 509 - "optional": true, 510 - "os": [ 511 - "linux" 512 - ], 513 - "engines": { 514 - "node": ">=18" 515 - } 516 - }, 517 - "node_modules/@esbuild/linux-x64": { 518 - "version": "0.25.3", 519 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", 520 - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", 521 - "cpu": [ 522 - "x64" 523 - ], 524 - "dev": true, 525 - "license": "MIT", 526 - "optional": true, 527 - "os": [ 528 - "linux" 529 - ], 530 - "engines": { 531 - "node": ">=18" 532 - } 533 - }, 534 - "node_modules/@esbuild/netbsd-arm64": { 535 - "version": "0.25.3", 536 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", 537 - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", 538 - "cpu": [ 539 - "arm64" 540 - ], 541 - "dev": true, 542 - "license": "MIT", 543 - "optional": true, 544 - "os": [ 545 - "netbsd" 546 - ], 547 - "engines": { 548 - "node": ">=18" 549 - } 550 - }, 551 - "node_modules/@esbuild/netbsd-x64": { 552 - "version": "0.25.3", 553 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", 554 - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", 555 - "cpu": [ 556 - "x64" 557 - ], 558 - "dev": true, 559 - "license": "MIT", 560 - "optional": true, 561 - "os": [ 562 - "netbsd" 563 - ], 564 - "engines": { 565 - "node": ">=18" 566 - } 567 - }, 568 - "node_modules/@esbuild/openbsd-arm64": { 569 - "version": "0.25.3", 570 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", 571 - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", 572 - "cpu": [ 573 - "arm64" 574 - ], 575 - "dev": true, 576 - "license": "MIT", 577 - "optional": true, 578 - "os": [ 579 - "openbsd" 580 - ], 581 - "engines": { 582 - "node": ">=18" 583 - } 584 - }, 585 - "node_modules/@esbuild/openbsd-x64": { 586 - "version": "0.25.3", 587 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", 588 - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", 589 - "cpu": [ 590 - "x64" 591 - ], 592 - "dev": true, 593 - "license": "MIT", 594 - "optional": true, 595 - "os": [ 596 - "openbsd" 597 - ], 598 - "engines": { 599 - "node": ">=18" 600 - } 601 - }, 602 - "node_modules/@esbuild/sunos-x64": { 603 - "version": "0.25.3", 604 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", 605 - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", 606 - "cpu": [ 607 - "x64" 608 - ], 609 - "dev": true, 610 - "license": "MIT", 611 - "optional": true, 612 - "os": [ 613 - "sunos" 614 - ], 615 - "engines": { 616 - "node": ">=18" 617 - } 618 - }, 619 - "node_modules/@esbuild/win32-arm64": { 620 - "version": "0.25.3", 621 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", 622 - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", 623 - "cpu": [ 624 - "arm64" 625 - ], 626 - "dev": true, 627 - "license": "MIT", 628 - "optional": true, 629 - "os": [ 630 - "win32" 631 - ], 632 - "engines": { 633 - "node": ">=18" 634 - } 635 - }, 636 - "node_modules/@esbuild/win32-ia32": { 637 - "version": "0.25.3", 638 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", 639 - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", 640 - "cpu": [ 641 - "ia32" 642 - ], 643 - "dev": true, 644 - "license": "MIT", 645 - "optional": true, 646 - "os": [ 647 - "win32" 648 - ], 649 - "engines": { 650 - "node": ">=18" 651 - } 652 - }, 653 - "node_modules/@esbuild/win32-x64": { 654 - "version": "0.25.3", 655 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", 656 - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", 657 - "cpu": [ 658 - "x64" 659 - ], 660 - "dev": true, 661 - "license": "MIT", 662 - "optional": true, 663 - "os": [ 664 - "win32" 665 - ], 666 - "engines": { 667 - "node": ">=18" 668 - } 669 - }, 670 - "node_modules/@fastify/busboy": { 671 - "version": "2.1.1", 672 - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 673 - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 674 - "dev": true, 675 - "license": "MIT", 676 - "engines": { 677 - "node": ">=14" 678 - } 679 - }, 680 - "node_modules/@img/sharp-darwin-arm64": { 681 - "version": "0.33.5", 682 - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", 683 - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", 684 - "cpu": [ 685 - "arm64" 686 - ], 687 - "dev": true, 688 - "license": "Apache-2.0", 689 - "optional": true, 690 - "os": [ 691 - "darwin" 692 - ], 693 - "engines": { 694 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 695 - }, 696 - "funding": { 697 - "url": "https://opencollective.com/libvips" 698 - }, 699 - "optionalDependencies": { 700 - "@img/sharp-libvips-darwin-arm64": "1.0.4" 701 - } 702 - }, 703 - "node_modules/@img/sharp-darwin-x64": { 704 - "version": "0.33.5", 705 - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", 706 - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", 707 - "cpu": [ 708 - "x64" 709 - ], 710 - "dev": true, 711 - "license": "Apache-2.0", 712 - "optional": true, 713 - "os": [ 714 - "darwin" 715 - ], 716 - "engines": { 717 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 718 - }, 719 - "funding": { 720 - "url": "https://opencollective.com/libvips" 721 - }, 722 - "optionalDependencies": { 723 - "@img/sharp-libvips-darwin-x64": "1.0.4" 724 - } 725 - }, 726 - "node_modules/@img/sharp-libvips-darwin-arm64": { 727 - "version": "1.0.4", 728 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", 729 - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", 730 - "cpu": [ 731 - "arm64" 732 - ], 733 - "dev": true, 734 - "license": "LGPL-3.0-or-later", 735 - "optional": true, 736 - "os": [ 737 - "darwin" 738 - ], 739 - "funding": { 740 - "url": "https://opencollective.com/libvips" 741 - } 742 - }, 743 - "node_modules/@img/sharp-libvips-darwin-x64": { 744 - "version": "1.0.4", 745 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", 746 - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", 747 - "cpu": [ 748 - "x64" 749 - ], 750 - "dev": true, 751 - "license": "LGPL-3.0-or-later", 752 - "optional": true, 753 - "os": [ 754 - "darwin" 755 - ], 756 - "funding": { 757 - "url": "https://opencollective.com/libvips" 758 - } 759 - }, 760 - "node_modules/@img/sharp-libvips-linux-arm": { 761 - "version": "1.0.5", 762 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", 763 - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", 764 - "cpu": [ 765 - "arm" 766 - ], 767 - "dev": true, 768 - "license": "LGPL-3.0-or-later", 769 - "optional": true, 770 - "os": [ 771 - "linux" 772 - ], 773 - "funding": { 774 - "url": "https://opencollective.com/libvips" 775 - } 776 - }, 777 - "node_modules/@img/sharp-libvips-linux-arm64": { 778 - "version": "1.0.4", 779 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", 780 - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", 781 - "cpu": [ 782 - "arm64" 783 - ], 784 - "dev": true, 785 - "license": "LGPL-3.0-or-later", 786 - "optional": true, 787 - "os": [ 788 - "linux" 789 - ], 790 - "funding": { 791 - "url": "https://opencollective.com/libvips" 792 - } 793 - }, 794 - "node_modules/@img/sharp-libvips-linux-s390x": { 795 - "version": "1.0.4", 796 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", 797 - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", 798 - "cpu": [ 799 - "s390x" 800 - ], 801 - "dev": true, 802 - "license": "LGPL-3.0-or-later", 803 - "optional": true, 804 - "os": [ 805 - "linux" 806 - ], 807 - "funding": { 808 - "url": "https://opencollective.com/libvips" 809 - } 810 - }, 811 - "node_modules/@img/sharp-libvips-linux-x64": { 812 - "version": "1.0.4", 813 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", 814 - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", 815 - "cpu": [ 816 - "x64" 817 - ], 818 - "dev": true, 819 - "license": "LGPL-3.0-or-later", 820 - "optional": true, 821 - "os": [ 822 - "linux" 823 - ], 824 - "funding": { 825 - "url": "https://opencollective.com/libvips" 826 - } 827 - }, 828 - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 829 - "version": "1.0.4", 830 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", 831 - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", 832 - "cpu": [ 833 - "arm64" 834 - ], 835 - "dev": true, 836 - "license": "LGPL-3.0-or-later", 837 - "optional": true, 838 - "os": [ 839 - "linux" 840 - ], 841 - "funding": { 842 - "url": "https://opencollective.com/libvips" 843 - } 844 - }, 845 - "node_modules/@img/sharp-libvips-linuxmusl-x64": { 846 - "version": "1.0.4", 847 - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", 848 - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", 849 - "cpu": [ 850 - "x64" 851 - ], 852 - "dev": true, 853 - "license": "LGPL-3.0-or-later", 854 - "optional": true, 855 - "os": [ 856 - "linux" 857 - ], 858 - "funding": { 859 - "url": "https://opencollective.com/libvips" 860 - } 861 - }, 862 - "node_modules/@img/sharp-linux-arm": { 863 - "version": "0.33.5", 864 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", 865 - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", 866 - "cpu": [ 867 - "arm" 868 - ], 869 - "dev": true, 870 - "license": "Apache-2.0", 871 - "optional": true, 872 - "os": [ 873 - "linux" 874 - ], 875 - "engines": { 876 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 877 - }, 878 - "funding": { 879 - "url": "https://opencollective.com/libvips" 880 - }, 881 - "optionalDependencies": { 882 - "@img/sharp-libvips-linux-arm": "1.0.5" 883 - } 884 - }, 885 - "node_modules/@img/sharp-linux-arm64": { 886 - "version": "0.33.5", 887 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", 888 - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", 889 - "cpu": [ 890 - "arm64" 891 - ], 892 - "dev": true, 893 - "license": "Apache-2.0", 894 - "optional": true, 895 - "os": [ 896 - "linux" 897 - ], 898 - "engines": { 899 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 900 - }, 901 - "funding": { 902 - "url": "https://opencollective.com/libvips" 903 - }, 904 - "optionalDependencies": { 905 - "@img/sharp-libvips-linux-arm64": "1.0.4" 906 - } 907 - }, 908 - "node_modules/@img/sharp-linux-s390x": { 909 - "version": "0.33.5", 910 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", 911 - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", 912 - "cpu": [ 913 - "s390x" 914 - ], 915 - "dev": true, 916 - "license": "Apache-2.0", 917 - "optional": true, 918 - "os": [ 919 - "linux" 920 - ], 921 - "engines": { 922 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 923 - }, 924 - "funding": { 925 - "url": "https://opencollective.com/libvips" 926 - }, 927 - "optionalDependencies": { 928 - "@img/sharp-libvips-linux-s390x": "1.0.4" 929 - } 930 - }, 931 - "node_modules/@img/sharp-linux-x64": { 932 - "version": "0.33.5", 933 - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", 934 - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", 935 - "cpu": [ 936 - "x64" 937 - ], 938 - "dev": true, 939 - "license": "Apache-2.0", 940 - "optional": true, 941 - "os": [ 942 - "linux" 943 - ], 944 - "engines": { 945 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 946 - }, 947 - "funding": { 948 - "url": "https://opencollective.com/libvips" 949 - }, 950 - "optionalDependencies": { 951 - "@img/sharp-libvips-linux-x64": "1.0.4" 952 - } 953 - }, 954 - "node_modules/@img/sharp-linuxmusl-arm64": { 955 - "version": "0.33.5", 956 - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", 957 - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", 958 - "cpu": [ 959 - "arm64" 960 - ], 961 - "dev": true, 962 - "license": "Apache-2.0", 963 - "optional": true, 964 - "os": [ 965 - "linux" 966 - ], 967 - "engines": { 968 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 969 - }, 970 - "funding": { 971 - "url": "https://opencollective.com/libvips" 972 - }, 973 - "optionalDependencies": { 974 - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" 975 - } 976 - }, 977 - "node_modules/@img/sharp-linuxmusl-x64": { 978 - "version": "0.33.5", 979 - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", 980 - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", 981 - "cpu": [ 982 - "x64" 983 - ], 984 - "dev": true, 985 - "license": "Apache-2.0", 986 - "optional": true, 987 - "os": [ 988 - "linux" 989 - ], 990 - "engines": { 991 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 992 - }, 993 - "funding": { 994 - "url": "https://opencollective.com/libvips" 995 - }, 996 - "optionalDependencies": { 997 - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" 998 - } 999 - }, 1000 - "node_modules/@img/sharp-wasm32": { 1001 - "version": "0.33.5", 1002 - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", 1003 - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", 1004 - "cpu": [ 1005 - "wasm32" 1006 - ], 1007 - "dev": true, 1008 - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 1009 - "optional": true, 1010 - "dependencies": { 1011 - "@emnapi/runtime": "^1.2.0" 1012 - }, 1013 - "engines": { 1014 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1015 - }, 1016 - "funding": { 1017 - "url": "https://opencollective.com/libvips" 1018 - } 1019 - }, 1020 - "node_modules/@img/sharp-win32-ia32": { 1021 - "version": "0.33.5", 1022 - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", 1023 - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", 1024 - "cpu": [ 1025 - "ia32" 1026 - ], 1027 - "dev": true, 1028 - "license": "Apache-2.0 AND LGPL-3.0-or-later", 1029 - "optional": true, 1030 - "os": [ 1031 - "win32" 1032 - ], 1033 - "engines": { 1034 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1035 - }, 1036 - "funding": { 1037 - "url": "https://opencollective.com/libvips" 1038 - } 1039 - }, 1040 - "node_modules/@img/sharp-win32-x64": { 1041 - "version": "0.33.5", 1042 - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", 1043 - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", 1044 - "cpu": [ 1045 - "x64" 1046 - ], 1047 - "dev": true, 1048 - "license": "Apache-2.0 AND LGPL-3.0-or-later", 1049 - "optional": true, 1050 - "os": [ 1051 - "win32" 1052 - ], 1053 - "engines": { 1054 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 1055 - }, 1056 - "funding": { 1057 - "url": "https://opencollective.com/libvips" 1058 - } 1059 - }, 1060 - "node_modules/@jridgewell/resolve-uri": { 1061 - "version": "3.1.2", 1062 - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 1063 - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 1064 - "dev": true, 1065 - "license": "MIT", 1066 - "engines": { 1067 - "node": ">=6.0.0" 1068 - } 1069 - }, 1070 - "node_modules/@jridgewell/sourcemap-codec": { 1071 - "version": "1.5.0", 1072 - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 1073 - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 1074 - "dev": true, 1075 - "license": "MIT" 1076 - }, 1077 - "node_modules/@jridgewell/trace-mapping": { 1078 - "version": "0.3.9", 1079 - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 1080 - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 1081 - "dev": true, 1082 - "license": "MIT", 1083 - "dependencies": { 1084 - "@jridgewell/resolve-uri": "^3.0.3", 1085 - "@jridgewell/sourcemap-codec": "^1.4.10" 1086 - } 1087 - }, 1088 - "node_modules/@noble/curves": { 1089 - "version": "1.9.7", 1090 - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", 1091 - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", 1092 - "license": "MIT", 1093 - "dependencies": { 1094 - "@noble/hashes": "1.8.0" 1095 - }, 1096 - "engines": { 1097 - "node": "^14.21.3 || >=16" 1098 - }, 1099 - "funding": { 1100 - "url": "https://paulmillr.com/funding/" 1101 - } 1102 - }, 1103 - "node_modules/@noble/hashes": { 1104 - "version": "1.8.0", 1105 - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", 1106 - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", 1107 - "license": "MIT", 1108 - "engines": { 1109 - "node": "^14.21.3 || >=16" 1110 - }, 1111 - "funding": { 1112 - "url": "https://paulmillr.com/funding/" 1113 - } 1114 - }, 1115 - "node_modules/@rollup/rollup-android-arm-eabi": { 1116 - "version": "4.40.1", 1117 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", 1118 - "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", 1119 - "cpu": [ 1120 - "arm" 1121 - ], 1122 - "dev": true, 1123 - "license": "MIT", 1124 - "optional": true, 1125 - "os": [ 1126 - "android" 1127 - ] 1128 - }, 1129 - "node_modules/@rollup/rollup-android-arm64": { 1130 - "version": "4.40.1", 1131 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", 1132 - "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", 1133 - "cpu": [ 1134 - "arm64" 1135 - ], 1136 - "dev": true, 1137 - "license": "MIT", 1138 - "optional": true, 1139 - "os": [ 1140 - "android" 1141 - ] 1142 - }, 1143 - "node_modules/@rollup/rollup-darwin-arm64": { 1144 - "version": "4.40.1", 1145 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", 1146 - "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", 1147 - "cpu": [ 1148 - "arm64" 1149 - ], 1150 - "dev": true, 1151 - "license": "MIT", 1152 - "optional": true, 1153 - "os": [ 1154 - "darwin" 1155 - ] 1156 - }, 1157 - "node_modules/@rollup/rollup-darwin-x64": { 1158 - "version": "4.40.1", 1159 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", 1160 - "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", 1161 - "cpu": [ 1162 - "x64" 1163 - ], 1164 - "dev": true, 1165 - "license": "MIT", 1166 - "optional": true, 1167 - "os": [ 1168 - "darwin" 1169 - ] 1170 - }, 1171 - "node_modules/@rollup/rollup-freebsd-arm64": { 1172 - "version": "4.40.1", 1173 - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", 1174 - "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", 1175 - "cpu": [ 1176 - "arm64" 1177 - ], 1178 - "dev": true, 1179 - "license": "MIT", 1180 - "optional": true, 1181 - "os": [ 1182 - "freebsd" 1183 - ] 1184 - }, 1185 - "node_modules/@rollup/rollup-freebsd-x64": { 1186 - "version": "4.40.1", 1187 - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", 1188 - "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", 1189 - "cpu": [ 1190 - "x64" 1191 - ], 1192 - "dev": true, 1193 - "license": "MIT", 1194 - "optional": true, 1195 - "os": [ 1196 - "freebsd" 1197 - ] 1198 - }, 1199 - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1200 - "version": "4.40.1", 1201 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", 1202 - "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", 1203 - "cpu": [ 1204 - "arm" 1205 - ], 1206 - "dev": true, 1207 - "license": "MIT", 1208 - "optional": true, 1209 - "os": [ 1210 - "linux" 1211 - ] 1212 - }, 1213 - "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1214 - "version": "4.40.1", 1215 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", 1216 - "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", 1217 - "cpu": [ 1218 - "arm" 1219 - ], 1220 - "dev": true, 1221 - "license": "MIT", 1222 - "optional": true, 1223 - "os": [ 1224 - "linux" 1225 - ] 1226 - }, 1227 - "node_modules/@rollup/rollup-linux-arm64-gnu": { 1228 - "version": "4.40.1", 1229 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", 1230 - "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", 1231 - "cpu": [ 1232 - "arm64" 1233 - ], 1234 - "dev": true, 1235 - "license": "MIT", 1236 - "optional": true, 1237 - "os": [ 1238 - "linux" 1239 - ] 1240 - }, 1241 - "node_modules/@rollup/rollup-linux-arm64-musl": { 1242 - "version": "4.40.1", 1243 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", 1244 - "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", 1245 - "cpu": [ 1246 - "arm64" 1247 - ], 1248 - "dev": true, 1249 - "license": "MIT", 1250 - "optional": true, 1251 - "os": [ 1252 - "linux" 1253 - ] 1254 - }, 1255 - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 1256 - "version": "4.40.1", 1257 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", 1258 - "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", 1259 - "cpu": [ 1260 - "loong64" 1261 - ], 1262 - "dev": true, 1263 - "license": "MIT", 1264 - "optional": true, 1265 - "os": [ 1266 - "linux" 1267 - ] 1268 - }, 1269 - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 1270 - "version": "4.40.1", 1271 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", 1272 - "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", 1273 - "cpu": [ 1274 - "ppc64" 1275 - ], 1276 - "dev": true, 1277 - "license": "MIT", 1278 - "optional": true, 1279 - "os": [ 1280 - "linux" 1281 - ] 1282 - }, 1283 - "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1284 - "version": "4.40.1", 1285 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", 1286 - "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", 1287 - "cpu": [ 1288 - "riscv64" 1289 - ], 1290 - "dev": true, 1291 - "license": "MIT", 1292 - "optional": true, 1293 - "os": [ 1294 - "linux" 1295 - ] 1296 - }, 1297 - "node_modules/@rollup/rollup-linux-riscv64-musl": { 1298 - "version": "4.40.1", 1299 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", 1300 - "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", 1301 - "cpu": [ 1302 - "riscv64" 1303 - ], 1304 - "dev": true, 1305 - "license": "MIT", 1306 - "optional": true, 1307 - "os": [ 1308 - "linux" 1309 - ] 1310 - }, 1311 - "node_modules/@rollup/rollup-linux-s390x-gnu": { 1312 - "version": "4.40.1", 1313 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", 1314 - "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", 1315 - "cpu": [ 1316 - "s390x" 1317 - ], 1318 - "dev": true, 1319 - "license": "MIT", 1320 - "optional": true, 1321 - "os": [ 1322 - "linux" 1323 - ] 1324 - }, 1325 - "node_modules/@rollup/rollup-linux-x64-gnu": { 1326 - "version": "4.40.1", 1327 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", 1328 - "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", 1329 - "cpu": [ 1330 - "x64" 1331 - ], 1332 - "dev": true, 1333 - "license": "MIT", 1334 - "optional": true, 1335 - "os": [ 1336 - "linux" 1337 - ] 1338 - }, 1339 - "node_modules/@rollup/rollup-linux-x64-musl": { 1340 - "version": "4.40.1", 1341 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", 1342 - "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", 1343 - "cpu": [ 1344 - "x64" 1345 - ], 1346 - "dev": true, 1347 - "license": "MIT", 1348 - "optional": true, 1349 - "os": [ 1350 - "linux" 1351 - ] 1352 - }, 1353 - "node_modules/@rollup/rollup-win32-arm64-msvc": { 1354 - "version": "4.40.1", 1355 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", 1356 - "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", 1357 - "cpu": [ 1358 - "arm64" 1359 - ], 1360 - "dev": true, 1361 - "license": "MIT", 1362 - "optional": true, 1363 - "os": [ 1364 - "win32" 1365 - ] 1366 - }, 1367 - "node_modules/@rollup/rollup-win32-ia32-msvc": { 1368 - "version": "4.40.1", 1369 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", 1370 - "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", 1371 - "cpu": [ 1372 - "ia32" 1373 - ], 1374 - "dev": true, 1375 - "license": "MIT", 1376 - "optional": true, 1377 - "os": [ 1378 - "win32" 1379 - ] 1380 - }, 1381 - "node_modules/@rollup/rollup-win32-x64-msvc": { 1382 - "version": "4.40.1", 1383 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", 1384 - "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", 1385 - "cpu": [ 1386 - "x64" 1387 - ], 1388 - "dev": true, 1389 - "license": "MIT", 1390 - "optional": true, 1391 - "os": [ 1392 - "win32" 1393 - ] 1394 - }, 1395 - "node_modules/@types/estree": { 1396 - "version": "1.0.7", 1397 - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 1398 - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 1399 - "dev": true, 1400 - "license": "MIT" 1401 - }, 1402 - "node_modules/@vitest/expect": { 1403 - "version": "3.0.9", 1404 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", 1405 - "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", 1406 - "dev": true, 1407 - "license": "MIT", 1408 - "dependencies": { 1409 - "@vitest/spy": "3.0.9", 1410 - "@vitest/utils": "3.0.9", 1411 - "chai": "^5.2.0", 1412 - "tinyrainbow": "^2.0.0" 1413 - }, 1414 - "funding": { 1415 - "url": "https://opencollective.com/vitest" 1416 - } 1417 - }, 1418 - "node_modules/@vitest/mocker": { 1419 - "version": "3.0.9", 1420 - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", 1421 - "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", 1422 - "dev": true, 1423 - "license": "MIT", 1424 - "dependencies": { 1425 - "@vitest/spy": "3.0.9", 1426 - "estree-walker": "^3.0.3", 1427 - "magic-string": "^0.30.17" 1428 - }, 1429 - "funding": { 1430 - "url": "https://opencollective.com/vitest" 1431 - }, 1432 - "peerDependencies": { 1433 - "msw": "^2.4.9", 1434 - "vite": "^5.0.0 || ^6.0.0" 1435 - }, 1436 - "peerDependenciesMeta": { 1437 - "msw": { 1438 - "optional": true 1439 - }, 1440 - "vite": { 1441 - "optional": true 1442 - } 1443 - } 1444 - }, 1445 - "node_modules/@vitest/pretty-format": { 1446 - "version": "3.1.2", 1447 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", 1448 - "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", 1449 - "dev": true, 1450 - "license": "MIT", 1451 - "dependencies": { 1452 - "tinyrainbow": "^2.0.0" 1453 - }, 1454 - "funding": { 1455 - "url": "https://opencollective.com/vitest" 1456 - } 1457 - }, 1458 - "node_modules/@vitest/runner": { 1459 - "version": "3.0.9", 1460 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", 1461 - "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", 1462 - "dev": true, 1463 - "license": "MIT", 1464 - "dependencies": { 1465 - "@vitest/utils": "3.0.9", 1466 - "pathe": "^2.0.3" 1467 - }, 1468 - "funding": { 1469 - "url": "https://opencollective.com/vitest" 1470 - } 1471 - }, 1472 - "node_modules/@vitest/snapshot": { 1473 - "version": "3.0.9", 1474 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", 1475 - "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", 1476 - "dev": true, 1477 - "license": "MIT", 1478 - "dependencies": { 1479 - "@vitest/pretty-format": "3.0.9", 1480 - "magic-string": "^0.30.17", 1481 - "pathe": "^2.0.3" 1482 - }, 1483 - "funding": { 1484 - "url": "https://opencollective.com/vitest" 1485 - } 1486 - }, 1487 - "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { 1488 - "version": "3.0.9", 1489 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1490 - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1491 - "dev": true, 1492 - "license": "MIT", 1493 - "dependencies": { 1494 - "tinyrainbow": "^2.0.0" 1495 - }, 1496 - "funding": { 1497 - "url": "https://opencollective.com/vitest" 1498 - } 1499 - }, 1500 - "node_modules/@vitest/spy": { 1501 - "version": "3.0.9", 1502 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", 1503 - "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", 1504 - "dev": true, 1505 - "license": "MIT", 1506 - "dependencies": { 1507 - "tinyspy": "^3.0.2" 1508 - }, 1509 - "funding": { 1510 - "url": "https://opencollective.com/vitest" 1511 - } 1512 - }, 1513 - "node_modules/@vitest/utils": { 1514 - "version": "3.0.9", 1515 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", 1516 - "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", 1517 - "dev": true, 1518 - "license": "MIT", 1519 - "dependencies": { 1520 - "@vitest/pretty-format": "3.0.9", 1521 - "loupe": "^3.1.3", 1522 - "tinyrainbow": "^2.0.0" 1523 - }, 1524 - "funding": { 1525 - "url": "https://opencollective.com/vitest" 1526 - } 1527 - }, 1528 - "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { 1529 - "version": "3.0.9", 1530 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1531 - "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1532 - "dev": true, 1533 - "license": "MIT", 1534 - "dependencies": { 1535 - "tinyrainbow": "^2.0.0" 1536 - }, 1537 - "funding": { 1538 - "url": "https://opencollective.com/vitest" 1539 - } 1540 - }, 1541 - "node_modules/acorn": { 1542 - "version": "8.14.0", 1543 - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 1544 - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 1545 - "dev": true, 1546 - "license": "MIT", 1547 - "bin": { 1548 - "acorn": "bin/acorn" 1549 - }, 1550 - "engines": { 1551 - "node": ">=0.4.0" 1552 - } 1553 - }, 1554 - "node_modules/acorn-walk": { 1555 - "version": "8.3.2", 1556 - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", 1557 - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", 1558 - "dev": true, 1559 - "license": "MIT", 1560 - "engines": { 1561 - "node": ">=0.4.0" 1562 - } 1563 - }, 1564 - "node_modules/as-table": { 1565 - "version": "1.0.55", 1566 - "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", 1567 - "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", 1568 - "dev": true, 1569 - "license": "MIT", 1570 - "dependencies": { 1571 - "printable-characters": "^1.0.42" 1572 - } 1573 - }, 1574 - "node_modules/assertion-error": { 1575 - "version": "2.0.1", 1576 - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1577 - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1578 - "dev": true, 1579 - "license": "MIT", 1580 - "engines": { 1581 - "node": ">=12" 1582 - } 1583 - }, 1584 - "node_modules/birpc": { 1585 - "version": "0.2.14", 1586 - "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", 1587 - "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", 1588 - "dev": true, 1589 - "license": "MIT", 1590 - "funding": { 1591 - "url": "https://github.com/sponsors/antfu" 1592 - } 1593 - }, 1594 - "node_modules/blake3-wasm": { 1595 - "version": "2.1.5", 1596 - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 1597 - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 1598 - "dev": true, 1599 - "license": "MIT" 1600 - }, 1601 - "node_modules/cac": { 1602 - "version": "6.7.14", 1603 - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 1604 - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 1605 - "dev": true, 1606 - "license": "MIT", 1607 - "engines": { 1608 - "node": ">=8" 1609 - } 1610 - }, 1611 - "node_modules/chai": { 1612 - "version": "5.2.0", 1613 - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", 1614 - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", 1615 - "dev": true, 1616 - "license": "MIT", 1617 - "dependencies": { 1618 - "assertion-error": "^2.0.1", 1619 - "check-error": "^2.1.1", 1620 - "deep-eql": "^5.0.1", 1621 - "loupe": "^3.1.0", 1622 - "pathval": "^2.0.0" 1623 - }, 1624 - "engines": { 1625 - "node": ">=12" 1626 - } 1627 - }, 1628 - "node_modules/check-error": { 1629 - "version": "2.1.1", 1630 - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 1631 - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 1632 - "dev": true, 1633 - "license": "MIT", 1634 - "engines": { 1635 - "node": ">= 16" 1636 - } 1637 - }, 1638 - "node_modules/cjs-module-lexer": { 1639 - "version": "1.4.3", 1640 - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", 1641 - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", 1642 - "dev": true, 1643 - "license": "MIT" 1644 - }, 1645 - "node_modules/color": { 1646 - "version": "4.2.3", 1647 - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 1648 - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 1649 - "dev": true, 1650 - "license": "MIT", 1651 - "optional": true, 1652 - "dependencies": { 1653 - "color-convert": "^2.0.1", 1654 - "color-string": "^1.9.0" 1655 - }, 1656 - "engines": { 1657 - "node": ">=12.5.0" 1658 - } 1659 - }, 1660 - "node_modules/color-convert": { 1661 - "version": "2.0.1", 1662 - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1663 - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1664 - "dev": true, 1665 - "license": "MIT", 1666 - "optional": true, 1667 - "dependencies": { 1668 - "color-name": "~1.1.4" 1669 - }, 1670 - "engines": { 1671 - "node": ">=7.0.0" 1672 - } 1673 - }, 1674 - "node_modules/color-name": { 1675 - "version": "1.1.4", 1676 - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1677 - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1678 - "dev": true, 1679 - "license": "MIT", 1680 - "optional": true 1681 - }, 1682 - "node_modules/color-string": { 1683 - "version": "1.9.1", 1684 - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1685 - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1686 - "dev": true, 1687 - "license": "MIT", 1688 - "optional": true, 1689 - "dependencies": { 1690 - "color-name": "^1.0.0", 1691 - "simple-swizzle": "^0.2.2" 1692 - } 1693 - }, 1694 - "node_modules/cookie": { 1695 - "version": "0.7.2", 1696 - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 1697 - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 1698 - "dev": true, 1699 - "license": "MIT", 1700 - "engines": { 1701 - "node": ">= 0.6" 1702 - } 1703 - }, 1704 - "node_modules/data-uri-to-buffer": { 1705 - "version": "2.0.2", 1706 - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", 1707 - "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", 1708 - "dev": true, 1709 - "license": "MIT" 1710 - }, 1711 - "node_modules/debug": { 1712 - "version": "4.4.0", 1713 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1714 - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1715 - "dev": true, 1716 - "license": "MIT", 1717 - "dependencies": { 1718 - "ms": "^2.1.3" 1719 - }, 1720 - "engines": { 1721 - "node": ">=6.0" 1722 - }, 1723 - "peerDependenciesMeta": { 1724 - "supports-color": { 1725 - "optional": true 1726 - } 1727 - } 1728 - }, 1729 - "node_modules/deep-eql": { 1730 - "version": "5.0.2", 1731 - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 1732 - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 1733 - "dev": true, 1734 - "license": "MIT", 1735 - "engines": { 1736 - "node": ">=6" 1737 - } 1738 - }, 1739 - "node_modules/defu": { 1740 - "version": "6.1.4", 1741 - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 1742 - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 1743 - "dev": true, 1744 - "license": "MIT" 1745 - }, 1746 - "node_modules/detect-libc": { 1747 - "version": "2.0.4", 1748 - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", 1749 - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", 1750 - "dev": true, 1751 - "license": "Apache-2.0", 1752 - "optional": true, 1753 - "engines": { 1754 - "node": ">=8" 1755 - } 1756 - }, 1757 - "node_modules/devalue": { 1758 - "version": "4.3.3", 1759 - "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", 1760 - "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", 1761 - "dev": true, 1762 - "license": "MIT" 1763 - }, 1764 - "node_modules/es-module-lexer": { 1765 - "version": "1.7.0", 1766 - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1767 - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1768 - "dev": true, 1769 - "license": "MIT" 1770 - }, 1771 - "node_modules/esbuild": { 1772 - "version": "0.25.3", 1773 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", 1774 - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", 1775 - "dev": true, 1776 - "hasInstallScript": true, 1777 - "license": "MIT", 1778 - "bin": { 1779 - "esbuild": "bin/esbuild" 1780 - }, 1781 - "engines": { 1782 - "node": ">=18" 1783 - }, 1784 - "optionalDependencies": { 1785 - "@esbuild/aix-ppc64": "0.25.3", 1786 - "@esbuild/android-arm": "0.25.3", 1787 - "@esbuild/android-arm64": "0.25.3", 1788 - "@esbuild/android-x64": "0.25.3", 1789 - "@esbuild/darwin-arm64": "0.25.3", 1790 - "@esbuild/darwin-x64": "0.25.3", 1791 - "@esbuild/freebsd-arm64": "0.25.3", 1792 - "@esbuild/freebsd-x64": "0.25.3", 1793 - "@esbuild/linux-arm": "0.25.3", 1794 - "@esbuild/linux-arm64": "0.25.3", 1795 - "@esbuild/linux-ia32": "0.25.3", 1796 - "@esbuild/linux-loong64": "0.25.3", 1797 - "@esbuild/linux-mips64el": "0.25.3", 1798 - "@esbuild/linux-ppc64": "0.25.3", 1799 - "@esbuild/linux-riscv64": "0.25.3", 1800 - "@esbuild/linux-s390x": "0.25.3", 1801 - "@esbuild/linux-x64": "0.25.3", 1802 - "@esbuild/netbsd-arm64": "0.25.3", 1803 - "@esbuild/netbsd-x64": "0.25.3", 1804 - "@esbuild/openbsd-arm64": "0.25.3", 1805 - "@esbuild/openbsd-x64": "0.25.3", 1806 - "@esbuild/sunos-x64": "0.25.3", 1807 - "@esbuild/win32-arm64": "0.25.3", 1808 - "@esbuild/win32-ia32": "0.25.3", 1809 - "@esbuild/win32-x64": "0.25.3" 1810 - } 1811 - }, 1812 - "node_modules/estree-walker": { 1813 - "version": "3.0.3", 1814 - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1815 - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1816 - "dev": true, 1817 - "license": "MIT", 1818 - "dependencies": { 1819 - "@types/estree": "^1.0.0" 1820 - } 1821 - }, 1822 - "node_modules/exit-hook": { 1823 - "version": "2.2.1", 1824 - "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 1825 - "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 1826 - "dev": true, 1827 - "license": "MIT", 1828 - "engines": { 1829 - "node": ">=6" 1830 - }, 1831 - "funding": { 1832 - "url": "https://github.com/sponsors/sindresorhus" 1833 - } 1834 - }, 1835 - "node_modules/expect-type": { 1836 - "version": "1.2.1", 1837 - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", 1838 - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", 1839 - "dev": true, 1840 - "license": "Apache-2.0", 1841 - "engines": { 1842 - "node": ">=12.0.0" 1843 - } 1844 - }, 1845 - "node_modules/exsolve": { 1846 - "version": "1.0.5", 1847 - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", 1848 - "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", 1849 - "dev": true, 1850 - "license": "MIT" 1851 - }, 1852 - "node_modules/fdir": { 1853 - "version": "6.4.4", 1854 - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", 1855 - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 1856 - "dev": true, 1857 - "license": "MIT", 1858 - "peerDependencies": { 1859 - "picomatch": "^3 || ^4" 1860 - }, 1861 - "peerDependenciesMeta": { 1862 - "picomatch": { 1863 - "optional": true 1864 - } 1865 - } 1866 - }, 1867 - "node_modules/fsevents": { 1868 - "version": "2.3.3", 1869 - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1870 - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1871 - "dev": true, 1872 - "hasInstallScript": true, 1873 - "license": "MIT", 1874 - "optional": true, 1875 - "os": [ 1876 - "darwin" 1877 - ], 1878 - "engines": { 1879 - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1880 - } 1881 - }, 1882 - "node_modules/get-source": { 1883 - "version": "2.0.12", 1884 - "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", 1885 - "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", 1886 - "dev": true, 1887 - "license": "Unlicense", 1888 - "dependencies": { 1889 - "data-uri-to-buffer": "^2.0.0", 1890 - "source-map": "^0.6.1" 1891 - } 1892 - }, 1893 - "node_modules/glob-to-regexp": { 1894 - "version": "0.4.1", 1895 - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 1896 - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 1897 - "dev": true, 1898 - "license": "BSD-2-Clause" 1899 - }, 1900 - "node_modules/is-arrayish": { 1901 - "version": "0.3.2", 1902 - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 1903 - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 1904 - "dev": true, 1905 - "license": "MIT", 1906 - "optional": true 1907 - }, 1908 - "node_modules/loupe": { 1909 - "version": "3.1.3", 1910 - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", 1911 - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", 1912 - "dev": true, 1913 - "license": "MIT" 1914 - }, 1915 - "node_modules/magic-string": { 1916 - "version": "0.30.17", 1917 - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 1918 - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 1919 - "dev": true, 1920 - "license": "MIT", 1921 - "dependencies": { 1922 - "@jridgewell/sourcemap-codec": "^1.5.0" 1923 - } 1924 - }, 1925 - "node_modules/mime": { 1926 - "version": "3.0.0", 1927 - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1928 - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1929 - "dev": true, 1930 - "license": "MIT", 1931 - "bin": { 1932 - "mime": "cli.js" 1933 - }, 1934 - "engines": { 1935 - "node": ">=10.0.0" 1936 - } 1937 - }, 1938 - "node_modules/miniflare": { 1939 - "version": "4.20250428.1", 1940 - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250428.1.tgz", 1941 - "integrity": "sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==", 1942 - "dev": true, 1943 - "license": "MIT", 1944 - "dependencies": { 1945 - "@cspotcode/source-map-support": "0.8.1", 1946 - "acorn": "8.14.0", 1947 - "acorn-walk": "8.3.2", 1948 - "exit-hook": "2.2.1", 1949 - "glob-to-regexp": "0.4.1", 1950 - "stoppable": "1.1.0", 1951 - "undici": "^5.28.5", 1952 - "workerd": "1.20250428.0", 1953 - "ws": "8.18.0", 1954 - "youch": "3.3.4", 1955 - "zod": "3.22.3" 1956 - }, 1957 - "bin": { 1958 - "miniflare": "bootstrap.js" 1959 - }, 1960 - "engines": { 1961 - "node": ">=18.0.0" 1962 - } 1963 - }, 1964 - "node_modules/miniflare/node_modules/zod": { 1965 - "version": "3.22.3", 1966 - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", 1967 - "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", 1968 - "dev": true, 1969 - "license": "MIT", 1970 - "funding": { 1971 - "url": "https://github.com/sponsors/colinhacks" 1972 - } 1973 - }, 1974 - "node_modules/ms": { 1975 - "version": "2.1.3", 1976 - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1977 - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1978 - "dev": true, 1979 - "license": "MIT" 1980 - }, 1981 - "node_modules/multiformats": { 1982 - "version": "9.9.0", 1983 - "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 1984 - "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 1985 - "license": "(Apache-2.0 AND MIT)" 1986 - }, 1987 - "node_modules/mustache": { 1988 - "version": "4.2.0", 1989 - "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 1990 - "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 1991 - "dev": true, 1992 - "license": "MIT", 1993 - "bin": { 1994 - "mustache": "bin/mustache" 1995 - } 1996 - }, 1997 - "node_modules/nanoid": { 1998 - "version": "3.3.11", 1999 - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 2000 - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 2001 - "dev": true, 2002 - "funding": [ 2003 - { 2004 - "type": "github", 2005 - "url": "https://github.com/sponsors/ai" 2006 - } 2007 - ], 2008 - "license": "MIT", 2009 - "bin": { 2010 - "nanoid": "bin/nanoid.cjs" 2011 - }, 2012 - "engines": { 2013 - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 2014 - } 2015 - }, 2016 - "node_modules/ohash": { 2017 - "version": "2.0.11", 2018 - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", 2019 - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", 2020 - "dev": true, 2021 - "license": "MIT" 2022 - }, 2023 - "node_modules/path-to-regexp": { 2024 - "version": "6.3.0", 2025 - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 2026 - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 2027 - "dev": true, 2028 - "license": "MIT" 2029 - }, 2030 - "node_modules/pathe": { 2031 - "version": "2.0.3", 2032 - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 2033 - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 2034 - "dev": true, 2035 - "license": "MIT" 2036 - }, 2037 - "node_modules/pathval": { 2038 - "version": "2.0.0", 2039 - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 2040 - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 2041 - "dev": true, 2042 - "license": "MIT", 2043 - "engines": { 2044 - "node": ">= 14.16" 2045 - } 2046 - }, 2047 - "node_modules/picocolors": { 2048 - "version": "1.1.1", 2049 - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 2050 - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 2051 - "dev": true, 2052 - "license": "ISC" 2053 - }, 2054 - "node_modules/picomatch": { 2055 - "version": "4.0.2", 2056 - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 2057 - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 2058 - "dev": true, 2059 - "license": "MIT", 2060 - "engines": { 2061 - "node": ">=12" 2062 - }, 2063 - "funding": { 2064 - "url": "https://github.com/sponsors/jonschlinkert" 2065 - } 2066 - }, 2067 - "node_modules/postcss": { 2068 - "version": "8.5.3", 2069 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", 2070 - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", 2071 - "dev": true, 2072 - "funding": [ 2073 - { 2074 - "type": "opencollective", 2075 - "url": "https://opencollective.com/postcss/" 2076 - }, 2077 - { 2078 - "type": "tidelift", 2079 - "url": "https://tidelift.com/funding/github/npm/postcss" 2080 - }, 2081 - { 2082 - "type": "github", 2083 - "url": "https://github.com/sponsors/ai" 2084 - } 2085 - ], 2086 - "license": "MIT", 2087 - "dependencies": { 2088 - "nanoid": "^3.3.8", 2089 - "picocolors": "^1.1.1", 2090 - "source-map-js": "^1.2.1" 2091 - }, 2092 - "engines": { 2093 - "node": "^10 || ^12 || >=14" 2094 - } 2095 - }, 2096 - "node_modules/printable-characters": { 2097 - "version": "1.0.42", 2098 - "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", 2099 - "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", 2100 - "dev": true, 2101 - "license": "Unlicense" 2102 - }, 2103 - "node_modules/rollup": { 2104 - "version": "4.40.1", 2105 - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", 2106 - "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", 2107 - "dev": true, 2108 - "license": "MIT", 2109 - "dependencies": { 2110 - "@types/estree": "1.0.7" 2111 - }, 2112 - "bin": { 2113 - "rollup": "dist/bin/rollup" 2114 - }, 2115 - "engines": { 2116 - "node": ">=18.0.0", 2117 - "npm": ">=8.0.0" 2118 - }, 2119 - "optionalDependencies": { 2120 - "@rollup/rollup-android-arm-eabi": "4.40.1", 2121 - "@rollup/rollup-android-arm64": "4.40.1", 2122 - "@rollup/rollup-darwin-arm64": "4.40.1", 2123 - "@rollup/rollup-darwin-x64": "4.40.1", 2124 - "@rollup/rollup-freebsd-arm64": "4.40.1", 2125 - "@rollup/rollup-freebsd-x64": "4.40.1", 2126 - "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", 2127 - "@rollup/rollup-linux-arm-musleabihf": "4.40.1", 2128 - "@rollup/rollup-linux-arm64-gnu": "4.40.1", 2129 - "@rollup/rollup-linux-arm64-musl": "4.40.1", 2130 - "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", 2131 - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", 2132 - "@rollup/rollup-linux-riscv64-gnu": "4.40.1", 2133 - "@rollup/rollup-linux-riscv64-musl": "4.40.1", 2134 - "@rollup/rollup-linux-s390x-gnu": "4.40.1", 2135 - "@rollup/rollup-linux-x64-gnu": "4.40.1", 2136 - "@rollup/rollup-linux-x64-musl": "4.40.1", 2137 - "@rollup/rollup-win32-arm64-msvc": "4.40.1", 2138 - "@rollup/rollup-win32-ia32-msvc": "4.40.1", 2139 - "@rollup/rollup-win32-x64-msvc": "4.40.1", 2140 - "fsevents": "~2.3.2" 2141 - } 2142 - }, 2143 - "node_modules/semver": { 2144 - "version": "7.7.1", 2145 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 2146 - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 2147 - "dev": true, 2148 - "license": "ISC", 2149 - "bin": { 2150 - "semver": "bin/semver.js" 2151 - }, 2152 - "engines": { 2153 - "node": ">=10" 2154 - } 2155 - }, 2156 - "node_modules/sharp": { 2157 - "version": "0.33.5", 2158 - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", 2159 - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", 2160 - "dev": true, 2161 - "hasInstallScript": true, 2162 - "license": "Apache-2.0", 2163 - "optional": true, 2164 - "dependencies": { 2165 - "color": "^4.2.3", 2166 - "detect-libc": "^2.0.3", 2167 - "semver": "^7.6.3" 2168 - }, 2169 - "engines": { 2170 - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 2171 - }, 2172 - "funding": { 2173 - "url": "https://opencollective.com/libvips" 2174 - }, 2175 - "optionalDependencies": { 2176 - "@img/sharp-darwin-arm64": "0.33.5", 2177 - "@img/sharp-darwin-x64": "0.33.5", 2178 - "@img/sharp-libvips-darwin-arm64": "1.0.4", 2179 - "@img/sharp-libvips-darwin-x64": "1.0.4", 2180 - "@img/sharp-libvips-linux-arm": "1.0.5", 2181 - "@img/sharp-libvips-linux-arm64": "1.0.4", 2182 - "@img/sharp-libvips-linux-s390x": "1.0.4", 2183 - "@img/sharp-libvips-linux-x64": "1.0.4", 2184 - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", 2185 - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", 2186 - "@img/sharp-linux-arm": "0.33.5", 2187 - "@img/sharp-linux-arm64": "0.33.5", 2188 - "@img/sharp-linux-s390x": "0.33.5", 2189 - "@img/sharp-linux-x64": "0.33.5", 2190 - "@img/sharp-linuxmusl-arm64": "0.33.5", 2191 - "@img/sharp-linuxmusl-x64": "0.33.5", 2192 - "@img/sharp-wasm32": "0.33.5", 2193 - "@img/sharp-win32-ia32": "0.33.5", 2194 - "@img/sharp-win32-x64": "0.33.5" 2195 - } 2196 - }, 2197 - "node_modules/siginfo": { 2198 - "version": "2.0.0", 2199 - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 2200 - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 2201 - "dev": true, 2202 - "license": "ISC" 2203 - }, 2204 - "node_modules/simple-swizzle": { 2205 - "version": "0.2.2", 2206 - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 2207 - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 2208 - "dev": true, 2209 - "license": "MIT", 2210 - "optional": true, 2211 - "dependencies": { 2212 - "is-arrayish": "^0.3.1" 2213 - } 2214 - }, 2215 - "node_modules/source-map": { 2216 - "version": "0.6.1", 2217 - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 2218 - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 2219 - "dev": true, 2220 - "license": "BSD-3-Clause", 2221 - "engines": { 2222 - "node": ">=0.10.0" 2223 - } 2224 - }, 2225 - "node_modules/source-map-js": { 2226 - "version": "1.2.1", 2227 - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2228 - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2229 - "dev": true, 2230 - "license": "BSD-3-Clause", 2231 - "engines": { 2232 - "node": ">=0.10.0" 2233 - } 2234 - }, 2235 - "node_modules/stackback": { 2236 - "version": "0.0.2", 2237 - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 2238 - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 2239 - "dev": true, 2240 - "license": "MIT" 2241 - }, 2242 - "node_modules/stacktracey": { 2243 - "version": "2.1.8", 2244 - "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", 2245 - "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", 2246 - "dev": true, 2247 - "license": "Unlicense", 2248 - "dependencies": { 2249 - "as-table": "^1.0.36", 2250 - "get-source": "^2.0.12" 2251 - } 2252 - }, 2253 - "node_modules/std-env": { 2254 - "version": "3.9.0", 2255 - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", 2256 - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", 2257 - "dev": true, 2258 - "license": "MIT" 2259 - }, 2260 - "node_modules/stoppable": { 2261 - "version": "1.1.0", 2262 - "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 2263 - "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 2264 - "dev": true, 2265 - "license": "MIT", 2266 - "engines": { 2267 - "node": ">=4", 2268 - "npm": ">=6" 2269 - } 2270 - }, 2271 - "node_modules/tinybench": { 2272 - "version": "2.9.0", 2273 - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 2274 - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 2275 - "dev": true, 2276 - "license": "MIT" 2277 - }, 2278 - "node_modules/tinyexec": { 2279 - "version": "0.3.2", 2280 - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 2281 - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 2282 - "dev": true, 2283 - "license": "MIT" 2284 - }, 2285 - "node_modules/tinyglobby": { 2286 - "version": "0.2.13", 2287 - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", 2288 - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", 2289 - "dev": true, 2290 - "license": "MIT", 2291 - "dependencies": { 2292 - "fdir": "^6.4.4", 2293 - "picomatch": "^4.0.2" 2294 - }, 2295 - "engines": { 2296 - "node": ">=12.0.0" 2297 - }, 2298 - "funding": { 2299 - "url": "https://github.com/sponsors/SuperchupuDev" 2300 - } 2301 - }, 2302 - "node_modules/tinypool": { 2303 - "version": "1.0.2", 2304 - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", 2305 - "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", 2306 - "dev": true, 2307 - "license": "MIT", 2308 - "engines": { 2309 - "node": "^18.0.0 || >=20.0.0" 2310 - } 2311 - }, 2312 - "node_modules/tinyrainbow": { 2313 - "version": "2.0.0", 2314 - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", 2315 - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", 2316 - "dev": true, 2317 - "license": "MIT", 2318 - "engines": { 2319 - "node": ">=14.0.0" 2320 - } 2321 - }, 2322 - "node_modules/tinyspy": { 2323 - "version": "3.0.2", 2324 - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", 2325 - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", 2326 - "dev": true, 2327 - "license": "MIT", 2328 - "engines": { 2329 - "node": ">=14.0.0" 2330 - } 2331 - }, 2332 - "node_modules/tslib": { 2333 - "version": "2.8.1", 2334 - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2335 - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2336 - "license": "0BSD" 2337 - }, 2338 - "node_modules/ufo": { 2339 - "version": "1.6.1", 2340 - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", 2341 - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", 2342 - "dev": true, 2343 - "license": "MIT" 2344 - }, 2345 - "node_modules/uint8arrays": { 2346 - "version": "3.0.0", 2347 - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 2348 - "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 2349 - "license": "MIT", 2350 - "dependencies": { 2351 - "multiformats": "^9.4.2" 2352 - } 2353 - }, 2354 - "node_modules/undici": { 2355 - "version": "5.29.0", 2356 - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 2357 - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 2358 - "dev": true, 2359 - "license": "MIT", 2360 - "dependencies": { 2361 - "@fastify/busboy": "^2.0.0" 2362 - }, 2363 - "engines": { 2364 - "node": ">=14.0" 2365 - } 2366 - }, 2367 - "node_modules/unenv": { 2368 - "version": "2.0.0-rc.15", 2369 - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz", 2370 - "integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==", 2371 - "dev": true, 2372 - "license": "MIT", 2373 - "dependencies": { 2374 - "defu": "^6.1.4", 2375 - "exsolve": "^1.0.4", 2376 - "ohash": "^2.0.11", 2377 - "pathe": "^2.0.3", 2378 - "ufo": "^1.5.4" 2379 - } 2380 - }, 2381 - "node_modules/unicode-segmenter": { 2382 - "version": "0.14.4", 2383 - "resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.4.tgz", 2384 - "integrity": "sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==", 2385 - "license": "MIT" 2386 - }, 2387 - "node_modules/vite": { 2388 - "version": "6.3.4", 2389 - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", 2390 - "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", 2391 - "dev": true, 2392 - "license": "MIT", 2393 - "dependencies": { 2394 - "esbuild": "^0.25.0", 2395 - "fdir": "^6.4.4", 2396 - "picomatch": "^4.0.2", 2397 - "postcss": "^8.5.3", 2398 - "rollup": "^4.34.9", 2399 - "tinyglobby": "^0.2.13" 2400 - }, 2401 - "bin": { 2402 - "vite": "bin/vite.js" 2403 - }, 2404 - "engines": { 2405 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2406 - }, 2407 - "funding": { 2408 - "url": "https://github.com/vitejs/vite?sponsor=1" 2409 - }, 2410 - "optionalDependencies": { 2411 - "fsevents": "~2.3.3" 2412 - }, 2413 - "peerDependencies": { 2414 - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2415 - "jiti": ">=1.21.0", 2416 - "less": "*", 2417 - "lightningcss": "^1.21.0", 2418 - "sass": "*", 2419 - "sass-embedded": "*", 2420 - "stylus": "*", 2421 - "sugarss": "*", 2422 - "terser": "^5.16.0", 2423 - "tsx": "^4.8.1", 2424 - "yaml": "^2.4.2" 2425 - }, 2426 - "peerDependenciesMeta": { 2427 - "@types/node": { 2428 - "optional": true 2429 - }, 2430 - "jiti": { 2431 - "optional": true 2432 - }, 2433 - "less": { 2434 - "optional": true 2435 - }, 2436 - "lightningcss": { 2437 - "optional": true 2438 - }, 2439 - "sass": { 2440 - "optional": true 2441 - }, 2442 - "sass-embedded": { 2443 - "optional": true 2444 - }, 2445 - "stylus": { 2446 - "optional": true 2447 - }, 2448 - "sugarss": { 2449 - "optional": true 2450 - }, 2451 - "terser": { 2452 - "optional": true 2453 - }, 2454 - "tsx": { 2455 - "optional": true 2456 - }, 2457 - "yaml": { 2458 - "optional": true 2459 - } 2460 - } 2461 - }, 2462 - "node_modules/vite-node": { 2463 - "version": "3.0.9", 2464 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", 2465 - "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", 2466 - "dev": true, 2467 - "license": "MIT", 2468 - "dependencies": { 2469 - "cac": "^6.7.14", 2470 - "debug": "^4.4.0", 2471 - "es-module-lexer": "^1.6.0", 2472 - "pathe": "^2.0.3", 2473 - "vite": "^5.0.0 || ^6.0.0" 2474 - }, 2475 - "bin": { 2476 - "vite-node": "vite-node.mjs" 2477 - }, 2478 - "engines": { 2479 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2480 - }, 2481 - "funding": { 2482 - "url": "https://opencollective.com/vitest" 2483 - } 2484 - }, 2485 - "node_modules/vitest": { 2486 - "version": "3.0.9", 2487 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", 2488 - "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", 2489 - "dev": true, 2490 - "license": "MIT", 2491 - "dependencies": { 2492 - "@vitest/expect": "3.0.9", 2493 - "@vitest/mocker": "3.0.9", 2494 - "@vitest/pretty-format": "^3.0.9", 2495 - "@vitest/runner": "3.0.9", 2496 - "@vitest/snapshot": "3.0.9", 2497 - "@vitest/spy": "3.0.9", 2498 - "@vitest/utils": "3.0.9", 2499 - "chai": "^5.2.0", 2500 - "debug": "^4.4.0", 2501 - "expect-type": "^1.1.0", 2502 - "magic-string": "^0.30.17", 2503 - "pathe": "^2.0.3", 2504 - "std-env": "^3.8.0", 2505 - "tinybench": "^2.9.0", 2506 - "tinyexec": "^0.3.2", 2507 - "tinypool": "^1.0.2", 2508 - "tinyrainbow": "^2.0.0", 2509 - "vite": "^5.0.0 || ^6.0.0", 2510 - "vite-node": "3.0.9", 2511 - "why-is-node-running": "^2.3.0" 2512 - }, 2513 - "bin": { 2514 - "vitest": "vitest.mjs" 2515 - }, 2516 - "engines": { 2517 - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2518 - }, 2519 - "funding": { 2520 - "url": "https://opencollective.com/vitest" 2521 - }, 2522 - "peerDependencies": { 2523 - "@edge-runtime/vm": "*", 2524 - "@types/debug": "^4.1.12", 2525 - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2526 - "@vitest/browser": "3.0.9", 2527 - "@vitest/ui": "3.0.9", 2528 - "happy-dom": "*", 2529 - "jsdom": "*" 2530 - }, 2531 - "peerDependenciesMeta": { 2532 - "@edge-runtime/vm": { 2533 - "optional": true 2534 - }, 2535 - "@types/debug": { 2536 - "optional": true 2537 - }, 2538 - "@types/node": { 2539 - "optional": true 2540 - }, 2541 - "@vitest/browser": { 2542 - "optional": true 2543 - }, 2544 - "@vitest/ui": { 2545 - "optional": true 2546 - }, 2547 - "happy-dom": { 2548 - "optional": true 2549 - }, 2550 - "jsdom": { 2551 - "optional": true 2552 - } 2553 - } 2554 - }, 2555 - "node_modules/why-is-node-running": { 2556 - "version": "2.3.0", 2557 - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 2558 - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 2559 - "dev": true, 2560 - "license": "MIT", 2561 - "dependencies": { 2562 - "siginfo": "^2.0.0", 2563 - "stackback": "0.0.2" 2564 - }, 2565 - "bin": { 2566 - "why-is-node-running": "cli.js" 2567 - }, 2568 - "engines": { 2569 - "node": ">=8" 2570 - } 2571 - }, 2572 - "node_modules/workerd": { 2573 - "version": "1.20250428.0", 2574 - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250428.0.tgz", 2575 - "integrity": "sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==", 2576 - "dev": true, 2577 - "hasInstallScript": true, 2578 - "license": "Apache-2.0", 2579 - "bin": { 2580 - "workerd": "bin/workerd" 2581 - }, 2582 - "engines": { 2583 - "node": ">=16" 2584 - }, 2585 - "optionalDependencies": { 2586 - "@cloudflare/workerd-darwin-64": "1.20250428.0", 2587 - "@cloudflare/workerd-darwin-arm64": "1.20250428.0", 2588 - "@cloudflare/workerd-linux-64": "1.20250428.0", 2589 - "@cloudflare/workerd-linux-arm64": "1.20250428.0", 2590 - "@cloudflare/workerd-windows-64": "1.20250428.0" 2591 - } 2592 - }, 2593 - "node_modules/wrangler": { 2594 - "version": "4.14.1", 2595 - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.14.1.tgz", 2596 - "integrity": "sha512-EU7IThP7i68TBftJJSveogvWZ5k/WRijcJh3UclDWiWWhDZTPbL6LOJEFhHKqFzHOaC4Y2Aewt48rfTz0e7oCw==", 2597 - "dev": true, 2598 - "license": "MIT OR Apache-2.0", 2599 - "dependencies": { 2600 - "@cloudflare/kv-asset-handler": "0.4.0", 2601 - "@cloudflare/unenv-preset": "2.3.1", 2602 - "blake3-wasm": "2.1.5", 2603 - "esbuild": "0.25.2", 2604 - "miniflare": "4.20250428.1", 2605 - "path-to-regexp": "6.3.0", 2606 - "unenv": "2.0.0-rc.15", 2607 - "workerd": "1.20250428.0" 2608 - }, 2609 - "bin": { 2610 - "wrangler": "bin/wrangler.js", 2611 - "wrangler2": "bin/wrangler.js" 2612 - }, 2613 - "engines": { 2614 - "node": ">=18.0.0" 2615 - }, 2616 - "optionalDependencies": { 2617 - "fsevents": "~2.3.2", 2618 - "sharp": "^0.33.5" 2619 - }, 2620 - "peerDependencies": { 2621 - "@cloudflare/workers-types": "^4.20250428.0" 2622 - }, 2623 - "peerDependenciesMeta": { 2624 - "@cloudflare/workers-types": { 2625 - "optional": true 2626 - } 2627 - } 2628 - }, 2629 - "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { 2630 - "version": "0.25.2", 2631 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", 2632 - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", 2633 - "cpu": [ 2634 - "ppc64" 2635 - ], 2636 - "dev": true, 2637 - "license": "MIT", 2638 - "optional": true, 2639 - "os": [ 2640 - "aix" 2641 - ], 2642 - "engines": { 2643 - "node": ">=18" 2644 - } 2645 - }, 2646 - "node_modules/wrangler/node_modules/@esbuild/android-arm": { 2647 - "version": "0.25.2", 2648 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", 2649 - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", 2650 - "cpu": [ 2651 - "arm" 2652 - ], 2653 - "dev": true, 2654 - "license": "MIT", 2655 - "optional": true, 2656 - "os": [ 2657 - "android" 2658 - ], 2659 - "engines": { 2660 - "node": ">=18" 2661 - } 2662 - }, 2663 - "node_modules/wrangler/node_modules/@esbuild/android-arm64": { 2664 - "version": "0.25.2", 2665 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", 2666 - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", 2667 - "cpu": [ 2668 - "arm64" 2669 - ], 2670 - "dev": true, 2671 - "license": "MIT", 2672 - "optional": true, 2673 - "os": [ 2674 - "android" 2675 - ], 2676 - "engines": { 2677 - "node": ">=18" 2678 - } 2679 - }, 2680 - "node_modules/wrangler/node_modules/@esbuild/android-x64": { 2681 - "version": "0.25.2", 2682 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", 2683 - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", 2684 - "cpu": [ 2685 - "x64" 2686 - ], 2687 - "dev": true, 2688 - "license": "MIT", 2689 - "optional": true, 2690 - "os": [ 2691 - "android" 2692 - ], 2693 - "engines": { 2694 - "node": ">=18" 2695 - } 2696 - }, 2697 - "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { 2698 - "version": "0.25.2", 2699 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", 2700 - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", 2701 - "cpu": [ 2702 - "arm64" 2703 - ], 2704 - "dev": true, 2705 - "license": "MIT", 2706 - "optional": true, 2707 - "os": [ 2708 - "darwin" 2709 - ], 2710 - "engines": { 2711 - "node": ">=18" 2712 - } 2713 - }, 2714 - "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { 2715 - "version": "0.25.2", 2716 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", 2717 - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", 2718 - "cpu": [ 2719 - "x64" 2720 - ], 2721 - "dev": true, 2722 - "license": "MIT", 2723 - "optional": true, 2724 - "os": [ 2725 - "darwin" 2726 - ], 2727 - "engines": { 2728 - "node": ">=18" 2729 - } 2730 - }, 2731 - "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { 2732 - "version": "0.25.2", 2733 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", 2734 - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", 2735 - "cpu": [ 2736 - "arm64" 2737 - ], 2738 - "dev": true, 2739 - "license": "MIT", 2740 - "optional": true, 2741 - "os": [ 2742 - "freebsd" 2743 - ], 2744 - "engines": { 2745 - "node": ">=18" 2746 - } 2747 - }, 2748 - "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { 2749 - "version": "0.25.2", 2750 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", 2751 - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", 2752 - "cpu": [ 2753 - "x64" 2754 - ], 2755 - "dev": true, 2756 - "license": "MIT", 2757 - "optional": true, 2758 - "os": [ 2759 - "freebsd" 2760 - ], 2761 - "engines": { 2762 - "node": ">=18" 2763 - } 2764 - }, 2765 - "node_modules/wrangler/node_modules/@esbuild/linux-arm": { 2766 - "version": "0.25.2", 2767 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", 2768 - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", 2769 - "cpu": [ 2770 - "arm" 2771 - ], 2772 - "dev": true, 2773 - "license": "MIT", 2774 - "optional": true, 2775 - "os": [ 2776 - "linux" 2777 - ], 2778 - "engines": { 2779 - "node": ">=18" 2780 - } 2781 - }, 2782 - "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { 2783 - "version": "0.25.2", 2784 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", 2785 - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", 2786 - "cpu": [ 2787 - "arm64" 2788 - ], 2789 - "dev": true, 2790 - "license": "MIT", 2791 - "optional": true, 2792 - "os": [ 2793 - "linux" 2794 - ], 2795 - "engines": { 2796 - "node": ">=18" 2797 - } 2798 - }, 2799 - "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { 2800 - "version": "0.25.2", 2801 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", 2802 - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", 2803 - "cpu": [ 2804 - "ia32" 2805 - ], 2806 - "dev": true, 2807 - "license": "MIT", 2808 - "optional": true, 2809 - "os": [ 2810 - "linux" 2811 - ], 2812 - "engines": { 2813 - "node": ">=18" 2814 - } 2815 - }, 2816 - "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { 2817 - "version": "0.25.2", 2818 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", 2819 - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", 2820 - "cpu": [ 2821 - "loong64" 2822 - ], 2823 - "dev": true, 2824 - "license": "MIT", 2825 - "optional": true, 2826 - "os": [ 2827 - "linux" 2828 - ], 2829 - "engines": { 2830 - "node": ">=18" 2831 - } 2832 - }, 2833 - "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { 2834 - "version": "0.25.2", 2835 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", 2836 - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", 2837 - "cpu": [ 2838 - "mips64el" 2839 - ], 2840 - "dev": true, 2841 - "license": "MIT", 2842 - "optional": true, 2843 - "os": [ 2844 - "linux" 2845 - ], 2846 - "engines": { 2847 - "node": ">=18" 2848 - } 2849 - }, 2850 - "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { 2851 - "version": "0.25.2", 2852 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", 2853 - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", 2854 - "cpu": [ 2855 - "ppc64" 2856 - ], 2857 - "dev": true, 2858 - "license": "MIT", 2859 - "optional": true, 2860 - "os": [ 2861 - "linux" 2862 - ], 2863 - "engines": { 2864 - "node": ">=18" 2865 - } 2866 - }, 2867 - "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { 2868 - "version": "0.25.2", 2869 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", 2870 - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", 2871 - "cpu": [ 2872 - "riscv64" 2873 - ], 2874 - "dev": true, 2875 - "license": "MIT", 2876 - "optional": true, 2877 - "os": [ 2878 - "linux" 2879 - ], 2880 - "engines": { 2881 - "node": ">=18" 2882 - } 2883 - }, 2884 - "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { 2885 - "version": "0.25.2", 2886 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", 2887 - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", 2888 - "cpu": [ 2889 - "s390x" 2890 - ], 2891 - "dev": true, 2892 - "license": "MIT", 2893 - "optional": true, 2894 - "os": [ 2895 - "linux" 2896 - ], 2897 - "engines": { 2898 - "node": ">=18" 2899 - } 2900 - }, 2901 - "node_modules/wrangler/node_modules/@esbuild/linux-x64": { 2902 - "version": "0.25.2", 2903 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", 2904 - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", 2905 - "cpu": [ 2906 - "x64" 2907 - ], 2908 - "dev": true, 2909 - "license": "MIT", 2910 - "optional": true, 2911 - "os": [ 2912 - "linux" 2913 - ], 2914 - "engines": { 2915 - "node": ">=18" 2916 - } 2917 - }, 2918 - "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { 2919 - "version": "0.25.2", 2920 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", 2921 - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", 2922 - "cpu": [ 2923 - "arm64" 2924 - ], 2925 - "dev": true, 2926 - "license": "MIT", 2927 - "optional": true, 2928 - "os": [ 2929 - "netbsd" 2930 - ], 2931 - "engines": { 2932 - "node": ">=18" 2933 - } 2934 - }, 2935 - "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { 2936 - "version": "0.25.2", 2937 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", 2938 - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", 2939 - "cpu": [ 2940 - "x64" 2941 - ], 2942 - "dev": true, 2943 - "license": "MIT", 2944 - "optional": true, 2945 - "os": [ 2946 - "netbsd" 2947 - ], 2948 - "engines": { 2949 - "node": ">=18" 2950 - } 2951 - }, 2952 - "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { 2953 - "version": "0.25.2", 2954 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", 2955 - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", 2956 - "cpu": [ 2957 - "arm64" 2958 - ], 2959 - "dev": true, 2960 - "license": "MIT", 2961 - "optional": true, 2962 - "os": [ 2963 - "openbsd" 2964 - ], 2965 - "engines": { 2966 - "node": ">=18" 2967 - } 2968 - }, 2969 - "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { 2970 - "version": "0.25.2", 2971 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", 2972 - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", 2973 - "cpu": [ 2974 - "x64" 2975 - ], 2976 - "dev": true, 2977 - "license": "MIT", 2978 - "optional": true, 2979 - "os": [ 2980 - "openbsd" 2981 - ], 2982 - "engines": { 2983 - "node": ">=18" 2984 - } 2985 - }, 2986 - "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { 2987 - "version": "0.25.2", 2988 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", 2989 - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", 2990 - "cpu": [ 2991 - "x64" 2992 - ], 2993 - "dev": true, 2994 - "license": "MIT", 2995 - "optional": true, 2996 - "os": [ 2997 - "sunos" 2998 - ], 2999 - "engines": { 3000 - "node": ">=18" 3001 - } 3002 - }, 3003 - "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { 3004 - "version": "0.25.2", 3005 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", 3006 - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", 3007 - "cpu": [ 3008 - "arm64" 3009 - ], 3010 - "dev": true, 3011 - "license": "MIT", 3012 - "optional": true, 3013 - "os": [ 3014 - "win32" 3015 - ], 3016 - "engines": { 3017 - "node": ">=18" 3018 - } 3019 - }, 3020 - "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { 3021 - "version": "0.25.2", 3022 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", 3023 - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", 3024 - "cpu": [ 3025 - "ia32" 3026 - ], 3027 - "dev": true, 3028 - "license": "MIT", 3029 - "optional": true, 3030 - "os": [ 3031 - "win32" 3032 - ], 3033 - "engines": { 3034 - "node": ">=18" 3035 - } 3036 - }, 3037 - "node_modules/wrangler/node_modules/@esbuild/win32-x64": { 3038 - "version": "0.25.2", 3039 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", 3040 - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", 3041 - "cpu": [ 3042 - "x64" 3043 - ], 3044 - "dev": true, 3045 - "license": "MIT", 3046 - "optional": true, 3047 - "os": [ 3048 - "win32" 3049 - ], 3050 - "engines": { 3051 - "node": ">=18" 3052 - } 3053 - }, 3054 - "node_modules/wrangler/node_modules/esbuild": { 3055 - "version": "0.25.2", 3056 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", 3057 - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 3058 - "dev": true, 3059 - "hasInstallScript": true, 3060 - "license": "MIT", 3061 - "bin": { 3062 - "esbuild": "bin/esbuild" 3063 - }, 3064 - "engines": { 3065 - "node": ">=18" 3066 - }, 3067 - "optionalDependencies": { 3068 - "@esbuild/aix-ppc64": "0.25.2", 3069 - "@esbuild/android-arm": "0.25.2", 3070 - "@esbuild/android-arm64": "0.25.2", 3071 - "@esbuild/android-x64": "0.25.2", 3072 - "@esbuild/darwin-arm64": "0.25.2", 3073 - "@esbuild/darwin-x64": "0.25.2", 3074 - "@esbuild/freebsd-arm64": "0.25.2", 3075 - "@esbuild/freebsd-x64": "0.25.2", 3076 - "@esbuild/linux-arm": "0.25.2", 3077 - "@esbuild/linux-arm64": "0.25.2", 3078 - "@esbuild/linux-ia32": "0.25.2", 3079 - "@esbuild/linux-loong64": "0.25.2", 3080 - "@esbuild/linux-mips64el": "0.25.2", 3081 - "@esbuild/linux-ppc64": "0.25.2", 3082 - "@esbuild/linux-riscv64": "0.25.2", 3083 - "@esbuild/linux-s390x": "0.25.2", 3084 - "@esbuild/linux-x64": "0.25.2", 3085 - "@esbuild/netbsd-arm64": "0.25.2", 3086 - "@esbuild/netbsd-x64": "0.25.2", 3087 - "@esbuild/openbsd-arm64": "0.25.2", 3088 - "@esbuild/openbsd-x64": "0.25.2", 3089 - "@esbuild/sunos-x64": "0.25.2", 3090 - "@esbuild/win32-arm64": "0.25.2", 3091 - "@esbuild/win32-ia32": "0.25.2", 3092 - "@esbuild/win32-x64": "0.25.2" 3093 - } 3094 - }, 3095 - "node_modules/ws": { 3096 - "version": "8.18.0", 3097 - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 3098 - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 3099 - "dev": true, 3100 - "license": "MIT", 3101 - "engines": { 3102 - "node": ">=10.0.0" 3103 - }, 3104 - "peerDependencies": { 3105 - "bufferutil": "^4.0.1", 3106 - "utf-8-validate": ">=5.0.2" 3107 - }, 3108 - "peerDependenciesMeta": { 3109 - "bufferutil": { 3110 - "optional": true 3111 - }, 3112 - "utf-8-validate": { 3113 - "optional": true 3114 - } 3115 - } 3116 - }, 3117 - "node_modules/youch": { 3118 - "version": "3.3.4", 3119 - "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", 3120 - "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", 3121 - "dev": true, 3122 - "license": "MIT", 3123 - "dependencies": { 3124 - "cookie": "^0.7.1", 3125 - "mustache": "^4.2.0", 3126 - "stacktracey": "^2.1.8" 3127 - } 3128 - }, 3129 - "node_modules/zod": { 3130 - "version": "3.24.3", 3131 - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", 3132 - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", 3133 - "license": "MIT", 3134 - "funding": { 3135 - "url": "https://github.com/sponsors/colinhacks" 3136 - } 3137 - } 3138 - } 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 + } 3139 3024 }
+14 -17
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 - "dependencies": { 12 - "@atproto/identity": "^0.4.1" 13 - }, 14 - "devDependencies": { 15 - "@cloudflare/vitest-pool-workers": "^0.8.19", 16 - "vitest": "~3.0.7", 17 - "wrangler": "^4.14.1" 18 - } 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 + } 19 16 }
+9 -121
avatar/src/index.js
··· 1 - import { IdResolver } from "@atproto/identity"; 2 - 3 1 export default { 4 2 async fetch(request, env) { 5 3 // Helper function to generate a color from a string ··· 16 14 return color; 17 15 }; 18 16 19 - // Helper function to fetch Tangled profile from PDS 20 - const getTangledAvatarFromPDS = async (actor, resolver) => { 21 - try { 22 - // Resolve the identity 23 - const identity = await resolver.resolve(actor); 24 - if (!identity) { 25 - console.log({ 26 - level: "debug", 27 - message: "failed to resolve identity", 28 - actor: actor, 29 - }); 30 - return null; 31 - } 32 - 33 - const did = identity.did; 34 - const pdsEndpoint = identity.pdsUrl; 35 - 36 - if (!pdsEndpoint) { 37 - console.log({ 38 - level: "debug", 39 - message: "no PDS endpoint found", 40 - actor: actor, 41 - did: did, 42 - }); 43 - return null; 44 - } 45 - 46 - console.log({ 47 - level: "debug", 48 - message: "fetching Tangled profile from PDS", 49 - actor: actor, 50 - did: did, 51 - pdsEndpoint: pdsEndpoint, 52 - }); 53 - 54 - // Fetch the Tangled profile record from PDS 55 - const profileResponse = await fetch( 56 - `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=org.tangled.actor.profile&rkey=self`, 57 - ); 58 - 59 - if (!profileResponse.ok) { 60 - console.log({ 61 - level: "debug", 62 - message: "no Tangled profile found on PDS", 63 - actor: actor, 64 - status: profileResponse.status, 65 - }); 66 - return null; 67 - } 68 - 69 - const profileData = await profileResponse.json(); 70 - const avatarCID = profileData?.value?.avatar; 71 - 72 - if (!avatarCID) { 73 - console.log({ 74 - level: "debug", 75 - message: "Tangled profile has no avatar CID", 76 - actor: actor, 77 - }); 78 - return null; 79 - } 80 - 81 - // Construct blob URL 82 - const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`; 83 - 84 - console.log({ 85 - level: "debug", 86 - message: "found Tangled avatar", 87 - actor: actor, 88 - avatarCID: avatarCID, 89 - }); 90 - 91 - return blobUrl; 92 - } catch (e) { 93 - console.log({ 94 - level: "warn", 95 - message: "error fetching Tangled avatar from PDS", 96 - actor: actor, 97 - error: e.message, 98 - }); 99 - return null; 100 - } 101 - }; 102 - 103 17 const url = new URL(request.url); 104 18 const { pathname, searchParams } = url; 105 19 106 20 if (!pathname || pathname === "/") { 107 - return new Response( 108 - `This is Tangled's avatar service. It fetches your pretty avatar from your PDS, Bluesky, or generates a placeholder. 109 - You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`, 110 - ); 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.`); 111 23 } 112 24 113 25 const size = searchParams.get("size"); ··· 156 68 } 157 69 158 70 try { 159 - let avatarUrl = null; 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; 160 76 161 - // Create identity resolver 162 - const resolver = new IdResolver(); 163 - 164 - // Try to get Tangled avatar from user's PDS first 165 - avatarUrl = await getTangledAvatarFromPDS(actor, resolver); 166 - 167 - // If no Tangled avatar, fall back to Bluesky 168 - if (!avatarUrl) { 169 - console.log({ 170 - level: "debug", 171 - message: "no Tangled avatar, falling back to Bluesky", 172 - actor: actor, 173 - }); 174 - 175 - const profileResponse = await fetch( 176 - `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, 177 - ); 178 - 179 - if (profileResponse.ok) { 180 - const profile = await profileResponse.json(); 181 - avatarUrl = profile.avatar; 182 - } 183 - } 77 + let avatarUrl = profile.avatar; 184 78 185 79 if (!avatarUrl) { 186 80 // Generate a random color based on the actor string 187 - console.log({ 188 - level: "debug", 189 - message: "no avatar found, generating placeholder", 190 - actor: actor, 191 - }); 192 - 193 81 const bgColor = stringToColor(actor); 194 82 const size = resizeToTiny ? 32 : 128; 195 83 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>`; ··· 205 93 return response; 206 94 } 207 95 208 - // Fetch and optionally resize the avatar 96 + // Resize if requested 209 97 let avatarResponse; 210 98 if (resizeToTiny) { 211 99 avatarResponse = await fetch(avatarUrl, {
+182
cmd/dolly/main.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "flag" 6 + "fmt" 7 + "image" 8 + "image/color" 9 + "image/png" 10 + "os" 11 + "path/filepath" 12 + "strconv" 13 + "strings" 14 + "text/template" 15 + 16 + "github.com/srwiley/oksvg" 17 + "github.com/srwiley/rasterx" 18 + "golang.org/x/image/draw" 19 + "tangled.org/core/appview/pages" 20 + "tangled.org/core/ico" 21 + ) 22 + 23 + func main() { 24 + var ( 25 + size string 26 + fillColor string 27 + output string 28 + ) 29 + 30 + flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 31 + flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 32 + flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 33 + flag.Parse() 34 + 35 + width, height, err := parseSize(size) 36 + if err != nil { 37 + fmt.Fprintf(os.Stderr, "Error parsing size: %v\n", err) 38 + os.Exit(1) 39 + } 40 + 41 + // Detect format from file extension 42 + ext := strings.ToLower(filepath.Ext(output)) 43 + format := strings.TrimPrefix(ext, ".") 44 + 45 + if format != "svg" && format != "png" && format != "ico" { 46 + fmt.Fprintf(os.Stderr, "Invalid file extension: %s. Must be .svg, .png, or .ico\n", ext) 47 + os.Exit(1) 48 + } 49 + 50 + if !isValidHexColor(fillColor) { 51 + fmt.Fprintf(os.Stderr, "Invalid color format: %s. Use hex format like #FF5733\n", fillColor) 52 + os.Exit(1) 53 + } 54 + 55 + svgData, err := dolly(fillColor) 56 + if err != nil { 57 + fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 58 + os.Exit(1) 59 + } 60 + 61 + // Create output directory if it doesn't exist 62 + dir := filepath.Dir(output) 63 + if dir != "" && dir != "." { 64 + if err := os.MkdirAll(dir, 0755); err != nil { 65 + fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err) 66 + os.Exit(1) 67 + } 68 + } 69 + 70 + switch format { 71 + case "svg": 72 + err = saveSVG(svgData, output, width, height) 73 + case "png": 74 + err = savePNG(svgData, output, width, height) 75 + case "ico": 76 + err = saveICO(svgData, output, width, height) 77 + } 78 + 79 + if err != nil { 80 + fmt.Fprintf(os.Stderr, "Error saving file: %v\n", err) 81 + os.Exit(1) 82 + } 83 + 84 + fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 85 + } 86 + 87 + func dolly(hexColor string) ([]byte, error) { 88 + tpl, err := template.New("dolly"). 89 + ParseFS(pages.Files, "templates/fragments/dolly/logo.html") 90 + if err != nil { 91 + return nil, err 92 + } 93 + 94 + var svgData bytes.Buffer 95 + if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", pages.DollyParams{ 96 + FillColor: hexColor, 97 + }); err != nil { 98 + return nil, err 99 + } 100 + 101 + return svgData.Bytes(), nil 102 + } 103 + 104 + func svgToImage(svgData []byte, w, h int) (image.Image, error) { 105 + icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 106 + if err != nil { 107 + return nil, fmt.Errorf("error parsing SVG: %v", err) 108 + } 109 + 110 + icon.SetTarget(0, 0, float64(w), float64(h)) 111 + rgba := image.NewRGBA(image.Rect(0, 0, w, h)) 112 + draw.Draw(rgba, rgba.Bounds(), &image.Uniform{color.Transparent}, image.Point{}, draw.Src) 113 + scanner := rasterx.NewScannerGV(w, h, rgba, rgba.Bounds()) 114 + raster := rasterx.NewDasher(w, h, scanner) 115 + icon.Draw(raster, 1.0) 116 + 117 + return rgba, nil 118 + } 119 + 120 + func parseSize(size string) (int, int, error) { 121 + parts := strings.Split(size, "x") 122 + if len(parts) != 2 { 123 + return 0, 0, fmt.Errorf("invalid size format, use WIDTHxHEIGHT") 124 + } 125 + 126 + width, err := strconv.Atoi(parts[0]) 127 + if err != nil { 128 + return 0, 0, fmt.Errorf("invalid width: %v", err) 129 + } 130 + 131 + height, err := strconv.Atoi(parts[1]) 132 + if err != nil { 133 + return 0, 0, fmt.Errorf("invalid height: %v", err) 134 + } 135 + 136 + if width <= 0 || height <= 0 { 137 + return 0, 0, fmt.Errorf("width and height must be positive") 138 + } 139 + 140 + return width, height, nil 141 + } 142 + 143 + func isValidHexColor(hex string) bool { 144 + if len(hex) != 7 || hex[0] != '#' { 145 + return false 146 + } 147 + _, err := strconv.ParseUint(hex[1:], 16, 32) 148 + return err == nil 149 + } 150 + 151 + func saveSVG(svgData []byte, filepath string, _, _ int) error { 152 + return os.WriteFile(filepath, svgData, 0644) 153 + } 154 + 155 + func savePNG(svgData []byte, filepath string, width, height int) error { 156 + img, err := svgToImage(svgData, width, height) 157 + if err != nil { 158 + return err 159 + } 160 + 161 + f, err := os.Create(filepath) 162 + if err != nil { 163 + return err 164 + } 165 + defer f.Close() 166 + 167 + return png.Encode(f, img) 168 + } 169 + 170 + func saveICO(svgData []byte, filepath string, width, height int) error { 171 + img, err := svgToImage(svgData, width, height) 172 + if err != nil { 173 + return err 174 + } 175 + 176 + icoData, err := ico.ImageToIco(img) 177 + if err != nil { 178 + return err 179 + } 180 + 181 + return os.WriteFile(filepath, icoData, 0644) 182 + }
+1527
docs/DOCS.md
··· 1 + --- 2 + title: Tangled docs 3 + author: The Tangled Contributors 4 + date: 21 Sun, Dec 2025 5 + abstract: | 6 + Tangled is a decentralized code hosting and collaboration 7 + platform. Every component of Tangled is open-source and 8 + self-hostable. [tangled.org](https://tangled.org) also 9 + provides hosting and CI services that are free to use. 10 + 11 + There are several models for decentralized code 12 + collaboration platforms, ranging from ActivityPubโ€™s 13 + (Forgejo) federated model, to Radicleโ€™s entirely P2P model. 14 + Our approach attempts to be the best of both worlds by 15 + adopting the AT Protocolโ€”a protocol for building decentralized 16 + social applications with a central identity 17 + 18 + Our approach to this is the idea of โ€œknotsโ€. Knots are 19 + lightweight, headless servers that enable users to host Git 20 + repositories with ease. Knots are designed for either single 21 + or multi-tenant use which is perfect for self-hosting on a 22 + Raspberry Pi at home, or larger โ€œcommunityโ€ servers. By 23 + default, Tangled provides managed knots where you can host 24 + your repositories for free. 25 + 26 + The appview at tangled.org acts as a consolidated "view" 27 + into the whole network, allowing users to access, clone and 28 + contribute to repositories hosted across different knots 29 + seamlessly. 30 + --- 31 + 32 + # Quick start guide 33 + 34 + ## Login or sign up 35 + 36 + You can [login](https://tangled.org) by using your AT Protocol 37 + account. If you are unclear on what that means, simply head 38 + to the [signup](https://tangled.org/signup) page and create 39 + an account. By doing so, you will be choosing Tangled as 40 + your account provider (you will be granted a handle of the 41 + form `user.tngl.sh`). 42 + 43 + In the AT Protocol network, users are free to choose their account 44 + provider (known as a "Personal Data Service", or PDS), and 45 + login to applications that support AT accounts. 46 + 47 + You can think of it as "one account for all of the atmosphere"! 48 + 49 + If you already have an AT account (you may have one if you 50 + signed up to Bluesky, for example), you can login with the 51 + same handle on Tangled (so just use `user.bsky.social` on 52 + the login page). 53 + 54 + ## Add an SSH key 55 + 56 + Once you are logged in, you can start creating repositories 57 + and pushing code. Tangled supports pushing git repositories 58 + over SSH. 59 + 60 + First, you'll need to generate an SSH key if you don't 61 + already have one: 62 + 63 + ```bash 64 + ssh-keygen -t ed25519 -C "foo@bar.com" 65 + ``` 66 + 67 + When prompted, save the key to the default location 68 + (`~/.ssh/id_ed25519`) and optionally set a passphrase. 69 + 70 + Copy your public key to your clipboard: 71 + 72 + ```bash 73 + # on X11 74 + cat ~/.ssh/id_ed25519.pub | xclip -sel c 75 + 76 + # on wayland 77 + cat ~/.ssh/id_ed25519.pub | wl-copy 78 + 79 + # on macos 80 + cat ~/.ssh/id_ed25519.pub | pbcopy 81 + ``` 82 + 83 + Now, navigate to 'Settings' -> 'Keys' and hit 'Add Key', 84 + paste your public key, give it a descriptive name, and hit 85 + save. 86 + 87 + ## Create a repository 88 + 89 + Once your SSH key is added, create your first repository: 90 + 91 + 1. Hit the green `+` icon on the topbar, and select 92 + repository 93 + 2. Enter a repository name 94 + 3. Add a description 95 + 4. Choose a knotserver to host this repository on 96 + 5. Hit create 97 + 98 + Knots are self-hostable, lightweight Git servers that can 99 + host your repository. Unlike traditional code forges, your 100 + code can live on any server. Read the [Knots](TODO) section 101 + for more. 102 + 103 + ## Configure SSH 104 + 105 + To ensure Git uses the correct SSH key and connects smoothly 106 + to Tangled, add this configuration to your `~/.ssh/config` 107 + file: 108 + 109 + ``` 110 + Host tangled.org 111 + Hostname tangled.org 112 + User git 113 + IdentityFile ~/.ssh/id_ed25519 114 + AddressFamily inet 115 + ``` 116 + 117 + This tells SSH to use your specific key when connecting to 118 + Tangled and prevents authentication issues if you have 119 + multiple SSH keys. 120 + 121 + Note that this configuration only works for knotservers that 122 + are hosted by tangled.org. If you use a custom knot, refer 123 + to the [Knots](TODO) section. 124 + 125 + ## Push your first repository 126 + 127 + Initialize a new Git repository: 128 + 129 + ```bash 130 + mkdir my-project 131 + cd my-project 132 + 133 + git init 134 + echo "# My Project" > README.md 135 + ``` 136 + 137 + Add some content and push! 138 + 139 + ```bash 140 + git add README.md 141 + git commit -m "Initial commit" 142 + git remote add origin git@tangled.org:user.tngl.sh/my-project 143 + git push -u origin main 144 + ``` 145 + 146 + That's it! Your code is now hosted on Tangled. 147 + 148 + ## Migrating an existing repository 149 + 150 + Moving your repositories from GitHub, GitLab, Bitbucket, or 151 + any other Git forge to Tangled is straightforward. You'll 152 + simply change your repository's remote URL. At the moment, 153 + Tangled does not have any tooling to migrate data such as 154 + GitHub issues or pull requests. 155 + 156 + First, create a new repository on tangled.org as described 157 + in the [Quick Start Guide](#create-a-repository). 158 + 159 + Navigate to your existing local repository: 160 + 161 + ```bash 162 + cd /path/to/your/existing/repo 163 + ``` 164 + 165 + You can inspect your existing Git remote like so: 166 + 167 + ```bash 168 + git remote -v 169 + ``` 170 + 171 + You'll see something like: 172 + 173 + ``` 174 + origin git@github.com:username/my-project (fetch) 175 + origin git@github.com:username/my-project (push) 176 + ``` 177 + 178 + Update the remote URL to point to tangled: 179 + 180 + ```bash 181 + git remote set-url origin git@tangled.org:user.tngl.sh/my-project 182 + ``` 183 + 184 + Verify the change: 185 + 186 + ```bash 187 + git remote -v 188 + ``` 189 + 190 + You should now see: 191 + 192 + ``` 193 + origin git@tangled.org:user.tngl.sh/my-project (fetch) 194 + origin git@tangled.org:user.tngl.sh/my-project (push) 195 + ``` 196 + 197 + Push all your branches and tags to Tangled: 198 + 199 + ```bash 200 + git push -u origin --all 201 + git push -u origin --tags 202 + ``` 203 + 204 + Your repository is now migrated to Tangled! All commit 205 + history, branches, and tags have been preserved. 206 + 207 + ## Mirroring a repository to Tangled 208 + 209 + If you want to maintain your repository on multiple forges 210 + simultaneously, for example, keeping your primary repository 211 + on GitHub while mirroring to Tangled for backup or 212 + redundancy, you can do so by adding multiple remotes. 213 + 214 + You can configure your local repository to push to both 215 + Tangled and, say, GitHub. You may already have the following 216 + setup: 217 + 218 + ``` 219 + $ git remote -v 220 + origin git@github.com:username/my-project (fetch) 221 + origin git@github.com:username/my-project (push) 222 + ``` 223 + 224 + Now add Tangled as an additional push URL to the same 225 + remote: 226 + 227 + ```bash 228 + git remote set-url --add --push origin git@tangled.org:user.tngl.sh/my-project 229 + ``` 230 + 231 + You also need to re-add the original URL as a push 232 + destination (Git replaces the push URL when you use `--add` 233 + the first time): 234 + 235 + ```bash 236 + git remote set-url --add --push origin git@github.com:username/my-project 237 + ``` 238 + 239 + Verify your configuration: 240 + 241 + ``` 242 + $ git remote -v 243 + origin git@github.com:username/repo (fetch) 244 + origin git@tangled.org:username/my-project (push) 245 + origin git@github.com:username/repo (push) 246 + ``` 247 + 248 + Notice that there's one fetch URL (the primary remote) and 249 + two push URLs. Now, whenever you push, Git will 250 + automatically push to both remotes: 251 + 252 + ```bash 253 + git push origin main 254 + ``` 255 + 256 + This single command pushes your `main` branch to both GitHub 257 + and Tangled simultaneously. 258 + 259 + To push all branches and tags: 260 + 261 + ```bash 262 + git push origin --all 263 + git push origin --tags 264 + ``` 265 + 266 + If you prefer more control over which remote you push to, 267 + you can maintain separate remotes: 268 + 269 + ```bash 270 + git remote add github git@github.com:username/my-project 271 + git remote add tangled git@tangled.org:username/my-project 272 + ``` 273 + 274 + Then push to each explicitly: 275 + 276 + ```bash 277 + git push github main 278 + git push tangled main 279 + ``` 280 + 281 + # Knot self-hosting guide 282 + 283 + So you want to run your own knot server? Great! Here are a few prerequisites: 284 + 285 + 1. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux distribution of some kind. 286 + 2. A (sub)domain name. People generally use `knot.example.com`. 287 + 3. A valid SSL certificate for your domain. 288 + 289 + ## NixOS 290 + 291 + Refer to the [knot 292 + module](https://tangled.org/tangled.org/core/blob/master/nix/modules/knot.nix) 293 + for a full list of options. Sample configurations: 294 + 295 + - [The test VM](https://tangled.org/tangled.org/core/blob/master/nix/vm.nix#L85) 296 + - [@pyrox.dev/nix](https://tangled.org/pyrox.dev/nix/blob/d19571cc1b5fe01035e1e6951ec8cf8a476b4dee/hosts/marvin/services/tangled.nix#L15-25) 297 + 298 + ## Docker 299 + 300 + Refer to 301 + [@tangled.org/knot-docker](https://tangled.org/@tangled.org/knot-docker). 302 + Note that this is community maintained. 303 + 304 + ## Manual setup 305 + 306 + First, clone this repository: 307 + 308 + ``` 309 + git clone https://tangled.org/@tangled.org/core 310 + ``` 311 + 312 + Then, build the `knot` CLI. This is the knot administration 313 + and operation tool. For the purpose of this guide, we're 314 + only concerned with these subcommands: 315 + 316 + * `knot server`: the main knot server process, typically 317 + run as a supervised service 318 + * `knot guard`: handles role-based access control for git 319 + over SSH (you'll never have to run this yourself) 320 + * `knot keys`: fetches SSH keys associated with your knot; 321 + we'll use this to generate the SSH 322 + `AuthorizedKeysCommand` 323 + 324 + ``` 325 + cd core 326 + export CGO_ENABLED=1 327 + go build -o knot ./cmd/knot 328 + ``` 329 + 330 + Next, move the `knot` binary to a location owned by `root` -- 331 + `/usr/local/bin/` is a good choice. Make sure the binary itself is also owned by `root`: 332 + 333 + ``` 334 + sudo mv knot /usr/local/bin/knot 335 + sudo chown root:root /usr/local/bin/knot 336 + ``` 337 + 338 + This is necessary because SSH `AuthorizedKeysCommand` requires [really 339 + specific permissions](https://stackoverflow.com/a/27638306). The 340 + `AuthorizedKeysCommand` specifies a command that is run by `sshd` to 341 + retrieve a user's public SSH keys dynamically for authentication. Let's 342 + set that up. 343 + 344 + ``` 345 + sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF 346 + Match User git 347 + AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys 348 + AuthorizedKeysCommandUser nobody 349 + EOF 350 + ``` 351 + 352 + Then, reload `sshd`: 353 + 354 + ``` 355 + sudo systemctl reload ssh 356 + ``` 357 + 358 + Next, create the `git` user. We'll use the `git` user's home directory 359 + to store repositories: 360 + 361 + ``` 362 + sudo adduser git 363 + ``` 364 + 365 + Create `/home/git/.knot.env` with the following, updating the values as 366 + necessary. The `KNOT_SERVER_OWNER` should be set to your 367 + DID, you can find your DID in the [Settings](https://tangled.sh/settings) page. 368 + 369 + ``` 370 + KNOT_REPO_SCAN_PATH=/home/git 371 + KNOT_SERVER_HOSTNAME=knot.example.com 372 + APPVIEW_ENDPOINT=https://tangled.org 373 + KNOT_SERVER_OWNER=did:plc:foobar 374 + KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 375 + KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 376 + ``` 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) 381 + to `/etc/systemd/system/`. Then, run: 382 + 383 + ``` 384 + systemctl enable knotserver 385 + systemctl start knotserver 386 + ``` 387 + 388 + The last step is to configure a reverse proxy like Nginx or Caddy to front your 389 + knot. Here's an example configuration for Nginx: 390 + 391 + ``` 392 + server { 393 + listen 80; 394 + listen [::]:80; 395 + server_name knot.example.com; 396 + 397 + location / { 398 + proxy_pass http://localhost:5555; 399 + proxy_set_header Host $host; 400 + proxy_set_header X-Real-IP $remote_addr; 401 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 402 + proxy_set_header X-Forwarded-Proto $scheme; 403 + } 404 + 405 + # wss endpoint for git events 406 + location /events { 407 + proxy_set_header X-Forwarded-For $remote_addr; 408 + proxy_set_header Host $http_host; 409 + proxy_set_header Upgrade websocket; 410 + proxy_set_header Connection Upgrade; 411 + proxy_pass http://localhost:5555; 412 + } 413 + # additional config for SSL/TLS go here. 414 + } 415 + 416 + ``` 417 + 418 + Remember to use Let's Encrypt or similar to procure a certificate for your 419 + knot domain. 420 + 421 + You should now have a running knot server! You can finalize 422 + your registration by hitting the `verify` button on the 423 + [/settings/knots](https://tangled.org/settings/knots) page. This simply creates 424 + a record on your PDS to announce the existence of the knot. 425 + 426 + ### Custom paths 427 + 428 + (This section applies to manual setup only. Docker users should edit the mounts 429 + in `docker-compose.yml` instead.) 430 + 431 + Right now, the database and repositories of your knot lives in `/home/git`. You 432 + can move these paths if you'd like to store them in another folder. Be careful 433 + when adjusting these paths: 434 + 435 + * Stop your knot when moving data (e.g. `systemctl stop knotserver`) to prevent 436 + any possible side effects. Remember to restart it once you're done. 437 + * Make backups before moving in case something goes wrong. 438 + * Make sure the `git` user can read and write from the new paths. 439 + 440 + #### Database 441 + 442 + As an example, let's say the current database is at `/home/git/knotserver.db`, 443 + and we want to move it to `/home/git/database/knotserver.db`. 444 + 445 + Copy the current database to the new location. Make sure to copy the `.db-shm` 446 + and `.db-wal` files if they exist. 447 + 448 + ``` 449 + mkdir /home/git/database 450 + cp /home/git/knotserver.db* /home/git/database 451 + ``` 452 + 453 + In the environment (e.g. `/home/git/.knot.env`), set `KNOT_SERVER_DB_PATH` to 454 + the new file path (_not_ the directory): 455 + 456 + ``` 457 + KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db 458 + ``` 459 + 460 + #### Repositories 461 + 462 + As an example, let's say the repositories are currently in `/home/git`, and we 463 + want to move them into `/home/git/repositories`. 464 + 465 + Create the new folder, then move the existing repositories (if there are any): 466 + 467 + ``` 468 + mkdir /home/git/repositories 469 + # move all DIDs into the new folder; these will vary for you! 470 + mv /home/git/did:plc:wshs7t2adsemcrrd4snkeqli /home/git/repositories 471 + ``` 472 + 473 + In the environment (e.g. `/home/git/.knot.env`), update `KNOT_REPO_SCAN_PATH` 474 + to the new directory: 475 + 476 + ``` 477 + KNOT_REPO_SCAN_PATH=/home/git/repositories 478 + ``` 479 + 480 + Similarly, update your `sshd` `AuthorizedKeysCommand` to use the updated 481 + repository path: 482 + 483 + ``` 484 + sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF 485 + Match User git 486 + AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys -git-dir /home/git/repositories 487 + AuthorizedKeysCommandUser nobody 488 + EOF 489 + ``` 490 + 491 + Make sure to restart your SSH server! 492 + 493 + #### MOTD (message of the day) 494 + 495 + To configure the MOTD used ("Welcome to this knot!" by default), edit the 496 + `/home/git/motd` file: 497 + 498 + ``` 499 + printf "Hi from this knot!\n" > /home/git/motd 500 + ``` 501 + 502 + Note that you should add a newline at the end if setting a non-empty message 503 + since the knot won't do this for you. 504 + 505 + # Spindles 506 + 507 + ## Pipelines 508 + 509 + Spindle workflows allow you to write CI/CD pipelines in a 510 + simple format. They're located in the `.tangled/workflows` 511 + directory at the root of your repository, and are defined 512 + using YAML. 513 + 514 + The fields are: 515 + 516 + - [Trigger](#trigger): A **required** field that defines 517 + when a workflow should be triggered. 518 + - [Engine](#engine): A **required** field that defines which 519 + engine a workflow should run on. 520 + - [Clone options](#clone-options): An **optional** field 521 + that defines how the repository should be cloned. 522 + - [Dependencies](#dependencies): An **optional** field that 523 + allows you to list dependencies you may need. 524 + - [Environment](#environment): An **optional** field that 525 + allows you to define environment variables. 526 + - [Steps](#steps): An **optional** field that allows you to 527 + define what steps should run in the workflow. 528 + 529 + ### Trigger 530 + 531 + The first thing to add to a workflow is the trigger, which 532 + defines when a workflow runs. This is defined using a `when` 533 + field, which takes in a list of conditions. Each condition 534 + has the following fields: 535 + 536 + - `event`: This is a **required** field that defines when 537 + your workflow should run. It's a list that can take one or 538 + more of the following values: 539 + - `push`: The workflow should run every time a commit is 540 + pushed to the repository. 541 + - `pull_request`: The workflow should run every time a 542 + pull request is made or updated. 543 + - `manual`: The workflow can be triggered manually. 544 + - `branch`: Defines which branches the workflow should run 545 + for. If used with the `push` event, commits to the 546 + branch(es) listed here will trigger the workflow. If used 547 + with the `pull_request` event, updates to pull requests 548 + targeting the branch(es) listed here will trigger the 549 + workflow. This field has no effect with the `manual` 550 + event. Supports glob patterns using `*` and `**` (e.g., 551 + `main`, `develop`, `release-*`). Either `branch` or `tag` 552 + (or both) must be specified for `push` events. 553 + - `tag`: Defines which tags the workflow should run for. 554 + Only used with the `push` event - when tags matching the 555 + pattern(s) listed here are pushed, the workflow will 556 + trigger. This field has no effect with `pull_request` or 557 + `manual` events. Supports glob patterns using `*` and `**` 558 + (e.g., `v*`, `v1.*`, `release-**`). Either `branch` or 559 + `tag` (or both) must be specified for `push` events. 560 + 561 + For example, if you'd like to define a workflow that runs 562 + when commits are pushed to the `main` and `develop` 563 + branches, or when pull requests that target the `main` 564 + branch are updated, or manually, you can do so with: 565 + 566 + ```yaml 567 + when: 568 + - event: ["push", "manual"] 569 + branch: ["main", "develop"] 570 + - event: ["pull_request"] 571 + branch: ["main"] 572 + ``` 573 + 574 + You can also trigger workflows on tag pushes. For instance, 575 + to run a deployment workflow when tags matching `v*` are 576 + pushed: 577 + 578 + ```yaml 579 + when: 580 + - event: ["push"] 581 + tag: ["v*"] 582 + ``` 583 + 584 + You can even combine branch and tag patterns in a single 585 + constraint (the workflow triggers if either matches): 586 + 587 + ```yaml 588 + when: 589 + - event: ["push"] 590 + branch: ["main", "release-*"] 591 + tag: ["v*", "stable"] 592 + ``` 593 + 594 + ### Engine 595 + 596 + Next is the engine on which the workflow should run, defined 597 + using the **required** `engine` field. The currently 598 + supported engines are: 599 + 600 + - `nixery`: This uses an instance of 601 + [Nixery](https://nixery.dev) to run steps, which allows 602 + you to add [dependencies](#dependencies) from 603 + Nixpkgs (https://github.com/NixOS/nixpkgs). You can 604 + search for packages on https://search.nixos.org, and 605 + there's a pretty good chance the package(s) you're looking 606 + for will be there. 607 + 608 + Example: 609 + 610 + ```yaml 611 + engine: "nixery" 612 + ``` 613 + 614 + ### Clone options 615 + 616 + When a workflow starts, the first step is to clone the 617 + repository. You can customize this behavior using the 618 + **optional** `clone` field. It has the following fields: 619 + 620 + - `skip`: Setting this to `true` will skip cloning the 621 + repository. This can be useful if your workflow is doing 622 + something that doesn't require anything from the 623 + repository itself. This is `false` by default. 624 + - `depth`: This sets the number of commits, or the "clone 625 + depth", to fetch from the repository. For example, if you 626 + set this to 2, the last 2 commits will be fetched. By 627 + default, the depth is set to 1, meaning only the most 628 + recent commit will be fetched, which is the commit that 629 + triggered the workflow. 630 + - `submodules`: If you use Git submodules 631 + (https://git-scm.com/book/en/v2/Git-Tools-Submodules) 632 + in your repository, setting this field to `true` will 633 + recursively fetch all submodules. This is `false` by 634 + default. 635 + 636 + The default settings are: 637 + 638 + ```yaml 639 + clone: 640 + skip: false 641 + depth: 1 642 + submodules: false 643 + ``` 644 + 645 + ### Dependencies 646 + 647 + Usually when you're running a workflow, you'll need 648 + additional dependencies. The `dependencies` field lets you 649 + define which dependencies to get, and from where. It's a 650 + key-value map, with the key being the registry to fetch 651 + dependencies from, and the value being the list of 652 + dependencies to fetch. 653 + 654 + Say you want to fetch Node.js and Go from `nixpkgs`, and a 655 + package called `my_pkg` you've made from your own registry 656 + at your repository at 657 + `https://tangled.org/@example.com/my_pkg`. You can define 658 + those dependencies like so: 659 + 660 + ```yaml 661 + dependencies: 662 + # nixpkgs 663 + nixpkgs: 664 + - nodejs 665 + - go 666 + # custom registry 667 + git+https://tangled.org/@example.com/my_pkg: 668 + - my_pkg 669 + ``` 670 + 671 + Now these dependencies are available to use in your 672 + workflow! 673 + 674 + ### Environment 675 + 676 + The `environment` field allows you define environment 677 + variables that will be available throughout the entire 678 + workflow. **Do not put secrets here, these environment 679 + variables are visible to anyone viewing the repository. You 680 + can add secrets for pipelines in your repository's 681 + settings.** 682 + 683 + Example: 684 + 685 + ```yaml 686 + environment: 687 + GOOS: "linux" 688 + GOARCH: "arm64" 689 + NODE_ENV: "production" 690 + MY_ENV_VAR: "MY_ENV_VALUE" 691 + ``` 692 + 693 + ### Steps 694 + 695 + The `steps` field allows you to define what steps should run 696 + in the workflow. It's a list of step objects, each with the 697 + following fields: 698 + 699 + - `name`: This field allows you to give your step a name. 700 + This name is visible in your workflow runs, and is used to 701 + describe what the step is doing. 702 + - `command`: This field allows you to define a command to 703 + run in that step. The step is run in a Bash shell, and the 704 + logs from the command will be visible in the pipelines 705 + page on the Tangled website. The 706 + [dependencies](#dependencies) you added will be available 707 + to use here. 708 + - `environment`: Similar to the global 709 + [environment](#environment) config, this **optional** 710 + field is a key-value map that allows you to set 711 + environment variables for the step. **Do not put secrets 712 + here, these environment variables are visible to anyone 713 + viewing the repository. You can add secrets for pipelines 714 + in your repository's settings.** 715 + 716 + Example: 717 + 718 + ```yaml 719 + steps: 720 + - name: "Build backend" 721 + command: "go build" 722 + environment: 723 + GOOS: "darwin" 724 + GOARCH: "arm64" 725 + - name: "Build frontend" 726 + command: "npm run build" 727 + environment: 728 + NODE_ENV: "production" 729 + ``` 730 + 731 + ### Complete workflow 732 + 733 + ```yaml 734 + # .tangled/workflows/build.yml 735 + 736 + when: 737 + - event: ["push", "manual"] 738 + branch: ["main", "develop"] 739 + - event: ["pull_request"] 740 + branch: ["main"] 741 + 742 + engine: "nixery" 743 + 744 + # using the default values 745 + clone: 746 + skip: false 747 + depth: 1 748 + submodules: false 749 + 750 + dependencies: 751 + # nixpkgs 752 + nixpkgs: 753 + - nodejs 754 + - go 755 + # custom registry 756 + git+https://tangled.org/@example.com/my_pkg: 757 + - my_pkg 758 + 759 + environment: 760 + GOOS: "linux" 761 + GOARCH: "arm64" 762 + NODE_ENV: "production" 763 + MY_ENV_VAR: "MY_ENV_VALUE" 764 + 765 + steps: 766 + - name: "Build backend" 767 + command: "go build" 768 + environment: 769 + GOOS: "darwin" 770 + GOARCH: "arm64" 771 + - name: "Build frontend" 772 + command: "npm run build" 773 + environment: 774 + NODE_ENV: "production" 775 + ``` 776 + 777 + If you want another example of a workflow, you can look at 778 + the one [Tangled uses to build the 779 + project](https://tangled.org/@tangled.org/core/blob/master/.tangled/workflows/build.yml). 780 + 781 + ## Self-hosting guide 782 + 783 + ### Prerequisites 784 + 785 + * Go 786 + * Docker (the only supported backend currently) 787 + 788 + ### Configuration 789 + 790 + Spindle is configured using environment variables. The following environment variables are available: 791 + 792 + * `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`). 793 + * `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`). 794 + * `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required). 795 + * `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`). 796 + * `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`). 797 + * `SPINDLE_SERVER_OWNER`: The DID of the owner (required). 798 + * `SPINDLE_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`). 799 + * `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT`: The default workflow timeout (default: `"5m"`). 800 + * `SPINDLE_PIPELINES_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`). 801 + 802 + ### Running spindle 803 + 804 + 1. **Set the environment variables.** For example: 805 + 806 + ```shell 807 + export SPINDLE_SERVER_HOSTNAME="your-hostname" 808 + export SPINDLE_SERVER_OWNER="your-did" 809 + ``` 810 + 811 + 2. **Build the Spindle binary.** 812 + 813 + ```shell 814 + cd core 815 + go mod download 816 + go build -o cmd/spindle/spindle cmd/spindle/main.go 817 + ``` 818 + 819 + 3. **Create the log directory.** 820 + 821 + ```shell 822 + sudo mkdir -p /var/log/spindle 823 + sudo chown $USER:$USER -R /var/log/spindle 824 + ``` 825 + 826 + 4. **Run the Spindle binary.** 827 + 828 + ```shell 829 + ./cmd/spindle/spindle 830 + ``` 831 + 832 + Spindle will now start, connect to the Jetstream server, and begin processing pipelines. 833 + 834 + ## Architecture 835 + 836 + Spindle is a small CI runner service. Here's a high-level overview of how it operates: 837 + 838 + * Listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and 839 + [`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream. 840 + * When a new repo record comes through (typically when you add a spindle to a 841 + repo from the settings), spindle then resolves the underlying knot and 842 + subscribes to repo events (see: 843 + [`sh.tangled.pipeline`](/lexicons/pipeline.json)). 844 + * The spindle engine then handles execution of the pipeline, with results and 845 + logs beamed on the spindle event stream over WebSocket 846 + 847 + ### The engine 848 + 849 + At present, the only supported backend is Docker (and Podman, if Docker 850 + compatibility is enabled, so that `/run/docker.sock` is created). spindle 851 + executes each step in the pipeline in a fresh container, with state persisted 852 + across steps within the `/tangled/workspace` directory. 853 + 854 + The base image for the container is constructed on the fly using 855 + [Nixery](https://nixery.dev), which is handy for caching layers for frequently 856 + used packages. 857 + 858 + The pipeline manifest is [specified here](https://docs.tangled.org/spindles.html#pipelines). 859 + 860 + ## Secrets with openbao 861 + 862 + This document covers setting up spindle to use OpenBao for secrets 863 + management via OpenBao Proxy instead of the default SQLite backend. 864 + 865 + ### Overview 866 + 867 + Spindle now uses OpenBao Proxy for secrets management. The proxy handles 868 + authentication automatically using AppRole credentials, while spindle 869 + connects to the local proxy instead of directly to the OpenBao server. 870 + 871 + This approach provides better security, automatic token renewal, and 872 + simplified application code. 873 + 874 + ### Installation 875 + 876 + Install OpenBao from Nixpkgs: 877 + 878 + ```bash 879 + nix shell nixpkgs#openbao # for a local server 880 + ``` 881 + 882 + ### Setup 883 + 884 + The setup process can is documented for both local development and production. 885 + 886 + #### Local development 887 + 888 + Start OpenBao in dev mode: 889 + 890 + ```bash 891 + bao server -dev -dev-root-token-id="root" -dev-listen-address=127.0.0.1:8201 892 + ``` 893 + 894 + This starts OpenBao on `http://localhost:8201` with a root token. 895 + 896 + Set up environment for bao CLI: 897 + 898 + ```bash 899 + export BAO_ADDR=http://localhost:8200 900 + export BAO_TOKEN=root 901 + ``` 902 + 903 + #### Production 904 + 905 + You would typically use a systemd service with a 906 + configuration file. Refer to 907 + [@tangled.org/infra](https://tangled.org/@tangled.org/infra) 908 + for how this can be achieved using Nix. 909 + 910 + Then, initialize the bao server: 911 + 912 + ```bash 913 + bao operator init -key-shares=1 -key-threshold=1 914 + ``` 915 + 916 + This will print out an unseal key and a root key. Save them 917 + somewhere (like a password manager). Then unseal the vault 918 + to begin setting it up: 919 + 920 + ```bash 921 + bao operator unseal <unseal_key> 922 + ``` 923 + 924 + All steps below remain the same across both dev and 925 + production setups. 926 + 927 + #### Configure openbao server 928 + 929 + Create the spindle KV mount: 930 + 931 + ```bash 932 + bao secrets enable -path=spindle -version=2 kv 933 + ``` 934 + 935 + Set up AppRole authentication and policy: 936 + 937 + Create a policy file `spindle-policy.hcl`: 938 + 939 + ```hcl 940 + # Full access to spindle KV v2 data 941 + path "spindle/data/*" { 942 + capabilities = ["create", "read", "update", "delete"] 943 + } 944 + 945 + # Access to metadata for listing and management 946 + path "spindle/metadata/*" { 947 + capabilities = ["list", "read", "delete", "update"] 948 + } 949 + 950 + # Allow listing at root level 951 + path "spindle/" { 952 + capabilities = ["list"] 953 + } 954 + 955 + # Required for connection testing and health checks 956 + path "auth/token/lookup-self" { 957 + capabilities = ["read"] 958 + } 959 + ``` 960 + 961 + Apply the policy and create an AppRole: 962 + 963 + ```bash 964 + bao policy write spindle-policy spindle-policy.hcl 965 + bao auth enable approle 966 + bao write auth/approle/role/spindle \ 967 + token_policies="spindle-policy" \ 968 + token_ttl=1h \ 969 + token_max_ttl=4h \ 970 + bind_secret_id=true \ 971 + secret_id_ttl=0 \ 972 + secret_id_num_uses=0 973 + ``` 974 + 975 + Get the credentials: 976 + 977 + ```bash 978 + # Get role ID (static) 979 + ROLE_ID=$(bao read -field=role_id auth/approle/role/spindle/role-id) 980 + 981 + # Generate secret ID 982 + SECRET_ID=$(bao write -f -field=secret_id auth/approle/role/spindle/secret-id) 983 + 984 + echo "Role ID: $ROLE_ID" 985 + echo "Secret ID: $SECRET_ID" 986 + ``` 987 + 988 + #### Create proxy configuration 989 + 990 + Create the credential files: 991 + 992 + ```bash 993 + # Create directory for OpenBao files 994 + mkdir -p /tmp/openbao 995 + 996 + # Save credentials 997 + echo "$ROLE_ID" > /tmp/openbao/role-id 998 + echo "$SECRET_ID" > /tmp/openbao/secret-id 999 + chmod 600 /tmp/openbao/role-id /tmp/openbao/secret-id 1000 + ``` 1001 + 1002 + Create a proxy configuration file `/tmp/openbao/proxy.hcl`: 1003 + 1004 + ```hcl 1005 + # OpenBao server connection 1006 + vault { 1007 + address = "http://localhost:8200" 1008 + } 1009 + 1010 + # Auto-Auth using AppRole 1011 + auto_auth { 1012 + method "approle" { 1013 + mount_path = "auth/approle" 1014 + config = { 1015 + role_id_file_path = "/tmp/openbao/role-id" 1016 + secret_id_file_path = "/tmp/openbao/secret-id" 1017 + } 1018 + } 1019 + 1020 + # Optional: write token to file for debugging 1021 + sink "file" { 1022 + config = { 1023 + path = "/tmp/openbao/token" 1024 + mode = 0640 1025 + } 1026 + } 1027 + } 1028 + 1029 + # Proxy listener for spindle 1030 + listener "tcp" { 1031 + address = "127.0.0.1:8201" 1032 + tls_disable = true 1033 + } 1034 + 1035 + # Enable API proxy with auto-auth token 1036 + api_proxy { 1037 + use_auto_auth_token = true 1038 + } 1039 + 1040 + # Enable response caching 1041 + cache { 1042 + use_auto_auth_token = true 1043 + } 1044 + 1045 + # Logging 1046 + log_level = "info" 1047 + ``` 1048 + 1049 + #### Start the proxy 1050 + 1051 + Start OpenBao Proxy: 1052 + 1053 + ```bash 1054 + bao proxy -config=/tmp/openbao/proxy.hcl 1055 + ``` 1056 + 1057 + The proxy will authenticate with OpenBao and start listening on 1058 + `127.0.0.1:8201`. 1059 + 1060 + #### Configure spindle 1061 + 1062 + Set these environment variables for spindle: 1063 + 1064 + ```bash 1065 + export SPINDLE_SERVER_SECRETS_PROVIDER=openbao 1066 + export SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=http://127.0.0.1:8201 1067 + export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle 1068 + ``` 1069 + 1070 + On startup, spindle will now connect to the local proxy, 1071 + which handles all authentication automatically. 1072 + 1073 + ### Production setup for proxy 1074 + 1075 + For production, you'll want to run the proxy as a service: 1076 + 1077 + Place your production configuration in 1078 + `/etc/openbao/proxy.hcl` with proper TLS settings for the 1079 + vault connection. 1080 + 1081 + ### Verifying setup 1082 + 1083 + Test the proxy directly: 1084 + 1085 + ```bash 1086 + # Check proxy health 1087 + curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/sys/health 1088 + 1089 + # Test token lookup through proxy 1090 + curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/auth/token/lookup-self 1091 + ``` 1092 + 1093 + Test OpenBao operations through the server: 1094 + 1095 + ```bash 1096 + # List all secrets 1097 + bao kv list spindle/ 1098 + 1099 + # Add a test secret via the spindle API, then check it exists 1100 + bao kv list spindle/repos/ 1101 + 1102 + # Get a specific secret 1103 + bao kv get spindle/repos/your_repo_path/SECRET_NAME 1104 + ``` 1105 + 1106 + ### How it works 1107 + 1108 + - Spindle connects to OpenBao Proxy on localhost (typically 1109 + port 8200 or 8201) 1110 + - The proxy authenticates with OpenBao using AppRole 1111 + credentials 1112 + - All spindle requests go through the proxy, which injects 1113 + authentication tokens 1114 + - Secrets are stored at 1115 + `spindle/repos/{sanitized_repo_path}/{secret_key}` 1116 + - Repository paths like `did:plc:alice/myrepo` become 1117 + `did_plc_alice_myrepo` 1118 + - The proxy handles all token renewal automatically 1119 + - Spindle no longer manages tokens or authentication 1120 + directly 1121 + 1122 + ### Troubleshooting 1123 + 1124 + **Connection refused**: Check that the OpenBao Proxy is 1125 + running and listening on the configured address. 1126 + 1127 + **403 errors**: Verify the AppRole credentials are correct 1128 + and the policy has the necessary permissions. 1129 + 1130 + **404 route errors**: The spindle KV mount probably doesn't 1131 + existโ€”run the mount creation step again. 1132 + 1133 + **Proxy authentication failures**: Check the proxy logs and 1134 + verify the role-id and secret-id files are readable and 1135 + contain valid credentials. 1136 + 1137 + **Secret not found after writing**: This can indicate policy 1138 + permission issues. Verify the policy includes both 1139 + `spindle/data/*` and `spindle/metadata/*` paths with 1140 + appropriate capabilities. 1141 + 1142 + Check proxy logs: 1143 + 1144 + ```bash 1145 + # If running as systemd service 1146 + journalctl -u openbao-proxy -f 1147 + 1148 + # If running directly, check the console output 1149 + ``` 1150 + 1151 + Test AppRole authentication manually: 1152 + 1153 + ```bash 1154 + bao write auth/approle/login \ 1155 + role_id="$(cat /tmp/openbao/role-id)" \ 1156 + secret_id="$(cat /tmp/openbao/secret-id)" 1157 + ``` 1158 + 1159 + # Migrating knots and spindles 1160 + 1161 + Sometimes, non-backwards compatible changes are made to the 1162 + knot/spindle XRPC APIs. If you host a knot or a spindle, you 1163 + will need to follow this guide to upgrade. Typically, this 1164 + only requires you to deploy the newest version. 1165 + 1166 + This document is laid out in reverse-chronological order. 1167 + Newer migration guides are listed first, and older guides 1168 + are further down the page. 1169 + 1170 + ## Upgrading from v1.8.x 1171 + 1172 + After v1.8.2, the HTTP API for knots and spindles has been 1173 + deprecated and replaced with XRPC. Repositories on outdated 1174 + knots will not be viewable from the appview. Upgrading is 1175 + straightforward however. 1176 + 1177 + For knots: 1178 + 1179 + - Upgrade to the latest tag (v1.9.0 or above) 1180 + - Head to the [knot dashboard](https://tangled.org/settings/knots) and 1181 + hit the "retry" button to verify your knot 1182 + 1183 + For spindles: 1184 + 1185 + - Upgrade to the latest tag (v1.9.0 or above) 1186 + - Head to the [spindle 1187 + dashboard](https://tangled.org/settings/spindles) and hit the 1188 + "retry" button to verify your spindle 1189 + 1190 + ## Upgrading from v1.7.x 1191 + 1192 + After v1.7.0, knot secrets have been deprecated. You no 1193 + longer need a secret from the appview to run a knot. All 1194 + authorized commands to knots are managed via [Inter-Service 1195 + Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt). 1196 + Knots will be read-only until upgraded. 1197 + 1198 + Upgrading is quite easy, in essence: 1199 + 1200 + - `KNOT_SERVER_SECRET` is no more, you can remove this 1201 + environment variable entirely 1202 + - `KNOT_SERVER_OWNER` is now required on boot, set this to 1203 + your DID. You can find your DID in the 1204 + [settings](https://tangled.org/settings) page. 1205 + - Restart your knot once you have replaced the environment 1206 + variable 1207 + - Head to the [knot dashboard](https://tangled.org/settings/knots) and 1208 + hit the "retry" button to verify your knot. This simply 1209 + writes a `sh.tangled.knot` record to your PDS. 1210 + 1211 + If you use the nix module, simply bump the flake to the 1212 + latest revision, and change your config block like so: 1213 + 1214 + ```diff 1215 + services.tangled.knot = { 1216 + enable = true; 1217 + server = { 1218 + - secretFile = /path/to/secret; 1219 + + owner = "did:plc:foo"; 1220 + }; 1221 + }; 1222 + ``` 1223 + 1224 + # Hacking on Tangled 1225 + 1226 + We highly recommend [installing 1227 + Nix](https://nixos.org/download/) (the package manager) 1228 + before working on the codebase. The Nix flake provides a lot 1229 + of helpers to get started and most importantly, builds and 1230 + dev shells are entirely deterministic. 1231 + 1232 + To set up your dev environment: 1233 + 1234 + ```bash 1235 + nix develop 1236 + ``` 1237 + 1238 + Non-Nix users can look at the `devShell` attribute in the 1239 + `flake.nix` file to determine necessary dependencies. 1240 + 1241 + ## Running the appview 1242 + 1243 + The Nix flake also exposes a few `app` attributes (run `nix 1244 + flake show` to see a full list of what the flake provides), 1245 + one of the apps runs the appview with the `air` 1246 + live-reloader: 1247 + 1248 + ```bash 1249 + TANGLED_DEV=true nix run .#watch-appview 1250 + 1251 + # TANGLED_DB_PATH might be of interest to point to 1252 + # different sqlite DBs 1253 + 1254 + # in a separate shell, you can live-reload tailwind 1255 + nix run .#watch-tailwind 1256 + ``` 1257 + 1258 + To authenticate with the appview, you will need Redis and 1259 + OAuth JWKs to be set up: 1260 + 1261 + ``` 1262 + # OAuth JWKs should already be set up by the Nix devshell: 1263 + echo $TANGLED_OAUTH_CLIENT_SECRET 1264 + z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc 1265 + 1266 + echo $TANGLED_OAUTH_CLIENT_KID 1267 + 1761667908 1268 + 1269 + # if not, you can set it up yourself: 1270 + goat key generate -t P-256 1271 + Key Type: P-256 / secp256r1 / ES256 private key 1272 + Secret Key (Multibase Syntax): save this securely (eg, add to password manager) 1273 + z42tuPDKRfM2mz2Kv953ARen2jmrPA8S9LX9tRq4RVcUMwwL 1274 + Public Key (DID Key Syntax): share or publish this (eg, in DID document) 1275 + did:key:zDnaeUBxtG6Xuv3ATJE4GaWeyXM3jyamJsZw3bSPpxx4bNXDR 1276 + 1277 + # the secret key from above 1278 + export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..." 1279 + 1280 + # Run Redis in a new shell to store OAuth sessions 1281 + redis-server 1282 + ``` 1283 + 1284 + ## Running knots and spindles 1285 + 1286 + An end-to-end knot setup requires setting up a machine with 1287 + `sshd`, `AuthorizedKeysCommand`, and a Git user, which is 1288 + quite cumbersome. So the Nix flake provides a 1289 + `nixosConfiguration` to do so. 1290 + 1291 + <details> 1292 + <summary><strong>macOS users will have to set up a Nix Builder first</strong></summary> 1293 + 1294 + In order to build Tangled's dev VM on macOS, you will 1295 + first need to set up a Linux Nix builder. The recommended 1296 + way to do so is to run a [`darwin.linux-builder` 1297 + VM](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder) 1298 + and to register it in `nix.conf` as a builder for Linux 1299 + with the same architecture as your Mac (`linux-aarch64` if 1300 + you are using Apple Silicon). 1301 + 1302 + > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside 1303 + > the Tangled repo so that it doesn't conflict with the other VM. For example, 1304 + > you can do 1305 + > 1306 + > ```shell 1307 + > cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder 1308 + > ``` 1309 + > 1310 + > to store the builder VM in a temporary dir. 1311 + > 1312 + > You should read and follow [all the other intructions][darwin builder vm] to 1313 + > avoid subtle problems. 1314 + 1315 + Alternatively, you can use any other method to set up a 1316 + Linux machine with Nix installed that you can `sudo ssh` 1317 + into (in other words, root user on your Mac has to be able 1318 + to ssh into the Linux machine without entering a password) 1319 + and that has the same architecture as your Mac. See 1320 + [remote builder 1321 + instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements) 1322 + for how to register such a builder in `nix.conf`. 1323 + 1324 + > WARNING: If you'd like to use 1325 + > [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or 1326 + > [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo 1327 + > ssh` works can be tricky. It seems to be [possible with 1328 + > Orbstack](https://github.com/orgs/orbstack/discussions/1669). 1329 + 1330 + </details> 1331 + 1332 + To begin, grab your DID from http://localhost:3000/settings. 1333 + Then, set `TANGLED_VM_KNOT_OWNER` and 1334 + `TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a 1335 + lightweight NixOS VM like so: 1336 + 1337 + ```bash 1338 + nix run --impure .#vm 1339 + 1340 + # type `poweroff` at the shell to exit the VM 1341 + ``` 1342 + 1343 + This starts a knot on port 6444, a spindle on port 6555 1344 + with `ssh` exposed on port 2222. 1345 + 1346 + Once the services are running, head to 1347 + http://localhost:3000/settings/knots and hit "Verify". It should 1348 + verify the ownership of the services instantly if everything 1349 + went smoothly. 1350 + 1351 + You can push repositories to this VM with this ssh config 1352 + block on your main machine: 1353 + 1354 + ```bash 1355 + Host nixos-shell 1356 + Hostname localhost 1357 + Port 2222 1358 + User git 1359 + IdentityFile ~/.ssh/my_tangled_key 1360 + ``` 1361 + 1362 + Set up a remote called `local-dev` on a git repo: 1363 + 1364 + ```bash 1365 + git remote add local-dev git@nixos-shell:user/repo 1366 + git push local-dev main 1367 + ``` 1368 + 1369 + The above VM should already be running a spindle on 1370 + `localhost:6555`. Head to http://localhost:3000/settings/spindles and 1371 + hit "Verify". You can then configure each repository to use 1372 + this spindle and run CI jobs. 1373 + 1374 + Of interest when debugging spindles: 1375 + 1376 + ``` 1377 + # Service logs from journald: 1378 + journalctl -xeu spindle 1379 + 1380 + # CI job logs from disk: 1381 + ls /var/log/spindle 1382 + 1383 + # Debugging spindle database: 1384 + sqlite3 /var/lib/spindle/spindle.db 1385 + 1386 + # litecli has a nicer REPL interface: 1387 + litecli /var/lib/spindle/spindle.db 1388 + ``` 1389 + 1390 + If for any reason you wish to disable either one of the 1391 + services in the VM, modify [nix/vm.nix](/nix/vm.nix) and set 1392 + `services.tangled.spindle.enable` (or 1393 + `services.tangled.knot.enable`) to `false`. 1394 + 1395 + # Contribution guide 1396 + 1397 + ## Commit guidelines 1398 + 1399 + We follow a commit style similar to the Go project. Please keep commits: 1400 + 1401 + * **atomic**: each commit should represent one logical change 1402 + * **descriptive**: the commit message should clearly describe what the 1403 + change does and why it's needed 1404 + 1405 + ### Message format 1406 + 1407 + ``` 1408 + <service/top-level directory>/<affected package/directory>: <short summary of change> 1409 + 1410 + Optional longer description can go here, if necessary. Explain what the 1411 + change does and why, especially if not obvious. Reference relevant 1412 + issues or PRs when applicable. These can be links for now since we don't 1413 + auto-link issues/PRs yet. 1414 + ``` 1415 + 1416 + Here are some examples: 1417 + 1418 + ``` 1419 + appview/state: fix token expiry check in middleware 1420 + 1421 + The previous check did not account for clock drift, leading to premature 1422 + token invalidation. 1423 + ``` 1424 + 1425 + ``` 1426 + knotserver/git/service: improve error checking in upload-pack 1427 + ``` 1428 + 1429 + 1430 + ### General notes 1431 + 1432 + - PRs get merged "as-is" (fast-forward)โ€”like applying a patch-series 1433 + using `git am`. At present, there is no squashingโ€”so please author 1434 + your commits as they would appear on `master`, following the above 1435 + guidelines. 1436 + - If there is a lot of nesting, for example "appview: 1437 + pages/templates/repo/fragments: ...", these can be truncated down to 1438 + just "appview: repo/fragments: ...". If the change affects a lot of 1439 + subdirectories, you may abbreviate to just the top-level names, e.g. 1440 + "appview: ..." or "knotserver: ...". 1441 + - Keep commits lowercased with no trailing period. 1442 + - Use the imperative mood in the summary line (e.g., "fix bug" not 1443 + "fixed bug" or "fixes bug"). 1444 + - Try to keep the summary line under 72 characters, but we aren't too 1445 + fussed about this. 1446 + - Follow the same formatting for PR titles if filled manually. 1447 + - Don't include unrelated changes in the same commit. 1448 + - Avoid noisy commit messages like "wip" or "final fix"โ€”rewrite history 1449 + before submitting if necessary. 1450 + 1451 + ## Code formatting 1452 + 1453 + We use a variety of tools to format our code, and multiplex them with 1454 + [`treefmt`](https://treefmt.com). All you need to do to format your changes 1455 + is run `nix run .#fmt` (or just `treefmt` if you're in the devshell). 1456 + 1457 + ## Proposals for bigger changes 1458 + 1459 + Small fixes like typos, minor bugs, or trivial refactors can be 1460 + submitted directly as PRs. 1461 + 1462 + For larger changesโ€”especially those introducing new features, significant 1463 + refactoring, or altering system behaviorโ€”please open a proposal first. This 1464 + helps us evaluate the scope, design, and potential impact before implementation. 1465 + 1466 + Create a new issue titled: 1467 + 1468 + ``` 1469 + proposal: <affected scope>: <summary of change> 1470 + ``` 1471 + 1472 + In the description, explain: 1473 + 1474 + - What the change is 1475 + - Why it's needed 1476 + - How you plan to implement it (roughly) 1477 + - Any open questions or tradeoffs 1478 + 1479 + We'll use the issue thread to discuss and refine the idea before moving 1480 + forward. 1481 + 1482 + ## Developer Certificate of Origin (DCO) 1483 + 1484 + We require all contributors to certify that they have the right to 1485 + submit the code they're contributing. To do this, we follow the 1486 + [Developer Certificate of Origin 1487 + (DCO)](https://developercertificate.org/). 1488 + 1489 + By signing your commits, you're stating that the contribution is your 1490 + own work, or that you have the right to submit it under the project's 1491 + license. This helps us keep things clean and legally sound. 1492 + 1493 + To sign your commit, just add the `-s` flag when committing: 1494 + 1495 + ```sh 1496 + git commit -s -m "your commit message" 1497 + ``` 1498 + 1499 + This appends a line like: 1500 + 1501 + ``` 1502 + Signed-off-by: Your Name <your.email@example.com> 1503 + ``` 1504 + 1505 + We won't merge commits if they aren't signed off. If you forget, you can 1506 + amend the last commit like this: 1507 + 1508 + ```sh 1509 + git commit --amend -s 1510 + ``` 1511 + 1512 + If you're submitting a PR with multiple commits, make sure each one is 1513 + signed. 1514 + 1515 + For [jj](https://jj-vcs.github.io/jj/latest/) users, you can run the following command 1516 + to make it sign off commits in the tangled repo: 1517 + 1518 + ```shell 1519 + # Safety check, should say "No matching config key..." 1520 + jj config list templates.commit_trailers 1521 + # The command below may need to be adjusted if the command above returned something. 1522 + jj config set --repo templates.commit_trailers "format_signed_off_by_trailer(self)" 1523 + ``` 1524 + 1525 + Refer to the [jujutsu 1526 + documentation](https://jj-vcs.github.io/jj/latest/config/#commit-trailers) 1527 + for more information.
-136
docs/contributing.md
··· 1 - # tangled contributing guide 2 - 3 - ## commit guidelines 4 - 5 - We follow a commit style similar to the Go project. Please keep commits: 6 - 7 - * **atomic**: each commit should represent one logical change 8 - * **descriptive**: the commit message should clearly describe what the 9 - change does and why it's needed 10 - 11 - ### message format 12 - 13 - ``` 14 - <service/top-level directory>/<affected package/directory>: <short summary of change> 15 - 16 - 17 - Optional longer description can go here, if necessary. Explain what the 18 - change does and why, especially if not obvious. Reference relevant 19 - issues or PRs when applicable. These can be links for now since we don't 20 - auto-link issues/PRs yet. 21 - ``` 22 - 23 - Here are some examples: 24 - 25 - ``` 26 - appview/state: fix token expiry check in middleware 27 - 28 - The previous check did not account for clock drift, leading to premature 29 - token invalidation. 30 - ``` 31 - 32 - ``` 33 - knotserver/git/service: improve error checking in upload-pack 34 - ``` 35 - 36 - 37 - ### general notes 38 - 39 - - PRs get merged "as-is" (fast-forward) -- like applying a patch-series 40 - using `git am`. At present, there is no squashing -- so please author 41 - your commits as they would appear on `master`, following the above 42 - guidelines. 43 - - If there is a lot of nesting, for example "appview: 44 - pages/templates/repo/fragments: ...", these can be truncated down to 45 - just "appview: repo/fragments: ...". If the change affects a lot of 46 - subdirectories, you may abbreviate to just the top-level names, e.g. 47 - "appview: ..." or "knotserver: ...". 48 - - Keep commits lowercased with no trailing period. 49 - - Use the imperative mood in the summary line (e.g., "fix bug" not 50 - "fixed bug" or "fixes bug"). 51 - - Try to keep the summary line under 72 characters, but we aren't too 52 - fussed about this. 53 - - Follow the same formatting for PR titles if filled manually. 54 - - Don't include unrelated changes in the same commit. 55 - - Avoid noisy commit messages like "wip" or "final fix"โ€”rewrite history 56 - before submitting if necessary. 57 - 58 - ## code formatting 59 - 60 - We use a variety of tools to format our code, and multiplex them with 61 - [`treefmt`](https://treefmt.com): all you need to do to format your changes 62 - is run `nix run .#fmt` (or just `treefmt` if you're in the devshell). 63 - 64 - ## proposals for bigger changes 65 - 66 - Small fixes like typos, minor bugs, or trivial refactors can be 67 - submitted directly as PRs. 68 - 69 - For larger changesโ€”especially those introducing new features, significant 70 - refactoring, or altering system behaviorโ€”please open a proposal first. This 71 - helps us evaluate the scope, design, and potential impact before implementation. 72 - 73 - ### proposal format 74 - 75 - Create a new issue titled: 76 - 77 - ``` 78 - proposal: <affected scope>: <summary of change> 79 - ``` 80 - 81 - In the description, explain: 82 - 83 - - What the change is 84 - - Why it's needed 85 - - How you plan to implement it (roughly) 86 - - Any open questions or tradeoffs 87 - 88 - We'll use the issue thread to discuss and refine the idea before moving 89 - forward. 90 - 91 - ## developer certificate of origin (DCO) 92 - 93 - We require all contributors to certify that they have the right to 94 - submit the code they're contributing. To do this, we follow the 95 - [Developer Certificate of Origin 96 - (DCO)](https://developercertificate.org/). 97 - 98 - By signing your commits, you're stating that the contribution is your 99 - own work, or that you have the right to submit it under the project's 100 - license. This helps us keep things clean and legally sound. 101 - 102 - To sign your commit, just add the `-s` flag when committing: 103 - 104 - ```sh 105 - git commit -s -m "your commit message" 106 - ``` 107 - 108 - This appends a line like: 109 - 110 - ``` 111 - Signed-off-by: Your Name <your.email@example.com> 112 - ``` 113 - 114 - We won't merge commits if they aren't signed off. If you forget, you can 115 - amend the last commit like this: 116 - 117 - ```sh 118 - git commit --amend -s 119 - ``` 120 - 121 - If you're submitting a PR with multiple commits, make sure each one is 122 - signed. 123 - 124 - For [jj](https://jj-vcs.github.io/jj/latest/) users, you can run the following command 125 - to make it sign off commits in the tangled repo: 126 - 127 - ```shell 128 - # Safety check, should say "No matching config key..." 129 - jj config list templates.commit_trailers 130 - # The command below may need to be adjusted if the command above returned something. 131 - jj config set --repo templates.commit_trailers "format_signed_off_by_trailer(self)" 132 - ``` 133 - 134 - Refer to the [jj 135 - documentation](https://jj-vcs.github.io/jj/latest/config/#commit-trailers) 136 - for more information.
-172
docs/hacking.md
··· 1 - # hacking on tangled 2 - 3 - We highly recommend [installing 4 - nix](https://nixos.org/download/) (the package manager) 5 - before working on the codebase. The nix flake provides a lot 6 - of helpers to get started and most importantly, builds and 7 - dev shells are entirely deterministic. 8 - 9 - To set up your dev environment: 10 - 11 - ```bash 12 - nix develop 13 - ``` 14 - 15 - Non-nix users can look at the `devShell` attribute in the 16 - `flake.nix` file to determine necessary dependencies. 17 - 18 - ## running the appview 19 - 20 - The nix flake also exposes a few `app` attributes (run `nix 21 - flake show` to see a full list of what the flake provides), 22 - one of the apps runs the appview with the `air` 23 - live-reloader: 24 - 25 - ```bash 26 - TANGLED_DEV=true nix run .#watch-appview 27 - 28 - # TANGLED_DB_PATH might be of interest to point to 29 - # different sqlite DBs 30 - 31 - # in a separate shell, you can live-reload tailwind 32 - nix run .#watch-tailwind 33 - ``` 34 - 35 - To authenticate with the appview, you will need redis and 36 - OAUTH JWKs to be setup: 37 - 38 - ``` 39 - # oauth jwks should already be setup by the nix devshell: 40 - echo $TANGLED_OAUTH_CLIENT_SECRET 41 - z42ty4RT1ovnTopY8B8ekz9NuziF2CuMkZ7rbRFpAR9jBqMc 42 - 43 - echo $TANGLED_OAUTH_CLIENT_KID 44 - 1761667908 45 - 46 - # if not, you can set it up yourself: 47 - goat key generate -t P-256 48 - Key Type: P-256 / secp256r1 / ES256 private key 49 - Secret Key (Multibase Syntax): save this securely (eg, add to password manager) 50 - z42tuPDKRfM2mz2Kv953ARen2jmrPA8S9LX9tRq4RVcUMwwL 51 - Public Key (DID Key Syntax): share or publish this (eg, in DID document) 52 - did:key:zDnaeUBxtG6Xuv3ATJE4GaWeyXM3jyamJsZw3bSPpxx4bNXDR 53 - 54 - # the secret key from above 55 - export TANGLED_OAUTH_CLIENT_SECRET="z42tuP..." 56 - 57 - # run redis in at a new shell to store oauth sessions 58 - redis-server 59 - ``` 60 - 61 - ## running knots and spindles 62 - 63 - An end-to-end knot setup requires setting up a machine with 64 - `sshd`, `AuthorizedKeysCommand`, and git user, which is 65 - quite cumbersome. So the nix flake provides a 66 - `nixosConfiguration` to do so. 67 - 68 - <details> 69 - <summary><strong>MacOS users will have to setup a Nix Builder first</strong></summary> 70 - 71 - In order to build Tangled's dev VM on macOS, you will 72 - first need to set up a Linux Nix builder. The recommended 73 - way to do so is to run a [`darwin.linux-builder` 74 - VM](https://nixos.org/manual/nixpkgs/unstable/#sec-darwin-builder) 75 - and to register it in `nix.conf` as a builder for Linux 76 - with the same architecture as your Mac (`linux-aarch64` if 77 - you are using Apple Silicon). 78 - 79 - > IMPORTANT: You must build `darwin.linux-builder` somewhere other than inside 80 - > the tangled repo so that it doesn't conflict with the other VM. For example, 81 - > you can do 82 - > 83 - > ```shell 84 - > cd $(mktemp -d buildervm.XXXXX) && nix run nixpkgs#darwin.linux-builder 85 - > ``` 86 - > 87 - > to store the builder VM in a temporary dir. 88 - > 89 - > You should read and follow [all the other intructions][darwin builder vm] to 90 - > avoid subtle problems. 91 - 92 - Alternatively, you can use any other method to set up a 93 - Linux machine with `nix` installed that you can `sudo ssh` 94 - into (in other words, root user on your Mac has to be able 95 - to ssh into the Linux machine without entering a password) 96 - and that has the same architecture as your Mac. See 97 - [remote builder 98 - instructions](https://nix.dev/manual/nix/2.28/advanced-topics/distributed-builds.html#requirements) 99 - for how to register such a builder in `nix.conf`. 100 - 101 - > WARNING: If you'd like to use 102 - > [`nixos-lima`](https://github.com/nixos-lima/nixos-lima) or 103 - > [Orbstack](https://orbstack.dev/), note that setting them up so that `sudo 104 - > ssh` works can be tricky. It seems to be [possible with 105 - > Orbstack](https://github.com/orgs/orbstack/discussions/1669). 106 - 107 - </details> 108 - 109 - To begin, grab your DID from http://localhost:3000/settings. 110 - Then, set `TANGLED_VM_KNOT_OWNER` and 111 - `TANGLED_VM_SPINDLE_OWNER` to your DID. You can now start a 112 - lightweight NixOS VM like so: 113 - 114 - ```bash 115 - nix run --impure .#vm 116 - 117 - # type `poweroff` at the shell to exit the VM 118 - ``` 119 - 120 - This starts a knot on port 6444, a spindle on port 6555 121 - with `ssh` exposed on port 2222. 122 - 123 - Once the services are running, head to 124 - http://localhost:3000/settings/knots and hit verify. It should 125 - verify the ownership of the services instantly if everything 126 - went smoothly. 127 - 128 - You can push repositories to this VM with this ssh config 129 - block on your main machine: 130 - 131 - ```bash 132 - Host nixos-shell 133 - Hostname localhost 134 - Port 2222 135 - User git 136 - IdentityFile ~/.ssh/my_tangled_key 137 - ``` 138 - 139 - Set up a remote called `local-dev` on a git repo: 140 - 141 - ```bash 142 - git remote add local-dev git@nixos-shell:user/repo 143 - git push local-dev main 144 - ``` 145 - 146 - ### running a spindle 147 - 148 - The above VM should already be running a spindle on 149 - `localhost:6555`. Head to http://localhost:3000/settings/spindles and 150 - hit verify. You can then configure each repository to use 151 - this spindle and run CI jobs. 152 - 153 - Of interest when debugging spindles: 154 - 155 - ``` 156 - # service logs from journald: 157 - journalctl -xeu spindle 158 - 159 - # CI job logs from disk: 160 - ls /var/log/spindle 161 - 162 - # debugging spindle db: 163 - sqlite3 /var/lib/spindle/spindle.db 164 - 165 - # litecli has a nicer REPL interface: 166 - litecli /var/lib/spindle/spindle.db 167 - ``` 168 - 169 - If for any reason you wish to disable either one of the 170 - services in the VM, modify [nix/vm.nix](/nix/vm.nix) and set 171 - `services.tangled.spindle.enable` (or 172 - `services.tangled.knot.enable`) to `false`.
+93
docs/highlight.theme
··· 1 + { 2 + "text-color": null, 3 + "background-color": null, 4 + "line-number-color": null, 5 + "line-number-background-color": null, 6 + "text-styles": { 7 + "Annotation": { 8 + "text-color": null, 9 + "background-color": null, 10 + "bold": false, 11 + "italic": true, 12 + "underline": false 13 + }, 14 + "ControlFlow": { 15 + "text-color": null, 16 + "background-color": null, 17 + "bold": true, 18 + "italic": false, 19 + "underline": false 20 + }, 21 + "Error": { 22 + "text-color": null, 23 + "background-color": null, 24 + "bold": true, 25 + "italic": false, 26 + "underline": false 27 + }, 28 + "Alert": { 29 + "text-color": null, 30 + "background-color": null, 31 + "bold": true, 32 + "italic": false, 33 + "underline": false 34 + }, 35 + "Preprocessor": { 36 + "text-color": null, 37 + "background-color": null, 38 + "bold": true, 39 + "italic": false, 40 + "underline": false 41 + }, 42 + "Information": { 43 + "text-color": null, 44 + "background-color": null, 45 + "bold": false, 46 + "italic": true, 47 + "underline": false 48 + }, 49 + "Warning": { 50 + "text-color": null, 51 + "background-color": null, 52 + "bold": false, 53 + "italic": true, 54 + "underline": false 55 + }, 56 + "Documentation": { 57 + "text-color": null, 58 + "background-color": null, 59 + "bold": false, 60 + "italic": true, 61 + "underline": false 62 + }, 63 + "DataType": { 64 + "text-color": "#8f4e8b", 65 + "background-color": null, 66 + "bold": false, 67 + "italic": false, 68 + "underline": false 69 + }, 70 + "Comment": { 71 + "text-color": null, 72 + "background-color": null, 73 + "bold": false, 74 + "italic": true, 75 + "underline": false 76 + }, 77 + "CommentVar": { 78 + "text-color": null, 79 + "background-color": null, 80 + "bold": false, 81 + "italic": true, 82 + "underline": false 83 + }, 84 + "Keyword": { 85 + "text-color": null, 86 + "background-color": null, 87 + "bold": true, 88 + "italic": false, 89 + "underline": false 90 + } 91 + } 92 + } 93 +
-214
docs/knot-hosting.md
··· 1 - # knot self-hosting guide 2 - 3 - So you want to run your own knot server? Great! Here are a few prerequisites: 4 - 5 - 1. A server of some kind (a VPS, a Raspberry Pi, etc.). Preferably running a Linux distribution of some kind. 6 - 2. A (sub)domain name. People generally use `knot.example.com`. 7 - 3. A valid SSL certificate for your domain. 8 - 9 - There's a couple of ways to get started: 10 - * NixOS: refer to 11 - [flake.nix](https://tangled.sh/@tangled.sh/core/blob/master/flake.nix) 12 - * Docker: Documented at 13 - [@tangled.sh/knot-docker](https://tangled.sh/@tangled.sh/knot-docker) 14 - (community maintained: support is not guaranteed!) 15 - * Manual: Documented below. 16 - 17 - ## manual setup 18 - 19 - First, clone this repository: 20 - 21 - ``` 22 - git clone https://tangled.org/@tangled.org/core 23 - ``` 24 - 25 - Then, build the `knot` CLI. This is the knot administration and operation tool. 26 - For the purpose of this guide, we're only concerned with these subcommands: 27 - 28 - * `knot server`: the main knot server process, typically run as a 29 - supervised service 30 - * `knot guard`: handles role-based access control for git over SSH 31 - (you'll never have to run this yourself) 32 - * `knot keys`: fetches SSH keys associated with your knot; we'll use 33 - this to generate the SSH `AuthorizedKeysCommand` 34 - 35 - ``` 36 - cd core 37 - export CGO_ENABLED=1 38 - go build -o knot ./cmd/knot 39 - ``` 40 - 41 - Next, move the `knot` binary to a location owned by `root` -- 42 - `/usr/local/bin/` is a good choice. Make sure the binary itself is also owned by `root`: 43 - 44 - ``` 45 - sudo mv knot /usr/local/bin/knot 46 - sudo chown root:root /usr/local/bin/knot 47 - ``` 48 - 49 - This is necessary because SSH `AuthorizedKeysCommand` requires [really 50 - specific permissions](https://stackoverflow.com/a/27638306). The 51 - `AuthorizedKeysCommand` specifies a command that is run by `sshd` to 52 - retrieve a user's public SSH keys dynamically for authentication. Let's 53 - set that up. 54 - 55 - ``` 56 - sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF 57 - Match User git 58 - AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys 59 - AuthorizedKeysCommandUser nobody 60 - EOF 61 - ``` 62 - 63 - Then, reload `sshd`: 64 - 65 - ``` 66 - sudo systemctl reload ssh 67 - ``` 68 - 69 - Next, create the `git` user. We'll use the `git` user's home directory 70 - to store repositories: 71 - 72 - ``` 73 - sudo adduser git 74 - ``` 75 - 76 - Create `/home/git/.knot.env` with the following, updating the values as 77 - necessary. The `KNOT_SERVER_OWNER` should be set to your 78 - DID, you can find your DID in the [Settings](https://tangled.sh/settings) page. 79 - 80 - ``` 81 - KNOT_REPO_SCAN_PATH=/home/git 82 - KNOT_SERVER_HOSTNAME=knot.example.com 83 - APPVIEW_ENDPOINT=https://tangled.sh 84 - KNOT_SERVER_OWNER=did:plc:foobar 85 - KNOT_SERVER_INTERNAL_LISTEN_ADDR=127.0.0.1:5444 86 - KNOT_SERVER_LISTEN_ADDR=127.0.0.1:5555 87 - ``` 88 - 89 - If you run a Linux distribution that uses systemd, you can use the provided 90 - service file to run the server. Copy 91 - [`knotserver.service`](/systemd/knotserver.service) 92 - to `/etc/systemd/system/`. Then, run: 93 - 94 - ``` 95 - systemctl enable knotserver 96 - systemctl start knotserver 97 - ``` 98 - 99 - The last step is to configure a reverse proxy like Nginx or Caddy to front your 100 - knot. Here's an example configuration for Nginx: 101 - 102 - ``` 103 - server { 104 - listen 80; 105 - listen [::]:80; 106 - server_name knot.example.com; 107 - 108 - location / { 109 - proxy_pass http://localhost:5555; 110 - proxy_set_header Host $host; 111 - proxy_set_header X-Real-IP $remote_addr; 112 - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 113 - proxy_set_header X-Forwarded-Proto $scheme; 114 - } 115 - 116 - # wss endpoint for git events 117 - location /events { 118 - proxy_set_header X-Forwarded-For $remote_addr; 119 - proxy_set_header Host $http_host; 120 - proxy_set_header Upgrade websocket; 121 - proxy_set_header Connection Upgrade; 122 - proxy_pass http://localhost:5555; 123 - } 124 - # additional config for SSL/TLS go here. 125 - } 126 - 127 - ``` 128 - 129 - Remember to use Let's Encrypt or similar to procure a certificate for your 130 - knot domain. 131 - 132 - You should now have a running knot server! You can finalize 133 - your registration by hitting the `verify` button on the 134 - [/settings/knots](https://tangled.org/settings/knots) page. This simply creates 135 - a record on your PDS to announce the existence of the knot. 136 - 137 - ### custom paths 138 - 139 - (This section applies to manual setup only. Docker users should edit the mounts 140 - in `docker-compose.yml` instead.) 141 - 142 - Right now, the database and repositories of your knot lives in `/home/git`. You 143 - can move these paths if you'd like to store them in another folder. Be careful 144 - when adjusting these paths: 145 - 146 - * Stop your knot when moving data (e.g. `systemctl stop knotserver`) to prevent 147 - any possible side effects. Remember to restart it once you're done. 148 - * Make backups before moving in case something goes wrong. 149 - * Make sure the `git` user can read and write from the new paths. 150 - 151 - #### database 152 - 153 - As an example, let's say the current database is at `/home/git/knotserver.db`, 154 - and we want to move it to `/home/git/database/knotserver.db`. 155 - 156 - Copy the current database to the new location. Make sure to copy the `.db-shm` 157 - and `.db-wal` files if they exist. 158 - 159 - ``` 160 - mkdir /home/git/database 161 - cp /home/git/knotserver.db* /home/git/database 162 - ``` 163 - 164 - In the environment (e.g. `/home/git/.knot.env`), set `KNOT_SERVER_DB_PATH` to 165 - the new file path (_not_ the directory): 166 - 167 - ``` 168 - KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db 169 - ``` 170 - 171 - #### repositories 172 - 173 - As an example, let's say the repositories are currently in `/home/git`, and we 174 - want to move them into `/home/git/repositories`. 175 - 176 - Create the new folder, then move the existing repositories (if there are any): 177 - 178 - ``` 179 - mkdir /home/git/repositories 180 - # move all DIDs into the new folder; these will vary for you! 181 - mv /home/git/did:plc:wshs7t2adsemcrrd4snkeqli /home/git/repositories 182 - ``` 183 - 184 - In the environment (e.g. `/home/git/.knot.env`), update `KNOT_REPO_SCAN_PATH` 185 - to the new directory: 186 - 187 - ``` 188 - KNOT_REPO_SCAN_PATH=/home/git/repositories 189 - ``` 190 - 191 - Similarly, update your `sshd` `AuthorizedKeysCommand` to use the updated 192 - repository path: 193 - 194 - ``` 195 - sudo tee /etc/ssh/sshd_config.d/authorized_keys_command.conf <<EOF 196 - Match User git 197 - AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys -git-dir /home/git/repositories 198 - AuthorizedKeysCommandUser nobody 199 - EOF 200 - ``` 201 - 202 - Make sure to restart your SSH server! 203 - 204 - #### MOTD (message of the day) 205 - 206 - To configure the MOTD used ("Welcome to this knot!" by default), edit the 207 - `/home/git/motd` file: 208 - 209 - ``` 210 - printf "Hi from this knot!\n" > /home/git/motd 211 - ``` 212 - 213 - Note that you should add a newline at the end if setting a non-empty message 214 - since the knot won't do this for you.
-59
docs/migrations.md
··· 1 - # Migrations 2 - 3 - This document is laid out in reverse-chronological order. 4 - Newer migration guides are listed first, and older guides 5 - are further down the page. 6 - 7 - ## Upgrading from v1.8.x 8 - 9 - After v1.8.2, the HTTP API for knot and spindles have been 10 - deprecated and replaced with XRPC. Repositories on outdated 11 - knots will not be viewable from the appview. Upgrading is 12 - straightforward however. 13 - 14 - For knots: 15 - 16 - - Upgrade to latest tag (v1.9.0 or above) 17 - - Head to the [knot dashboard](https://tangled.org/settings/knots) and 18 - hit the "retry" button to verify your knot 19 - 20 - For spindles: 21 - 22 - - Upgrade to latest tag (v1.9.0 or above) 23 - - Head to the [spindle 24 - dashboard](https://tangled.org/settings/spindles) and hit the 25 - "retry" button to verify your spindle 26 - 27 - ## Upgrading from v1.7.x 28 - 29 - After v1.7.0, knot secrets have been deprecated. You no 30 - longer need a secret from the appview to run a knot. All 31 - authorized commands to knots are managed via [Inter-Service 32 - Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt). 33 - Knots will be read-only until upgraded. 34 - 35 - Upgrading is quite easy, in essence: 36 - 37 - - `KNOT_SERVER_SECRET` is no more, you can remove this 38 - environment variable entirely 39 - - `KNOT_SERVER_OWNER` is now required on boot, set this to 40 - your DID. You can find your DID in the 41 - [settings](https://tangled.org/settings) page. 42 - - Restart your knot once you have replaced the environment 43 - variable 44 - - Head to the [knot dashboard](https://tangled.org/settings/knots) and 45 - hit the "retry" button to verify your knot. This simply 46 - writes a `sh.tangled.knot` record to your PDS. 47 - 48 - If you use the nix module, simply bump the flake to the 49 - latest revision, and change your config block like so: 50 - 51 - ```diff 52 - services.tangled.knot = { 53 - enable = true; 54 - server = { 55 - - secretFile = /path/to/secret; 56 - + owner = "did:plc:foo"; 57 - }; 58 - }; 59 - ```
+3
docs/mode.html
··· 1 + <a class="px-4 py-2 mt-8 block text-center w-full rounded-sm shadow-sm border border-gray-200 dark:border-gray-700 no-underline hover:no-underline" href="$if(single-page)$/$else$/single-page.html$endif$"> 2 + $if(single-page)$View as multi-page$else$View as single-page$endif$ 3 + </a>
+7
docs/search.html
··· 1 + <form action="https://google.com/search" role="search" aria-label="Sitewide" class="w-full"> 2 + <input type="hidden" name="q" value="+[inurl:https://docs.tangled.org]"> 3 + <label> 4 + <span style="display:none;">Search</span> 5 + <input type="text" name="q" placeholder="Search docs ..." class="w-full font-normal"> 6 + </label> 7 + </form>
-25
docs/spindle/architecture.md
··· 1 - # spindle architecture 2 - 3 - Spindle is a small CI runner service. Here's a high level overview of how it operates: 4 - 5 - * listens for [`sh.tangled.spindle.member`](/lexicons/spindle/member.json) and 6 - [`sh.tangled.repo`](/lexicons/repo.json) records on the Jetstream. 7 - * when a new repo record comes through (typically when you add a spindle to a 8 - repo from the settings), spindle then resolves the underlying knot and 9 - subscribes to repo events (see: 10 - [`sh.tangled.pipeline`](/lexicons/pipeline.json)). 11 - * the spindle engine then handles execution of the pipeline, with results and 12 - logs beamed on the spindle event stream over wss 13 - 14 - ### the engine 15 - 16 - At present, the only supported backend is Docker (and Podman, if Docker 17 - compatibility is enabled, so that `/run/docker.sock` is created). Spindle 18 - executes each step in the pipeline in a fresh container, with state persisted 19 - across steps within the `/tangled/workspace` directory. 20 - 21 - The base image for the container is constructed on the fly using 22 - [Nixery](https://nixery.dev), which is handy for caching layers for frequently 23 - used packages. 24 - 25 - The pipeline manifest is [specified here](/docs/spindle/pipeline.md).
-52
docs/spindle/hosting.md
··· 1 - # spindle self-hosting guide 2 - 3 - ## prerequisites 4 - 5 - * Go 6 - * Docker (the only supported backend currently) 7 - 8 - ## configuration 9 - 10 - Spindle is configured using environment variables. The following environment variables are available: 11 - 12 - * `SPINDLE_SERVER_LISTEN_ADDR`: The address the server listens on (default: `"0.0.0.0:6555"`). 13 - * `SPINDLE_SERVER_DB_PATH`: The path to the SQLite database file (default: `"spindle.db"`). 14 - * `SPINDLE_SERVER_HOSTNAME`: The hostname of the server (required). 15 - * `SPINDLE_SERVER_JETSTREAM_ENDPOINT`: The endpoint of the Jetstream server (default: `"wss://jetstream1.us-west.bsky.network/subscribe"`). 16 - * `SPINDLE_SERVER_DEV`: A boolean indicating whether the server is running in development mode (default: `false`). 17 - * `SPINDLE_SERVER_OWNER`: The DID of the owner (required). 18 - * `SPINDLE_PIPELINES_NIXERY`: The Nixery URL (default: `"nixery.tangled.sh"`). 19 - * `SPINDLE_PIPELINES_WORKFLOW_TIMEOUT`: The default workflow timeout (default: `"5m"`). 20 - * `SPINDLE_PIPELINES_LOG_DIR`: The directory to store workflow logs (default: `"/var/log/spindle"`). 21 - 22 - ## running spindle 23 - 24 - 1. **Set the environment variables.** For example: 25 - 26 - ```shell 27 - export SPINDLE_SERVER_HOSTNAME="your-hostname" 28 - export SPINDLE_SERVER_OWNER="your-did" 29 - ``` 30 - 31 - 2. **Build the Spindle binary.** 32 - 33 - ```shell 34 - cd core 35 - go mod download 36 - go build -o cmd/spindle/spindle cmd/spindle/main.go 37 - ``` 38 - 39 - 3. **Create the log directory.** 40 - 41 - ```shell 42 - sudo mkdir -p /var/log/spindle 43 - sudo chown $USER:$USER -R /var/log/spindle 44 - ``` 45 - 46 - 4. **Run the Spindle binary.** 47 - 48 - ```shell 49 - ./cmd/spindle/spindle 50 - ``` 51 - 52 - Spindle will now start, connect to the Jetstream server, and begin processing pipelines.
-285
docs/spindle/openbao.md
··· 1 - # spindle secrets with openbao 2 - 3 - This document covers setting up Spindle to use OpenBao for secrets 4 - management via OpenBao Proxy instead of the default SQLite backend. 5 - 6 - ## overview 7 - 8 - Spindle now uses OpenBao Proxy for secrets management. The proxy handles 9 - authentication automatically using AppRole credentials, while Spindle 10 - connects to the local proxy instead of directly to the OpenBao server. 11 - 12 - This approach provides better security, automatic token renewal, and 13 - simplified application code. 14 - 15 - ## installation 16 - 17 - Install OpenBao from nixpkgs: 18 - 19 - ```bash 20 - nix shell nixpkgs#openbao # for a local server 21 - ``` 22 - 23 - ## setup 24 - 25 - The setup process can is documented for both local development and production. 26 - 27 - ### local development 28 - 29 - Start OpenBao in dev mode: 30 - 31 - ```bash 32 - bao server -dev -dev-root-token-id="root" -dev-listen-address=127.0.0.1:8201 33 - ``` 34 - 35 - This starts OpenBao on `http://localhost:8201` with a root token. 36 - 37 - Set up environment for bao CLI: 38 - 39 - ```bash 40 - export BAO_ADDR=http://localhost:8200 41 - export BAO_TOKEN=root 42 - ``` 43 - 44 - ### production 45 - 46 - You would typically use a systemd service with a configuration file. Refer to 47 - [@tangled.org/infra](https://tangled.org/@tangled.org/infra) for how this can be 48 - achieved using Nix. 49 - 50 - Then, initialize the bao server: 51 - ```bash 52 - bao operator init -key-shares=1 -key-threshold=1 53 - ``` 54 - 55 - This will print out an unseal key and a root key. Save them somewhere (like a password manager). Then unseal the vault to begin setting it up: 56 - ```bash 57 - bao operator unseal <unseal_key> 58 - ``` 59 - 60 - All steps below remain the same across both dev and production setups. 61 - 62 - ### configure openbao server 63 - 64 - Create the spindle KV mount: 65 - 66 - ```bash 67 - bao secrets enable -path=spindle -version=2 kv 68 - ``` 69 - 70 - Set up AppRole authentication and policy: 71 - 72 - Create a policy file `spindle-policy.hcl`: 73 - 74 - ```hcl 75 - # Full access to spindle KV v2 data 76 - path "spindle/data/*" { 77 - capabilities = ["create", "read", "update", "delete"] 78 - } 79 - 80 - # Access to metadata for listing and management 81 - path "spindle/metadata/*" { 82 - capabilities = ["list", "read", "delete", "update"] 83 - } 84 - 85 - # Allow listing at root level 86 - path "spindle/" { 87 - capabilities = ["list"] 88 - } 89 - 90 - # Required for connection testing and health checks 91 - path "auth/token/lookup-self" { 92 - capabilities = ["read"] 93 - } 94 - ``` 95 - 96 - Apply the policy and create an AppRole: 97 - 98 - ```bash 99 - bao policy write spindle-policy spindle-policy.hcl 100 - bao auth enable approle 101 - bao write auth/approle/role/spindle \ 102 - token_policies="spindle-policy" \ 103 - token_ttl=1h \ 104 - token_max_ttl=4h \ 105 - bind_secret_id=true \ 106 - secret_id_ttl=0 \ 107 - secret_id_num_uses=0 108 - ``` 109 - 110 - Get the credentials: 111 - 112 - ```bash 113 - # Get role ID (static) 114 - ROLE_ID=$(bao read -field=role_id auth/approle/role/spindle/role-id) 115 - 116 - # Generate secret ID 117 - SECRET_ID=$(bao write -f -field=secret_id auth/approle/role/spindle/secret-id) 118 - 119 - echo "Role ID: $ROLE_ID" 120 - echo "Secret ID: $SECRET_ID" 121 - ``` 122 - 123 - ### create proxy configuration 124 - 125 - Create the credential files: 126 - 127 - ```bash 128 - # Create directory for OpenBao files 129 - mkdir -p /tmp/openbao 130 - 131 - # Save credentials 132 - echo "$ROLE_ID" > /tmp/openbao/role-id 133 - echo "$SECRET_ID" > /tmp/openbao/secret-id 134 - chmod 600 /tmp/openbao/role-id /tmp/openbao/secret-id 135 - ``` 136 - 137 - Create a proxy configuration file `/tmp/openbao/proxy.hcl`: 138 - 139 - ```hcl 140 - # OpenBao server connection 141 - vault { 142 - address = "http://localhost:8200" 143 - } 144 - 145 - # Auto-Auth using AppRole 146 - auto_auth { 147 - method "approle" { 148 - mount_path = "auth/approle" 149 - config = { 150 - role_id_file_path = "/tmp/openbao/role-id" 151 - secret_id_file_path = "/tmp/openbao/secret-id" 152 - } 153 - } 154 - 155 - # Optional: write token to file for debugging 156 - sink "file" { 157 - config = { 158 - path = "/tmp/openbao/token" 159 - mode = 0640 160 - } 161 - } 162 - } 163 - 164 - # Proxy listener for Spindle 165 - listener "tcp" { 166 - address = "127.0.0.1:8201" 167 - tls_disable = true 168 - } 169 - 170 - # Enable API proxy with auto-auth token 171 - api_proxy { 172 - use_auto_auth_token = true 173 - } 174 - 175 - # Enable response caching 176 - cache { 177 - use_auto_auth_token = true 178 - } 179 - 180 - # Logging 181 - log_level = "info" 182 - ``` 183 - 184 - ### start the proxy 185 - 186 - Start OpenBao Proxy: 187 - 188 - ```bash 189 - bao proxy -config=/tmp/openbao/proxy.hcl 190 - ``` 191 - 192 - The proxy will authenticate with OpenBao and start listening on 193 - `127.0.0.1:8201`. 194 - 195 - ### configure spindle 196 - 197 - Set these environment variables for Spindle: 198 - 199 - ```bash 200 - export SPINDLE_SERVER_SECRETS_PROVIDER=openbao 201 - export SPINDLE_SERVER_SECRETS_OPENBAO_PROXY_ADDR=http://127.0.0.1:8201 202 - export SPINDLE_SERVER_SECRETS_OPENBAO_MOUNT=spindle 203 - ``` 204 - 205 - Start Spindle: 206 - 207 - Spindle will now connect to the local proxy, which handles all 208 - authentication automatically. 209 - 210 - ## production setup for proxy 211 - 212 - For production, you'll want to run the proxy as a service: 213 - 214 - Place your production configuration in `/etc/openbao/proxy.hcl` with 215 - proper TLS settings for the vault connection. 216 - 217 - ## verifying setup 218 - 219 - Test the proxy directly: 220 - 221 - ```bash 222 - # Check proxy health 223 - curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/sys/health 224 - 225 - # Test token lookup through proxy 226 - curl -H "X-Vault-Request: true" http://127.0.0.1:8201/v1/auth/token/lookup-self 227 - ``` 228 - 229 - Test OpenBao operations through the server: 230 - 231 - ```bash 232 - # List all secrets 233 - bao kv list spindle/ 234 - 235 - # Add a test secret via Spindle API, then check it exists 236 - bao kv list spindle/repos/ 237 - 238 - # Get a specific secret 239 - bao kv get spindle/repos/your_repo_path/SECRET_NAME 240 - ``` 241 - 242 - ## how it works 243 - 244 - - Spindle connects to OpenBao Proxy on localhost (typically port 8200 or 8201) 245 - - The proxy authenticates with OpenBao using AppRole credentials 246 - - All Spindle requests go through the proxy, which injects authentication tokens 247 - - Secrets are stored at `spindle/repos/{sanitized_repo_path}/{secret_key}` 248 - - Repository paths like `did:plc:alice/myrepo` become `did_plc_alice_myrepo` 249 - - The proxy handles all token renewal automatically 250 - - Spindle no longer manages tokens or authentication directly 251 - 252 - ## troubleshooting 253 - 254 - **Connection refused**: Check that the OpenBao Proxy is running and 255 - listening on the configured address. 256 - 257 - **403 errors**: Verify the AppRole credentials are correct and the policy 258 - has the necessary permissions. 259 - 260 - **404 route errors**: The spindle KV mount probably doesn't exist - run 261 - the mount creation step again. 262 - 263 - **Proxy authentication failures**: Check the proxy logs and verify the 264 - role-id and secret-id files are readable and contain valid credentials. 265 - 266 - **Secret not found after writing**: This can indicate policy permission 267 - issues. Verify the policy includes both `spindle/data/*` and 268 - `spindle/metadata/*` paths with appropriate capabilities. 269 - 270 - Check proxy logs: 271 - 272 - ```bash 273 - # If running as systemd service 274 - journalctl -u openbao-proxy -f 275 - 276 - # If running directly, check the console output 277 - ``` 278 - 279 - Test AppRole authentication manually: 280 - 281 - ```bash 282 - bao write auth/approle/login \ 283 - role_id="$(cat /tmp/openbao/role-id)" \ 284 - secret_id="$(cat /tmp/openbao/secret-id)" 285 - ```
-183
docs/spindle/pipeline.md
··· 1 - # spindle pipelines 2 - 3 - Spindle workflows allow you to write CI/CD pipelines in a simple format. They're located in the `.tangled/workflows` directory at the root of your repository, and are defined using YAML. 4 - 5 - The fields are: 6 - 7 - - [Trigger](#trigger): A **required** field that defines when a workflow should be triggered. 8 - - [Engine](#engine): A **required** field that defines which engine a workflow should run on. 9 - - [Clone options](#clone-options): An **optional** field that defines how the repository should be cloned. 10 - - [Dependencies](#dependencies): An **optional** field that allows you to list dependencies you may need. 11 - - [Environment](#environment): An **optional** field that allows you to define environment variables. 12 - - [Steps](#steps): An **optional** field that allows you to define what steps should run in the workflow. 13 - 14 - ## Trigger 15 - 16 - The first thing to add to a workflow is the trigger, which defines when a workflow runs. This is defined using a `when` field, which takes in a list of conditions. Each condition has the following fields: 17 - 18 - - `event`: This is a **required** field that defines when your workflow should run. It's a list that can take one or more of the following values: 19 - - `push`: The workflow should run every time a commit is pushed to the repository. 20 - - `pull_request`: The workflow should run every time a pull request is made or updated. 21 - - `manual`: The workflow can be triggered manually. 22 - - `branch`: Defines which branches the workflow should run for. If used with the `push` event, commits to the branch(es) listed here will trigger the workflow. If used with the `pull_request` event, updates to pull requests targeting the branch(es) listed here will trigger the workflow. This field has no effect with the `manual` event. Supports glob patterns using `*` and `**` (e.g., `main`, `develop`, `release-*`). Either `branch` or `tag` (or both) must be specified for `push` events. 23 - - `tag`: Defines which tags the workflow should run for. Only used with the `push` event - when tags matching the pattern(s) listed here are pushed, the workflow will trigger. This field has no effect with `pull_request` or `manual` events. Supports glob patterns using `*` and `**` (e.g., `v*`, `v1.*`, `release-**`). Either `branch` or `tag` (or both) must be specified for `push` events. 24 - 25 - For example, if you'd like to define a workflow that runs when commits are pushed to the `main` and `develop` branches, or when pull requests that target the `main` branch are updated, or manually, you can do so with: 26 - 27 - ```yaml 28 - when: 29 - - event: ["push", "manual"] 30 - branch: ["main", "develop"] 31 - - event: ["pull_request"] 32 - branch: ["main"] 33 - ``` 34 - 35 - You can also trigger workflows on tag pushes. For instance, to run a deployment workflow when tags matching `v*` are pushed: 36 - 37 - ```yaml 38 - when: 39 - - event: ["push"] 40 - tag: ["v*"] 41 - ``` 42 - 43 - You can even combine branch and tag patterns in a single constraint (the workflow triggers if either matches): 44 - 45 - ```yaml 46 - when: 47 - - event: ["push"] 48 - branch: ["main", "release-*"] 49 - tag: ["v*", "stable"] 50 - ``` 51 - 52 - ## Engine 53 - 54 - Next is the engine on which the workflow should run, defined using the **required** `engine` field. The currently supported engines are: 55 - 56 - - `nixery`: This uses an instance of [Nixery](https://nixery.dev) to run steps, which allows you to add [dependencies](#dependencies) from [Nixpkgs](https://github.com/NixOS/nixpkgs). You can search for packages on https://search.nixos.org, and there's a pretty good chance the package(s) you're looking for will be there. 57 - 58 - Example: 59 - 60 - ```yaml 61 - engine: "nixery" 62 - ``` 63 - 64 - ## Clone options 65 - 66 - When a workflow starts, the first step is to clone the repository. You can customize this behavior using the **optional** `clone` field. It has the following fields: 67 - 68 - - `skip`: Setting this to `true` will skip cloning the repository. This can be useful if your workflow is doing something that doesn't require anything from the repository itself. This is `false` by default. 69 - - `depth`: This sets the number of commits, or the "clone depth", to fetch from the repository. For example, if you set this to 2, the last 2 commits will be fetched. By default, the depth is set to 1, meaning only the most recent commit will be fetched, which is the commit that triggered the workflow. 70 - - `submodules`: If you use [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) in your repository, setting this field to `true` will recursively fetch all submodules. This is `false` by default. 71 - 72 - The default settings are: 73 - 74 - ```yaml 75 - clone: 76 - skip: false 77 - depth: 1 78 - submodules: false 79 - ``` 80 - 81 - ## Dependencies 82 - 83 - Usually when you're running a workflow, you'll need additional dependencies. The `dependencies` field lets you define which dependencies to get, and from where. It's a key-value map, with the key being the registry to fetch dependencies from, and the value being the list of dependencies to fetch. 84 - 85 - Say you want to fetch Node.js and Go from `nixpkgs`, and a package called `my_pkg` you've made from your own registry at your repository at `https://tangled.sh/@example.com/my_pkg`. You can define those dependencies like so: 86 - 87 - ```yaml 88 - dependencies: 89 - # nixpkgs 90 - nixpkgs: 91 - - nodejs 92 - - go 93 - # custom registry 94 - git+https://tangled.org/@example.com/my_pkg: 95 - - my_pkg 96 - ``` 97 - 98 - Now these dependencies are available to use in your workflow! 99 - 100 - ## Environment 101 - 102 - The `environment` field allows you define environment variables that will be available throughout the entire workflow. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.** 103 - 104 - Example: 105 - 106 - ```yaml 107 - environment: 108 - GOOS: "linux" 109 - GOARCH: "arm64" 110 - NODE_ENV: "production" 111 - MY_ENV_VAR: "MY_ENV_VALUE" 112 - ``` 113 - 114 - ## Steps 115 - 116 - The `steps` field allows you to define what steps should run in the workflow. It's a list of step objects, each with the following fields: 117 - 118 - - `name`: This field allows you to give your step a name. This name is visible in your workflow runs, and is used to describe what the step is doing. 119 - - `command`: This field allows you to define a command to run in that step. The step is run in a Bash shell, and the logs from the command will be visible in the pipelines page on the Tangled website. The [dependencies](#dependencies) you added will be available to use here. 120 - - `environment`: Similar to the global [environment](#environment) config, this **optional** field is a key-value map that allows you to set environment variables for the step. **Do not put secrets here, these environment variables are visible to anyone viewing the repository. You can add secrets for pipelines in your repository's settings.** 121 - 122 - Example: 123 - 124 - ```yaml 125 - steps: 126 - - name: "Build backend" 127 - command: "go build" 128 - environment: 129 - GOOS: "darwin" 130 - GOARCH: "arm64" 131 - - name: "Build frontend" 132 - command: "npm run build" 133 - environment: 134 - NODE_ENV: "production" 135 - ``` 136 - 137 - ## Complete workflow 138 - 139 - ```yaml 140 - # .tangled/workflows/build.yml 141 - 142 - when: 143 - - event: ["push", "manual"] 144 - branch: ["main", "develop"] 145 - - event: ["pull_request"] 146 - branch: ["main"] 147 - 148 - engine: "nixery" 149 - 150 - # using the default values 151 - clone: 152 - skip: false 153 - depth: 1 154 - submodules: false 155 - 156 - dependencies: 157 - # nixpkgs 158 - nixpkgs: 159 - - nodejs 160 - - go 161 - # custom registry 162 - git+https://tangled.org/@example.com/my_pkg: 163 - - my_pkg 164 - 165 - environment: 166 - GOOS: "linux" 167 - GOARCH: "arm64" 168 - NODE_ENV: "production" 169 - MY_ENV_VAR: "MY_ENV_VALUE" 170 - 171 - steps: 172 - - name: "Build backend" 173 - command: "go build" 174 - environment: 175 - GOOS: "darwin" 176 - GOARCH: "arm64" 177 - - name: "Build frontend" 178 - command: "npm run build" 179 - environment: 180 - NODE_ENV: "production" 181 - ``` 182 - 183 - If you want another example of a workflow, you can look at the one [Tangled uses to build the project](https://tangled.sh/@tangled.sh/core/blob/master/.tangled/workflows/build.yml).
+101
docs/styles.css
··· 1 + svg { 2 + width: 16px; 3 + height: 16px; 4 + } 5 + 6 + :root { 7 + --syntax-alert: #d20f39; 8 + --syntax-annotation: #fe640b; 9 + --syntax-attribute: #df8e1d; 10 + --syntax-basen: #40a02b; 11 + --syntax-builtin: #1e66f5; 12 + --syntax-controlflow: #8839ef; 13 + --syntax-char: #04a5e5; 14 + --syntax-constant: #fe640b; 15 + --syntax-comment: #9ca0b0; 16 + --syntax-commentvar: #7c7f93; 17 + --syntax-documentation: #9ca0b0; 18 + --syntax-datatype: #df8e1d; 19 + --syntax-decval: #40a02b; 20 + --syntax-error: #d20f39; 21 + --syntax-extension: #4c4f69; 22 + --syntax-float: #40a02b; 23 + --syntax-function: #1e66f5; 24 + --syntax-import: #40a02b; 25 + --syntax-information: #04a5e5; 26 + --syntax-keyword: #8839ef; 27 + --syntax-operator: #179299; 28 + --syntax-other: #8839ef; 29 + --syntax-preprocessor: #ea76cb; 30 + --syntax-specialchar: #04a5e5; 31 + --syntax-specialstring: #ea76cb; 32 + --syntax-string: #40a02b; 33 + --syntax-variable: #8839ef; 34 + --syntax-verbatimstring: #40a02b; 35 + --syntax-warning: #df8e1d; 36 + } 37 + 38 + @media (prefers-color-scheme: dark) { 39 + :root { 40 + --syntax-alert: #f38ba8; 41 + --syntax-annotation: #fab387; 42 + --syntax-attribute: #f9e2af; 43 + --syntax-basen: #a6e3a1; 44 + --syntax-builtin: #89b4fa; 45 + --syntax-controlflow: #cba6f7; 46 + --syntax-char: #89dceb; 47 + --syntax-constant: #fab387; 48 + --syntax-comment: #6c7086; 49 + --syntax-commentvar: #585b70; 50 + --syntax-documentation: #6c7086; 51 + --syntax-datatype: #f9e2af; 52 + --syntax-decval: #a6e3a1; 53 + --syntax-error: #f38ba8; 54 + --syntax-extension: #cdd6f4; 55 + --syntax-float: #a6e3a1; 56 + --syntax-function: #89b4fa; 57 + --syntax-import: #a6e3a1; 58 + --syntax-information: #89dceb; 59 + --syntax-keyword: #cba6f7; 60 + --syntax-operator: #94e2d5; 61 + --syntax-other: #cba6f7; 62 + --syntax-preprocessor: #f5c2e7; 63 + --syntax-specialchar: #89dceb; 64 + --syntax-specialstring: #f5c2e7; 65 + --syntax-string: #a6e3a1; 66 + --syntax-variable: #cba6f7; 67 + --syntax-verbatimstring: #a6e3a1; 68 + --syntax-warning: #f9e2af; 69 + } 70 + } 71 + 72 + /* pandoc syntax highlighting classes */ 73 + code span.al { color: var(--syntax-alert); font-weight: bold; } /* alert */ 74 + code span.an { color: var(--syntax-annotation); font-weight: bold; font-style: italic; } /* annotation */ 75 + code span.at { color: var(--syntax-attribute); } /* attribute */ 76 + code span.bn { color: var(--syntax-basen); } /* basen */ 77 + code span.bu { color: var(--syntax-builtin); } /* builtin */ 78 + code span.cf { color: var(--syntax-controlflow); font-weight: bold; } /* controlflow */ 79 + code span.ch { color: var(--syntax-char); } /* char */ 80 + code span.cn { color: var(--syntax-constant); } /* constant */ 81 + code span.co { color: var(--syntax-comment); font-style: italic; } /* comment */ 82 + code span.cv { color: var(--syntax-commentvar); font-weight: bold; font-style: italic; } /* commentvar */ 83 + code span.do { color: var(--syntax-documentation); font-style: italic; } /* documentation */ 84 + code span.dt { color: var(--syntax-datatype); } /* datatype */ 85 + code span.dv { color: var(--syntax-decval); } /* decval */ 86 + code span.er { color: var(--syntax-error); font-weight: bold; } /* error */ 87 + code span.ex { color: var(--syntax-extension); } /* extension */ 88 + code span.fl { color: var(--syntax-float); } /* float */ 89 + code span.fu { color: var(--syntax-function); } /* function */ 90 + code span.im { color: var(--syntax-import); font-weight: bold; } /* import */ 91 + code span.in { color: var(--syntax-information); font-weight: bold; font-style: italic; } /* information */ 92 + code span.kw { color: var(--syntax-keyword); font-weight: bold; } /* keyword */ 93 + code span.op { color: var(--syntax-operator); } /* operator */ 94 + code span.ot { color: var(--syntax-other); } /* other */ 95 + code span.pp { color: var(--syntax-preprocessor); } /* preprocessor */ 96 + code span.sc { color: var(--syntax-specialchar); } /* specialchar */ 97 + code span.ss { color: var(--syntax-specialstring); } /* specialstring */ 98 + code span.st { color: var(--syntax-string); } /* string */ 99 + code span.va { color: var(--syntax-variable); } /* variable */ 100 + code span.vs { color: var(--syntax-verbatimstring); } /* verbatimstring */ 101 + code span.wa { color: var(--syntax-warning); font-weight: bold; font-style: italic; } /* warning */
+156
docs/template.html
··· 1 + <!DOCTYPE html> 2 + <html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <meta name="generator" content="pandoc" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" /> 7 + $for(author-meta)$ 8 + <meta name="author" content="$author-meta$" /> 9 + $endfor$ 10 + 11 + $if(date-meta)$ 12 + <meta name="dcterms.date" content="$date-meta$" /> 13 + $endif$ 14 + 15 + $if(keywords)$ 16 + <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$" /> 17 + $endif$ 18 + 19 + $if(description-meta)$ 20 + <meta name="description" content="$description-meta$" /> 21 + $endif$ 22 + 23 + <title>$pagetitle$</title> 24 + 25 + <style> 26 + $styles.css()$ 27 + </style> 28 + 29 + $for(css)$ 30 + <link rel="stylesheet" href="$css$" /> 31 + $endfor$ 32 + 33 + $for(header-includes)$ 34 + $header-includes$ 35 + $endfor$ 36 + 37 + <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 38 + 39 + </head> 40 + <body class="bg-white dark:bg-gray-900 flex flex-col min-h-svh"> 41 + $for(include-before)$ 42 + $include-before$ 43 + $endfor$ 44 + 45 + $if(toc)$ 46 + <!-- mobile TOC trigger --> 47 + <div class="md:hidden px-6 py-4 border-b border-gray-200 dark:border-gray-700"> 48 + <button 49 + type="button" 50 + popovertarget="mobile-toc-popover" 51 + popovertargetaction="toggle" 52 + class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white" 53 + > 54 + ${ menu.svg() } 55 + $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 56 + </button> 57 + </div> 58 + 59 + <div 60 + id="mobile-toc-popover" 61 + popover 62 + class="mobile-toc-popover 63 + bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 64 + h-full overflow-y-auto shadow-sm 65 + px-6 py-4 fixed inset-x-0 top-0 w-fit max-w-4/5 m-0" 66 + > 67 + <div class="flex flex-col min-h-full"> 68 + <div class="flex-1 space-y-4"> 69 + <button 70 + type="button" 71 + popovertarget="mobile-toc-popover" 72 + popovertargetaction="toggle" 73 + class="w-full flex gap-2 items-center text-sm font-semibold dark:text-white mb-4"> 74 + ${ x.svg() } 75 + $if(toc-title)$$toc-title$$else$Table of Contents$endif$ 76 + </button> 77 + ${ search.html() } 78 + ${ table-of-contents:toc.html() } 79 + </div> 80 + ${ single-page:mode.html() } 81 + </div> 82 + </div> 83 + 84 + <!-- desktop sidebar toc --> 85 + <nav 86 + id="$idprefix$TOC" 87 + role="doc-toc" 88 + class="hidden md:flex md:flex-col gap-4 fixed left-0 top-0 w-80 h-screen 89 + bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 90 + p-4 z-50 overflow-y-auto"> 91 + ${ search.html() } 92 + <div class="flex-1"> 93 + $if(toc-title)$ 94 + <h2 id="$idprefix$toc-title" class="text-lg font-semibold mb-4 text-gray-900">$toc-title$</h2> 95 + $endif$ 96 + ${ table-of-contents:toc.html() } 97 + </div> 98 + ${ single-page:mode.html() } 99 + </nav> 100 + $endif$ 101 + 102 + <div class="$if(toc)$md:ml-80$endif$ flex-1 flex flex-col"> 103 + <main class="max-w-4xl w-full mx-auto p-6 flex-1"> 104 + $if(top)$ 105 + $-- only print title block if this is NOT the top page 106 + $else$ 107 + $if(title)$ 108 + <header id="title-block-header" class="mb-8 pb-8 border-b border-gray-200 dark:border-gray-700"> 109 + <h1 class="text-4xl font-bold mb-2 text-black dark:text-white">$title$</h1> 110 + $if(subtitle)$ 111 + <p class="text-xl text-gray-500 dark:text-gray-400 mb-2">$subtitle$</p> 112 + $endif$ 113 + $for(author)$ 114 + <p class="text-sm text-gray-500 dark:text-gray-400">$author$</p> 115 + $endfor$ 116 + $if(date)$ 117 + <p class="text-sm text-gray-500 dark:text-gray-400">Updated on $date$</p> 118 + $endif$ 119 + $endif$ 120 + </header> 121 + $endif$ 122 + 123 + $if(abstract)$ 124 + <article class="prose dark:prose-invert max-w-none"> 125 + $abstract$ 126 + </article> 127 + $endif$ 128 + 129 + <article class="prose dark:prose-invert max-w-none"> 130 + $body$ 131 + </article> 132 + </main> 133 + <nav id="sitenav" class="border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800"> 134 + <div class="max-w-4xl mx-auto px-8 py-4"> 135 + <div class="flex justify-between gap-4"> 136 + <span class="flex-1"> 137 + $if(previous.url)$ 138 + <span class="text-xs text-gray-500 dark:text-gray-400 uppercase block mb-1">Previous</span> 139 + <a href="$previous.url$" accesskey="p" rel="previous">$previous.title$</a> 140 + $endif$ 141 + </span> 142 + <span class="flex-1 text-right"> 143 + $if(next.url)$ 144 + <span class="text-xs text-gray-500 dark:text-gray-400 uppercase block mb-1">Next</span> 145 + <a href="$next.url$" accesskey="n" rel="next">$next.title$</a> 146 + $endif$ 147 + </span> 148 + </div> 149 + </div> 150 + </nav> 151 + </div> 152 + $for(include-after)$ 153 + $include-after$ 154 + $endfor$ 155 + </body> 156 + </html>
+4
docs/toc.html
··· 1 + <div class="[&_ul]:space-y-6 [&_ul]:pl-0 [&_ul]:font-bold [&_ul_ul]:pl-4 [&_ul_ul]:font-normal [&_ul_ul]:space-y-2 [&_li]:space-y-2"> 2 + $table-of-contents$ 3 + </div> 4 +
+9 -9
flake.lock
··· 35 35 "systems": "systems" 36 36 }, 37 37 "locked": { 38 - "lastModified": 1694529238, 39 - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 38 + "lastModified": 1731533236, 39 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 40 40 "owner": "numtide", 41 41 "repo": "flake-utils", 42 - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 42 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 43 43 "type": "github" 44 44 }, 45 45 "original": { ··· 56 56 ] 57 57 }, 58 58 "locked": { 59 - "lastModified": 1754078208, 60 - "narHash": "sha256-YVoIFDCDpYuU3riaDEJ3xiGdPOtsx4sR5eTzHTytPV8=", 59 + "lastModified": 1763982521, 60 + "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=", 61 61 "owner": "nix-community", 62 62 "repo": "gomod2nix", 63 - "rev": "7f963246a71626c7fc70b431a315c4388a0c95cf", 63 + "rev": "02e63a239d6eabd595db56852535992c898eba72", 64 64 "type": "github" 65 65 }, 66 66 "original": { ··· 150 150 }, 151 151 "nixpkgs": { 152 152 "locked": { 153 - "lastModified": 1751984180, 154 - "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", 153 + "lastModified": 1766070988, 154 + "narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=", 155 155 "owner": "nixos", 156 156 "repo": "nixpkgs", 157 - "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", 157 + "rev": "c6245e83d836d0433170a16eb185cefe0572f8b8", 158 158 "type": "github" 159 159 }, 160 160 "original": {
+21 -5
flake.nix
··· 76 76 }; 77 77 buildGoApplication = 78 78 (self.callPackage "${gomod2nix}/builder" { 79 - gomod2nix = gomod2nix.legacyPackages.${pkgs.system}.gomod2nix; 79 + gomod2nix = gomod2nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}.gomod2nix; 80 80 }).buildGoApplication; 81 81 modules = ./nix/gomod2nix.toml; 82 82 sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix { 83 - inherit (pkgs) gcc; 84 83 inherit sqlite-lib-src; 85 84 }; 86 85 lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;}; ··· 89 88 inherit htmx-src htmx-ws-src lucide-src inter-fonts-src ibm-plex-mono-src actor-typeahead-src; 90 89 }; 91 90 appview = self.callPackage ./nix/pkgs/appview.nix {}; 91 + docs = self.callPackage ./nix/pkgs/docs.nix { 92 + inherit inter-fonts-src ibm-plex-mono-src lucide-src; 93 + }; 92 94 spindle = self.callPackage ./nix/pkgs/spindle.nix {}; 93 95 knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {}; 94 96 knot = self.callPackage ./nix/pkgs/knot.nix {}; 97 + dolly = self.callPackage ./nix/pkgs/dolly.nix {}; 95 98 }); 96 99 in { 97 100 overlays.default = final: prev: { 98 - inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview; 101 + inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview docs dolly; 99 102 }; 100 103 101 104 packages = forAllSystems (system: let ··· 104 107 staticPackages = mkPackageSet pkgs.pkgsStatic; 105 108 crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic; 106 109 in { 107 - inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib; 110 + inherit 111 + (packages) 112 + appview 113 + appview-static-files 114 + lexgen 115 + goat 116 + spindle 117 + knot 118 + knot-unwrapped 119 + sqlite-lib 120 + docs 121 + dolly 122 + ; 108 123 109 124 pkgsStatic-appview = staticPackages.appview; 110 125 pkgsStatic-knot = staticPackages.knot; 111 126 pkgsStatic-knot-unwrapped = staticPackages.knot-unwrapped; 112 127 pkgsStatic-spindle = staticPackages.spindle; 113 128 pkgsStatic-sqlite-lib = staticPackages.sqlite-lib; 129 + pkgsStatic-dolly = staticPackages.dolly; 114 130 115 131 pkgsCross-gnu64-pkgsStatic-appview = crossPackages.appview; 116 132 pkgsCross-gnu64-pkgsStatic-knot = crossPackages.knot; 117 133 pkgsCross-gnu64-pkgsStatic-knot-unwrapped = crossPackages.knot-unwrapped; 118 134 pkgsCross-gnu64-pkgsStatic-spindle = crossPackages.spindle; 135 + pkgsCross-gnu64-pkgsStatic-dolly = crossPackages.dolly; 119 136 120 137 treefmt-wrapper = pkgs.treefmt.withConfig { 121 138 settings.formatter = { ··· 156 173 nativeBuildInputs = [ 157 174 pkgs.go 158 175 pkgs.air 159 - pkgs.tilt 160 176 pkgs.gopls 161 177 pkgs.httpie 162 178 pkgs.litecli
+3 -2
go.mod
··· 1 1 module tangled.org/core 2 2 3 - go 1.24.4 3 + go 1.25.0 4 4 5 5 require ( 6 6 github.com/Blank-Xu/sql-adapter v1.1.1 ··· 45 45 github.com/urfave/cli/v3 v3.3.3 46 46 github.com/whyrusleeping/cbor-gen v0.3.1 47 47 github.com/yuin/goldmark v1.7.13 48 + github.com/yuin/goldmark-emoji v1.0.6 48 49 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc 49 50 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab 50 51 golang.org/x/crypto v0.40.0 51 52 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b 52 53 golang.org/x/image v0.31.0 53 54 golang.org/x/net v0.42.0 54 - golang.org/x/sync v0.17.0 55 55 golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da 56 56 gopkg.in/yaml.v3 v3.0.1 57 57 ) ··· 203 203 go.uber.org/atomic v1.11.0 // indirect 204 204 go.uber.org/multierr v1.11.0 // indirect 205 205 go.uber.org/zap v1.27.0 // indirect 206 + golang.org/x/sync v0.17.0 // indirect 206 207 golang.org/x/sys v0.34.0 // indirect 207 208 golang.org/x/text v0.29.0 // indirect 208 209 golang.org/x/time v0.12.0 // indirect
+2
go.sum
··· 505 505 github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 506 506 github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= 507 507 github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 508 + github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= 509 + github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= 508 510 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ= 509 511 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= 510 512 gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
+4 -4
hook/hook.go
··· 48 48 }, 49 49 Commands: []*cli.Command{ 50 50 { 51 - Name: "post-recieve", 52 - Usage: "sends a post-recieve hook to the knot (waits for stdin)", 53 - Action: postRecieve, 51 + Name: "post-receive", 52 + Usage: "sends a post-receive hook to the knot (waits for stdin)", 53 + Action: postReceive, 54 54 }, 55 55 }, 56 56 } 57 57 } 58 58 59 - func postRecieve(ctx context.Context, cmd *cli.Command) error { 59 + func postReceive(ctx context.Context, cmd *cli.Command) error { 60 60 gitDir := cmd.String("git-dir") 61 61 userDid := cmd.String("user-did") 62 62 userHandle := cmd.String("user-handle")
+1 -1
hook/setup.go
··· 138 138 option_var="GIT_PUSH_OPTION_$i" 139 139 push_options+=(-push-option "${!option_var}") 140 140 done 141 - %s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-recieve 141 + %s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive 142 142 `, executablePath, config.internalApi) 143 143 144 144 return os.WriteFile(hookPath, []byte(hookContent), 0755)
+88
ico/ico.go
··· 1 + package ico 2 + 3 + import ( 4 + "bytes" 5 + "encoding/binary" 6 + "fmt" 7 + "image" 8 + "image/png" 9 + ) 10 + 11 + type IconDir struct { 12 + Reserved uint16 // must be 0 13 + Type uint16 // 1 for ICO, 2 for CUR 14 + Count uint16 // number of images 15 + } 16 + 17 + type IconDirEntry struct { 18 + Width uint8 // 0 means 256 19 + Height uint8 // 0 means 256 20 + ColorCount uint8 21 + Reserved uint8 // must be 0 22 + ColorPlanes uint16 // 0 or 1 23 + BitsPerPixel uint16 24 + SizeInBytes uint32 25 + Offset uint32 26 + } 27 + 28 + func ImageToIco(img image.Image) ([]byte, error) { 29 + // encode image as png 30 + var pngBuf bytes.Buffer 31 + if err := png.Encode(&pngBuf, img); err != nil { 32 + return nil, fmt.Errorf("failed to encode PNG: %w", err) 33 + } 34 + pngData := pngBuf.Bytes() 35 + 36 + // get image dimensions 37 + bounds := img.Bounds() 38 + width := bounds.Dx() 39 + height := bounds.Dy() 40 + 41 + // prepare output buffer 42 + var icoBuf bytes.Buffer 43 + 44 + iconDir := IconDir{ 45 + Reserved: 0, 46 + Type: 1, // ICO format 47 + Count: 1, // One image 48 + } 49 + 50 + w := uint8(width) 51 + h := uint8(height) 52 + 53 + // width/height of 256 should be stored as 0 54 + if width == 256 { 55 + w = 0 56 + } 57 + if height == 256 { 58 + h = 0 59 + } 60 + 61 + iconDirEntry := IconDirEntry{ 62 + Width: w, 63 + Height: h, 64 + ColorCount: 0, // 0 for PNG (32-bit) 65 + Reserved: 0, 66 + ColorPlanes: 1, 67 + BitsPerPixel: 32, // PNG with alpha 68 + SizeInBytes: uint32(len(pngData)), 69 + Offset: 6 + 16, // Size of ICONDIR + ICONDIRENTRY 70 + } 71 + 72 + // write IconDir 73 + if err := binary.Write(&icoBuf, binary.LittleEndian, iconDir); err != nil { 74 + return nil, fmt.Errorf("failed to write ICONDIR: %w", err) 75 + } 76 + 77 + // write IconDirEntry 78 + if err := binary.Write(&icoBuf, binary.LittleEndian, iconDirEntry); err != nil { 79 + return nil, fmt.Errorf("failed to write ICONDIRENTRY: %w", err) 80 + } 81 + 82 + // write PNG data directly 83 + if _, err := icoBuf.Write(pngData); err != nil { 84 + return nil, fmt.Errorf("failed to write PNG data: %w", err) 85 + } 86 + 87 + return icoBuf.Bytes(), nil 88 + }
+2 -1
input.css
··· 162 162 } 163 163 164 164 .prose a.mention { 165 - @apply no-underline hover:underline; 165 + @apply no-underline hover:underline font-bold; 166 166 } 167 167 168 168 .prose li { ··· 255 255 @apply py-1 text-gray-900 dark:text-gray-100; 256 256 } 257 257 } 258 + 258 259 } 259 260 260 261 /* Background */
+81
knotserver/db/db.go
··· 1 + package db 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "log/slog" 7 + "strings" 8 + 9 + _ "github.com/mattn/go-sqlite3" 10 + "tangled.org/core/log" 11 + ) 12 + 13 + type DB struct { 14 + db *sql.DB 15 + logger *slog.Logger 16 + } 17 + 18 + func Setup(ctx context.Context, dbPath string) (*DB, error) { 19 + // https://github.com/mattn/go-sqlite3#connection-string 20 + opts := []string{ 21 + "_foreign_keys=1", 22 + "_journal_mode=WAL", 23 + "_synchronous=NORMAL", 24 + "_auto_vacuum=incremental", 25 + } 26 + 27 + logger := log.FromContext(ctx) 28 + logger = log.SubLogger(logger, "db") 29 + 30 + db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 31 + if err != nil { 32 + return nil, err 33 + } 34 + 35 + conn, err := db.Conn(ctx) 36 + if err != nil { 37 + return nil, err 38 + } 39 + defer conn.Close() 40 + 41 + _, err = conn.ExecContext(ctx, ` 42 + create table if not exists known_dids ( 43 + did text primary key 44 + ); 45 + 46 + create table if not exists public_keys ( 47 + id integer primary key autoincrement, 48 + did text not null, 49 + key text not null, 50 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 51 + unique(did, key), 52 + foreign key (did) references known_dids(did) on delete cascade 53 + ); 54 + 55 + create table if not exists _jetstream ( 56 + id integer primary key autoincrement, 57 + last_time_us integer not null 58 + ); 59 + 60 + create table if not exists events ( 61 + rkey text not null, 62 + nsid text not null, 63 + event text not null, -- json 64 + created integer not null default (strftime('%s', 'now')), 65 + primary key (rkey, nsid) 66 + ); 67 + 68 + create table if not exists migrations ( 69 + id integer primary key autoincrement, 70 + name text unique 71 + ); 72 + `) 73 + if err != nil { 74 + return nil, err 75 + } 76 + 77 + return &DB{ 78 + db: db, 79 + logger: logger, 80 + }, nil 81 + }
-64
knotserver/db/init.go
··· 1 - package db 2 - 3 - import ( 4 - "database/sql" 5 - "strings" 6 - 7 - _ "github.com/mattn/go-sqlite3" 8 - ) 9 - 10 - type DB struct { 11 - db *sql.DB 12 - } 13 - 14 - func Setup(dbPath string) (*DB, error) { 15 - // https://github.com/mattn/go-sqlite3#connection-string 16 - opts := []string{ 17 - "_foreign_keys=1", 18 - "_journal_mode=WAL", 19 - "_synchronous=NORMAL", 20 - "_auto_vacuum=incremental", 21 - } 22 - 23 - db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&")) 24 - if err != nil { 25 - return nil, err 26 - } 27 - 28 - // NOTE: If any other migration is added here, you MUST 29 - // copy the pattern in appview: use a single sql.Conn 30 - // for every migration. 31 - 32 - _, err = db.Exec(` 33 - create table if not exists known_dids ( 34 - did text primary key 35 - ); 36 - 37 - create table if not exists public_keys ( 38 - id integer primary key autoincrement, 39 - did text not null, 40 - key text not null, 41 - created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 42 - unique(did, key), 43 - foreign key (did) references known_dids(did) on delete cascade 44 - ); 45 - 46 - create table if not exists _jetstream ( 47 - id integer primary key autoincrement, 48 - last_time_us integer not null 49 - ); 50 - 51 - create table if not exists events ( 52 - rkey text not null, 53 - nsid text not null, 54 - event text not null, -- json 55 - created integer not null default (strftime('%s', 'now')), 56 - primary key (rkey, nsid) 57 - ); 58 - `) 59 - if err != nil { 60 - return nil, err 61 - } 62 - 63 - return &DB{db: db}, nil 64 - }
+13 -1
knotserver/git/service/service.go
··· 95 95 return c.RunService(cmd) 96 96 } 97 97 98 + func (c *ServiceCommand) UploadArchive() error { 99 + cmd := exec.Command("git", []string{ 100 + "upload-archive", 101 + ".", 102 + }...) 103 + 104 + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 105 + cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol)) 106 + cmd.Dir = c.Dir 107 + 108 + return c.RunService(cmd) 109 + } 110 + 98 111 func (c *ServiceCommand) UploadPack() error { 99 112 cmd := exec.Command("git", []string{ 100 - "-c", "uploadpack.allowFilter=true", 101 113 "upload-pack", 102 114 "--stateless-rpc", 103 115 ".",
+47
knotserver/git.go
··· 56 56 } 57 57 } 58 58 59 + func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) { 60 + did := chi.URLParam(r, "did") 61 + name := chi.URLParam(r, "name") 62 + repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name)) 63 + if err != nil { 64 + gitError(w, err.Error(), http.StatusInternalServerError) 65 + h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err) 66 + return 67 + } 68 + 69 + const expectedContentType = "application/x-git-upload-archive-request" 70 + contentType := r.Header.Get("Content-Type") 71 + if contentType != expectedContentType { 72 + gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType) 73 + } 74 + 75 + var bodyReader io.ReadCloser = r.Body 76 + if r.Header.Get("Content-Encoding") == "gzip" { 77 + gzipReader, err := gzip.NewReader(r.Body) 78 + if err != nil { 79 + gitError(w, err.Error(), http.StatusInternalServerError) 80 + h.l.Error("git: failed to create gzip reader", "handler", "UploadArchive", "error", err) 81 + return 82 + } 83 + defer gzipReader.Close() 84 + bodyReader = gzipReader 85 + } 86 + 87 + w.Header().Set("Content-Type", "application/x-git-upload-archive-result") 88 + 89 + h.l.Info("git: executing git-upload-archive", "handler", "UploadArchive", "repo", repo) 90 + 91 + cmd := service.ServiceCommand{ 92 + GitProtocol: r.Header.Get("Git-Protocol"), 93 + Dir: repo, 94 + Stdout: w, 95 + Stdin: bodyReader, 96 + } 97 + 98 + w.WriteHeader(http.StatusOK) 99 + 100 + if err := cmd.UploadArchive(); err != nil { 101 + h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err) 102 + return 103 + } 104 + } 105 + 59 106 func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) { 60 107 did := chi.URLParam(r, "did") 61 108 name := chi.URLParam(r, "name")
+1
knotserver/router.go
··· 82 82 r.Route("/{name}", func(r chi.Router) { 83 83 // routes for git operations 84 84 r.Get("/info/refs", h.InfoRefs) 85 + r.Post("/git-upload-archive", h.UploadArchive) 85 86 r.Post("/git-upload-pack", h.UploadPack) 86 87 r.Post("/git-receive-pack", h.ReceivePack) 87 88 })
+1 -1
knotserver/server.go
··· 64 64 logger.Info("running in dev mode, signature verification is disabled") 65 65 } 66 66 67 - db, err := db.Setup(c.Server.DBPath) 67 + db, err := db.Setup(ctx, c.Server.DBPath) 68 68 if err != nil { 69 69 return fmt.Errorf("failed to load db: %w", err) 70 70 }
+3 -7
lexicons/actor/profile.json
··· 8 8 "key": "literal:self", 9 9 "record": { 10 10 "type": "object", 11 - "required": ["bluesky"], 11 + "required": [ 12 + "bluesky" 13 + ], 12 14 "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 - }, 19 15 "description": { 20 16 "type": "string", 21 17 "description": "Free-form profile description text.",
+10 -2
lexicons/pulls/pull.json
··· 12 12 "required": [ 13 13 "target", 14 14 "title", 15 - "patch", 15 + "patchBlob", 16 16 "createdAt" 17 17 ], 18 18 "properties": { ··· 27 27 "type": "string" 28 28 }, 29 29 "patch": { 30 - "type": "string" 30 + "type": "string", 31 + "description": "(deprecated) use patchBlob instead" 32 + }, 33 + "patchBlob": { 34 + "type": "blob", 35 + "accept": [ 36 + "text/x-patch" 37 + ], 38 + "description": "patch content" 31 39 }, 32 40 "source": { 33 41 "type": "ref",
+3
nix/gomod2nix.toml
··· 530 530 [mod."github.com/yuin/goldmark"] 531 531 version = "v1.7.13" 532 532 hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE=" 533 + [mod."github.com/yuin/goldmark-emoji"] 534 + version = "v1.0.6" 535 + hash = "sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY=" 533 536 [mod."github.com/yuin/goldmark-highlighting/v2"] 534 537 version = "v2.0.0-20230729083705-37449abec8cc" 535 538 hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
+6 -1
nix/pkgs/appview-static-files.nix
··· 8 8 actor-typeahead-src, 9 9 sqlite-lib, 10 10 tailwindcss, 11 + dolly, 11 12 src, 12 13 }: 13 14 runCommandLocal "appview-static-files" { ··· 17 18 (allow file-read* (subpath "/System/Library/OpenSSL")) 18 19 ''; 19 20 } '' 20 - mkdir -p $out/{fonts,icons} && cd $out 21 + mkdir -p $out/{fonts,icons,logos} && cd $out 21 22 cp -f ${htmx-src} htmx.min.js 22 23 cp -f ${htmx-ws-src} htmx-ext-ws.min.js 23 24 cp -rf ${lucide-src}/*.svg icons/ ··· 26 27 cp -f ${inter-fonts-src}/InterVariable*.ttf fonts/ 27 28 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ 28 29 cp -f ${actor-typeahead-src}/actor-typeahead.js . 30 + 31 + ${dolly}/bin/dolly -output logos/dolly.png -size 180x180 32 + ${dolly}/bin/dolly -output logos/dolly.ico -size 48x48 33 + ${dolly}/bin/dolly -output logos/dolly.svg 29 34 # tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work 30 35 # for whatever reason (produces broken css), so we are doing this instead 31 36 cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css
+53
nix/pkgs/docs.nix
··· 1 + { 2 + pandoc, 3 + tailwindcss, 4 + runCommandLocal, 5 + inter-fonts-src, 6 + ibm-plex-mono-src, 7 + lucide-src, 8 + src, 9 + }: 10 + runCommandLocal "docs" {} '' 11 + mkdir -p working 12 + 13 + # copy templates, themes, styles, filters to working directory 14 + cp ${src}/docs/*.html working/ 15 + cp ${src}/docs/*.theme working/ 16 + cp ${src}/docs/*.css working/ 17 + 18 + # icons 19 + cp -rf ${lucide-src}/*.svg working/ 20 + 21 + # content - chunked 22 + ${pandoc}/bin/pandoc ${src}/docs/DOCS.md \ 23 + -o $out/ \ 24 + -t chunkedhtml \ 25 + --variable toc \ 26 + --variable-json single-page=false \ 27 + --toc-depth=2 \ 28 + --css=stylesheet.css \ 29 + --chunk-template="%i.html" \ 30 + --highlight-style=working/highlight.theme \ 31 + --template=working/template.html 32 + 33 + # content - single page 34 + ${pandoc}/bin/pandoc ${src}/docs/DOCS.md \ 35 + -o $out/single-page.html \ 36 + --toc \ 37 + --variable toc \ 38 + --variable single-page \ 39 + --toc-depth=2 \ 40 + --css=stylesheet.css \ 41 + --highlight-style=working/highlight.theme \ 42 + --template=working/template.html 43 + 44 + # fonts 45 + mkdir -p $out/static/fonts 46 + cp -f ${inter-fonts-src}/web/InterVariable*.woff2 $out/static/fonts/ 47 + cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 $out/static/fonts/ 48 + cp -f ${inter-fonts-src}/InterVariable*.ttf $out/static/fonts/ 49 + cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 $out/static/fonts/ 50 + 51 + # styles 52 + cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/stylesheet.css 53 + ''
+21
nix/pkgs/dolly.nix
··· 1 + { 2 + buildGoApplication, 3 + 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 + }
+7 -5
nix/pkgs/sqlite-lib.nix
··· 1 1 { 2 - gcc, 3 2 stdenv, 4 3 sqlite-lib-src, 5 4 }: 6 5 stdenv.mkDerivation { 7 6 name = "sqlite-lib"; 8 7 src = sqlite-lib-src; 9 - nativeBuildInputs = [gcc]; 8 + 10 9 buildPhase = '' 11 - gcc -c sqlite3.c 12 - ar rcs libsqlite3.a sqlite3.o 13 - ranlib libsqlite3.a 10 + $CC -c sqlite3.c 11 + $AR rcs libsqlite3.a sqlite3.o 12 + $RANLIB libsqlite3.a 13 + ''; 14 + 15 + installPhase = '' 14 16 mkdir -p $out/include $out/lib 15 17 cp *.h $out/include 16 18 cp libsqlite3.a $out/lib
+1 -1
nix/vm.nix
··· 8 8 var = builtins.getEnv name; 9 9 in 10 10 if var == "" 11 - then throw "\$${name} must be defined, see docs/hacking.md for more details" 11 + then throw "\$${name} must be defined, see https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled for more details" 12 12 else var; 13 13 envVarOr = name: default: let 14 14 var = builtins.getEnv name;
+3 -3
readme.md
··· 10 10 11 11 ## docs 12 12 13 - * [knot hosting guide](/docs/knot-hosting.md) 14 - * [contributing guide](/docs/contributing.md) **please read before opening a PR!** 15 - * [hacking on tangled](/docs/hacking.md) 13 + - [knot hosting guide](https://docs.tangled.org/knot-self-hosting-guide.html#knot-self-hosting-guide) 14 + - [contributing guide](https://docs.tangled.org/contribution-guide.html#contribution-guide) **please read before opening a PR!** 15 + - [hacking on tangled](https://docs.tangled.org/hacking-on-tangled.html#hacking-on-tangled) 16 16 17 17 ## security 18 18
+31
sets/gen.go
··· 1 + package sets 2 + 3 + import ( 4 + "math/rand" 5 + "reflect" 6 + "testing/quick" 7 + ) 8 + 9 + func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value { 10 + s := New[T]() 11 + 12 + var zero T 13 + itemType := reflect.TypeOf(zero) 14 + 15 + for { 16 + if s.Len() >= size { 17 + break 18 + } 19 + 20 + item, ok := quick.Value(itemType, rand) 21 + if !ok { 22 + continue 23 + } 24 + 25 + if val, ok := item.Interface().(T); ok { 26 + s.Insert(val) 27 + } 28 + } 29 + 30 + return reflect.ValueOf(s) 31 + }
+35
sets/readme.txt
··· 1 + sets 2 + ---- 3 + set datastructure for go with generics and iterators. the 4 + api is supposed to mimic rust's std::collections::HashSet api. 5 + 6 + s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4})) 7 + s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6})) 8 + 9 + union := sets.Collect(s1.Union(s2)) 10 + intersect := sets.Collect(s1.Intersection(s2)) 11 + diff := sets.Collect(s1.Difference(s2)) 12 + symdiff := sets.Collect(s1.SymmetricDifference(s2)) 13 + 14 + s1.Len() // 4 15 + s1.Contains(1) // true 16 + s1.IsEmpty() // false 17 + s1.IsSubset(s2) // true 18 + s1.IsSuperset(s2) // false 19 + s1.IsDisjoint(s2) // false 20 + 21 + if exists := s1.Insert(1); exists { 22 + // already existed in set 23 + } 24 + 25 + if existed := s1.Remove(1); existed { 26 + // existed in set, now removed 27 + } 28 + 29 + 30 + testing 31 + ------- 32 + includes property-based tests using the wonderful 33 + testing/quick module! 34 + 35 + go test -v
+174
sets/set.go
··· 1 + package sets 2 + 3 + import ( 4 + "iter" 5 + "maps" 6 + ) 7 + 8 + type Set[T comparable] struct { 9 + data map[T]struct{} 10 + } 11 + 12 + func New[T comparable]() Set[T] { 13 + return Set[T]{ 14 + data: make(map[T]struct{}), 15 + } 16 + } 17 + 18 + func (s *Set[T]) Insert(item T) bool { 19 + _, exists := s.data[item] 20 + s.data[item] = struct{}{} 21 + return !exists 22 + } 23 + 24 + func Singleton[T comparable](item T) Set[T] { 25 + n := New[T]() 26 + _ = n.Insert(item) 27 + return n 28 + } 29 + 30 + func (s *Set[T]) Remove(item T) bool { 31 + _, exists := s.data[item] 32 + if exists { 33 + delete(s.data, item) 34 + } 35 + return exists 36 + } 37 + 38 + func (s Set[T]) Contains(item T) bool { 39 + _, exists := s.data[item] 40 + return exists 41 + } 42 + 43 + func (s Set[T]) Len() int { 44 + return len(s.data) 45 + } 46 + 47 + func (s Set[T]) IsEmpty() bool { 48 + return len(s.data) == 0 49 + } 50 + 51 + func (s *Set[T]) Clear() { 52 + s.data = make(map[T]struct{}) 53 + } 54 + 55 + func (s Set[T]) All() iter.Seq[T] { 56 + return func(yield func(T) bool) { 57 + for item := range s.data { 58 + if !yield(item) { 59 + return 60 + } 61 + } 62 + } 63 + } 64 + 65 + func (s Set[T]) Clone() Set[T] { 66 + return Set[T]{ 67 + data: maps.Clone(s.data), 68 + } 69 + } 70 + 71 + func (s Set[T]) Union(other Set[T]) iter.Seq[T] { 72 + if s.Len() >= other.Len() { 73 + return chain(s.All(), other.Difference(s)) 74 + } else { 75 + return chain(other.All(), s.Difference(other)) 76 + } 77 + } 78 + 79 + func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] { 80 + return func(yield func(T) bool) { 81 + for _, seq := range seqs { 82 + for item := range seq { 83 + if !yield(item) { 84 + return 85 + } 86 + } 87 + } 88 + } 89 + } 90 + 91 + func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] { 92 + return func(yield func(T) bool) { 93 + for item := range s.data { 94 + if other.Contains(item) { 95 + if !yield(item) { 96 + return 97 + } 98 + } 99 + } 100 + } 101 + } 102 + 103 + func (s Set[T]) Difference(other Set[T]) iter.Seq[T] { 104 + return func(yield func(T) bool) { 105 + for item := range s.data { 106 + if !other.Contains(item) { 107 + if !yield(item) { 108 + return 109 + } 110 + } 111 + } 112 + } 113 + } 114 + 115 + func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] { 116 + return func(yield func(T) bool) { 117 + for item := range s.data { 118 + if !other.Contains(item) { 119 + if !yield(item) { 120 + return 121 + } 122 + } 123 + } 124 + for item := range other.data { 125 + if !s.Contains(item) { 126 + if !yield(item) { 127 + return 128 + } 129 + } 130 + } 131 + } 132 + } 133 + 134 + func (s Set[T]) IsSubset(other Set[T]) bool { 135 + for item := range s.data { 136 + if !other.Contains(item) { 137 + return false 138 + } 139 + } 140 + return true 141 + } 142 + 143 + func (s Set[T]) IsSuperset(other Set[T]) bool { 144 + return other.IsSubset(s) 145 + } 146 + 147 + func (s Set[T]) IsDisjoint(other Set[T]) bool { 148 + for item := range s.data { 149 + if other.Contains(item) { 150 + return false 151 + } 152 + } 153 + return true 154 + } 155 + 156 + func (s Set[T]) Equal(other Set[T]) bool { 157 + if s.Len() != other.Len() { 158 + return false 159 + } 160 + for item := range s.data { 161 + if !other.Contains(item) { 162 + return false 163 + } 164 + } 165 + return true 166 + } 167 + 168 + func Collect[T comparable](seq iter.Seq[T]) Set[T] { 169 + result := New[T]() 170 + for item := range seq { 171 + result.Insert(item) 172 + } 173 + return result 174 + }
+411
sets/set_test.go
··· 1 + package sets 2 + 3 + import ( 4 + "slices" 5 + "testing" 6 + "testing/quick" 7 + ) 8 + 9 + func TestNew(t *testing.T) { 10 + s := New[int]() 11 + if s.Len() != 0 { 12 + t.Errorf("New set should be empty, got length %d", s.Len()) 13 + } 14 + if !s.IsEmpty() { 15 + t.Error("New set should be empty") 16 + } 17 + } 18 + 19 + func TestFromSlice(t *testing.T) { 20 + s := Collect(slices.Values([]int{1, 2, 3, 2, 1})) 21 + if s.Len() != 3 { 22 + t.Errorf("Expected length 3, got %d", s.Len()) 23 + } 24 + if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) { 25 + t.Error("Set should contain all unique elements from slice") 26 + } 27 + } 28 + 29 + func TestInsert(t *testing.T) { 30 + s := New[string]() 31 + 32 + if !s.Insert("hello") { 33 + t.Error("First insert should return true") 34 + } 35 + if s.Insert("hello") { 36 + t.Error("Duplicate insert should return false") 37 + } 38 + if s.Len() != 1 { 39 + t.Errorf("Expected length 1, got %d", s.Len()) 40 + } 41 + } 42 + 43 + func TestRemove(t *testing.T) { 44 + s := Collect(slices.Values([]int{1, 2, 3})) 45 + 46 + if !s.Remove(2) { 47 + t.Error("Remove existing element should return true") 48 + } 49 + if s.Remove(2) { 50 + t.Error("Remove non-existing element should return false") 51 + } 52 + if s.Contains(2) { 53 + t.Error("Element should be removed") 54 + } 55 + if s.Len() != 2 { 56 + t.Errorf("Expected length 2, got %d", s.Len()) 57 + } 58 + } 59 + 60 + func TestContains(t *testing.T) { 61 + s := Collect(slices.Values([]int{1, 2, 3})) 62 + 63 + if !s.Contains(1) { 64 + t.Error("Should contain 1") 65 + } 66 + if s.Contains(4) { 67 + t.Error("Should not contain 4") 68 + } 69 + } 70 + 71 + func TestClear(t *testing.T) { 72 + s := Collect(slices.Values([]int{1, 2, 3})) 73 + s.Clear() 74 + 75 + if !s.IsEmpty() { 76 + t.Error("Set should be empty after clear") 77 + } 78 + if s.Len() != 0 { 79 + t.Errorf("Expected length 0, got %d", s.Len()) 80 + } 81 + } 82 + 83 + func TestIterator(t *testing.T) { 84 + s := Collect(slices.Values([]int{1, 2, 3})) 85 + var items []int 86 + 87 + for item := range s.All() { 88 + items = append(items, item) 89 + } 90 + 91 + slices.Sort(items) 92 + expected := []int{1, 2, 3} 93 + if !slices.Equal(items, expected) { 94 + t.Errorf("Expected %v, got %v", expected, items) 95 + } 96 + } 97 + 98 + func TestClone(t *testing.T) { 99 + s1 := Collect(slices.Values([]int{1, 2, 3})) 100 + s2 := s1.Clone() 101 + 102 + if !s1.Equal(s2) { 103 + t.Error("Cloned set should be equal to original") 104 + } 105 + 106 + s2.Insert(4) 107 + if s1.Contains(4) { 108 + t.Error("Modifying clone should not affect original") 109 + } 110 + } 111 + 112 + func TestUnion(t *testing.T) { 113 + s1 := Collect(slices.Values([]int{1, 2})) 114 + s2 := Collect(slices.Values([]int{2, 3})) 115 + 116 + result := Collect(s1.Union(s2)) 117 + expected := Collect(slices.Values([]int{1, 2, 3})) 118 + 119 + if !result.Equal(expected) { 120 + t.Errorf("Expected %v, got %v", expected, result) 121 + } 122 + } 123 + 124 + func TestIntersection(t *testing.T) { 125 + s1 := Collect(slices.Values([]int{1, 2, 3})) 126 + s2 := Collect(slices.Values([]int{2, 3, 4})) 127 + 128 + expected := Collect(slices.Values([]int{2, 3})) 129 + result := Collect(s1.Intersection(s2)) 130 + 131 + if !result.Equal(expected) { 132 + t.Errorf("Expected %v, got %v", expected, result) 133 + } 134 + } 135 + 136 + func TestDifference(t *testing.T) { 137 + s1 := Collect(slices.Values([]int{1, 2, 3})) 138 + s2 := Collect(slices.Values([]int{2, 3, 4})) 139 + 140 + expected := Collect(slices.Values([]int{1})) 141 + result := Collect(s1.Difference(s2)) 142 + 143 + if !result.Equal(expected) { 144 + t.Errorf("Expected %v, got %v", expected, result) 145 + } 146 + } 147 + 148 + func TestSymmetricDifference(t *testing.T) { 149 + s1 := Collect(slices.Values([]int{1, 2, 3})) 150 + s2 := Collect(slices.Values([]int{2, 3, 4})) 151 + 152 + expected := Collect(slices.Values([]int{1, 4})) 153 + result := Collect(s1.SymmetricDifference(s2)) 154 + 155 + if !result.Equal(expected) { 156 + t.Errorf("Expected %v, got %v", expected, result) 157 + } 158 + } 159 + 160 + func TestSymmetricDifferenceCommutativeProperty(t *testing.T) { 161 + s1 := Collect(slices.Values([]int{1, 2, 3})) 162 + s2 := Collect(slices.Values([]int{2, 3, 4})) 163 + 164 + result1 := Collect(s1.SymmetricDifference(s2)) 165 + result2 := Collect(s2.SymmetricDifference(s1)) 166 + 167 + if !result1.Equal(result2) { 168 + t.Errorf("Expected %v, got %v", result1, result2) 169 + } 170 + } 171 + 172 + func TestIsSubset(t *testing.T) { 173 + s1 := Collect(slices.Values([]int{1, 2})) 174 + s2 := Collect(slices.Values([]int{1, 2, 3})) 175 + 176 + if !s1.IsSubset(s2) { 177 + t.Error("s1 should be subset of s2") 178 + } 179 + if s2.IsSubset(s1) { 180 + t.Error("s2 should not be subset of s1") 181 + } 182 + } 183 + 184 + func TestIsSuperset(t *testing.T) { 185 + s1 := Collect(slices.Values([]int{1, 2, 3})) 186 + s2 := Collect(slices.Values([]int{1, 2})) 187 + 188 + if !s1.IsSuperset(s2) { 189 + t.Error("s1 should be superset of s2") 190 + } 191 + if s2.IsSuperset(s1) { 192 + t.Error("s2 should not be superset of s1") 193 + } 194 + } 195 + 196 + func TestIsDisjoint(t *testing.T) { 197 + s1 := Collect(slices.Values([]int{1, 2})) 198 + s2 := Collect(slices.Values([]int{3, 4})) 199 + s3 := Collect(slices.Values([]int{2, 3})) 200 + 201 + if !s1.IsDisjoint(s2) { 202 + t.Error("s1 and s2 should be disjoint") 203 + } 204 + if s1.IsDisjoint(s3) { 205 + t.Error("s1 and s3 should not be disjoint") 206 + } 207 + } 208 + 209 + func TestEqual(t *testing.T) { 210 + s1 := Collect(slices.Values([]int{1, 2, 3})) 211 + s2 := Collect(slices.Values([]int{3, 2, 1})) 212 + s3 := Collect(slices.Values([]int{1, 2})) 213 + 214 + if !s1.Equal(s2) { 215 + t.Error("s1 and s2 should be equal") 216 + } 217 + if s1.Equal(s3) { 218 + t.Error("s1 and s3 should not be equal") 219 + } 220 + } 221 + 222 + func TestCollect(t *testing.T) { 223 + s1 := Collect(slices.Values([]int{1, 2})) 224 + s2 := Collect(slices.Values([]int{2, 3})) 225 + 226 + unionSet := Collect(s1.Union(s2)) 227 + if unionSet.Len() != 3 { 228 + t.Errorf("Expected union set length 3, got %d", unionSet.Len()) 229 + } 230 + if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) { 231 + t.Error("Union set should contain 1, 2, and 3") 232 + } 233 + 234 + diffSet := Collect(s1.Difference(s2)) 235 + if diffSet.Len() != 1 { 236 + t.Errorf("Expected difference set length 1, got %d", diffSet.Len()) 237 + } 238 + if !diffSet.Contains(1) { 239 + t.Error("Difference set should contain 1") 240 + } 241 + } 242 + 243 + func TestPropertySingleonLen(t *testing.T) { 244 + f := func(item int) bool { 245 + single := Singleton(item) 246 + return single.Len() == 1 247 + } 248 + 249 + if err := quick.Check(f, nil); err != nil { 250 + t.Error(err) 251 + } 252 + } 253 + 254 + func TestPropertyInsertIdempotent(t *testing.T) { 255 + f := func(s Set[int], item int) bool { 256 + clone := s.Clone() 257 + 258 + clone.Insert(item) 259 + firstLen := clone.Len() 260 + 261 + clone.Insert(item) 262 + secondLen := clone.Len() 263 + 264 + return firstLen == secondLen 265 + } 266 + 267 + if err := quick.Check(f, nil); err != nil { 268 + t.Error(err) 269 + } 270 + } 271 + 272 + func TestPropertyUnionCommutative(t *testing.T) { 273 + f := func(s1 Set[int], s2 Set[int]) bool { 274 + union1 := Collect(s1.Union(s2)) 275 + union2 := Collect(s2.Union(s1)) 276 + return union1.Equal(union2) 277 + } 278 + 279 + if err := quick.Check(f, nil); err != nil { 280 + t.Error(err) 281 + } 282 + } 283 + 284 + func TestPropertyIntersectionCommutative(t *testing.T) { 285 + f := func(s1 Set[int], s2 Set[int]) bool { 286 + inter1 := Collect(s1.Intersection(s2)) 287 + inter2 := Collect(s2.Intersection(s1)) 288 + return inter1.Equal(inter2) 289 + } 290 + 291 + if err := quick.Check(f, nil); err != nil { 292 + t.Error(err) 293 + } 294 + } 295 + 296 + func TestPropertyCloneEquals(t *testing.T) { 297 + f := func(s Set[int]) bool { 298 + clone := s.Clone() 299 + return s.Equal(clone) 300 + } 301 + 302 + if err := quick.Check(f, nil); err != nil { 303 + t.Error(err) 304 + } 305 + } 306 + 307 + func TestPropertyIntersectionIsSubset(t *testing.T) { 308 + f := func(s1 Set[int], s2 Set[int]) bool { 309 + inter := Collect(s1.Intersection(s2)) 310 + return inter.IsSubset(s1) && inter.IsSubset(s2) 311 + } 312 + 313 + if err := quick.Check(f, nil); err != nil { 314 + t.Error(err) 315 + } 316 + } 317 + 318 + func TestPropertyUnionIsSuperset(t *testing.T) { 319 + f := func(s1 Set[int], s2 Set[int]) bool { 320 + union := Collect(s1.Union(s2)) 321 + return union.IsSuperset(s1) && union.IsSuperset(s2) 322 + } 323 + 324 + if err := quick.Check(f, nil); err != nil { 325 + t.Error(err) 326 + } 327 + } 328 + 329 + func TestPropertyDifferenceDisjoint(t *testing.T) { 330 + f := func(s1 Set[int], s2 Set[int]) bool { 331 + diff := Collect(s1.Difference(s2)) 332 + return diff.IsDisjoint(s2) 333 + } 334 + 335 + if err := quick.Check(f, nil); err != nil { 336 + t.Error(err) 337 + } 338 + } 339 + 340 + func TestPropertySymmetricDifferenceCommutative(t *testing.T) { 341 + f := func(s1 Set[int], s2 Set[int]) bool { 342 + symDiff1 := Collect(s1.SymmetricDifference(s2)) 343 + symDiff2 := Collect(s2.SymmetricDifference(s1)) 344 + return symDiff1.Equal(symDiff2) 345 + } 346 + 347 + if err := quick.Check(f, nil); err != nil { 348 + t.Error(err) 349 + } 350 + } 351 + 352 + func TestPropertyRemoveWorks(t *testing.T) { 353 + f := func(s Set[int], item int) bool { 354 + clone := s.Clone() 355 + clone.Insert(item) 356 + clone.Remove(item) 357 + return !clone.Contains(item) 358 + } 359 + 360 + if err := quick.Check(f, nil); err != nil { 361 + t.Error(err) 362 + } 363 + } 364 + 365 + func TestPropertyClearEmpty(t *testing.T) { 366 + f := func(s Set[int]) bool { 367 + s.Clear() 368 + return s.IsEmpty() && s.Len() == 0 369 + } 370 + 371 + if err := quick.Check(f, nil); err != nil { 372 + t.Error(err) 373 + } 374 + } 375 + 376 + func TestPropertyIsSubsetReflexive(t *testing.T) { 377 + f := func(s Set[int]) bool { 378 + return s.IsSubset(s) 379 + } 380 + 381 + if err := quick.Check(f, nil); err != nil { 382 + t.Error(err) 383 + } 384 + } 385 + 386 + func TestPropertyDeMorganUnion(t *testing.T) { 387 + f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool { 388 + // create a universe that contains both sets 389 + u := universe.Clone() 390 + for item := range s1.All() { 391 + u.Insert(item) 392 + } 393 + for item := range s2.All() { 394 + u.Insert(item) 395 + } 396 + 397 + // (A u B)' = A' n B' 398 + union := Collect(s1.Union(s2)) 399 + complementUnion := Collect(u.Difference(union)) 400 + 401 + complementS1 := Collect(u.Difference(s1)) 402 + complementS2 := Collect(u.Difference(s2)) 403 + intersectionComplements := Collect(complementS1.Intersection(complementS2)) 404 + 405 + return complementUnion.Equal(intersectionComplements) 406 + } 407 + 408 + if err := quick.Check(f, nil); err != nil { 409 + t.Error(err) 410 + } 411 + }
+1
spindle/db/repos.go
··· 16 16 if err != nil { 17 17 return nil, err 18 18 } 19 + defer rows.Close() 19 20 20 21 var knots []string 21 22 for rows.Next() {
+22 -21
spindle/engine/engine.go
··· 3 3 import ( 4 4 "context" 5 5 "errors" 6 - "fmt" 7 6 "log/slog" 7 + "sync" 8 8 9 9 securejoin "github.com/cyphar/filepath-securejoin" 10 - "golang.org/x/sync/errgroup" 11 10 "tangled.org/core/notifier" 12 11 "tangled.org/core/spindle/config" 13 12 "tangled.org/core/spindle/db" ··· 31 30 } 32 31 } 33 32 34 - eg, ctx := errgroup.WithContext(ctx) 33 + var wg sync.WaitGroup 35 34 for eng, wfs := range pipeline.Workflows { 36 35 workflowTimeout := eng.WorkflowTimeout() 37 36 l.Info("using workflow timeout", "timeout", workflowTimeout) 38 37 39 38 for _, w := range wfs { 40 - eg.Go(func() error { 39 + wg.Add(1) 40 + go func() { 41 + defer wg.Done() 42 + 41 43 wid := models.WorkflowId{ 42 44 PipelineId: pipelineId, 43 45 Name: w.Name, ··· 45 47 46 48 err := db.StatusRunning(wid, n) 47 49 if err != nil { 48 - return err 50 + l.Error("failed to set workflow status to running", "wid", wid, "err", err) 51 + return 49 52 } 50 53 51 54 err = eng.SetupWorkflow(ctx, wid, &w) ··· 61 64 62 65 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 63 66 if dbErr != nil { 64 - return dbErr 67 + l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 65 68 } 66 - return err 69 + return 67 70 } 68 71 defer eng.DestroyWorkflow(ctx, wid) 69 72 70 - wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid) 73 + secretValues := make([]string, len(allSecrets)) 74 + for i, s := range allSecrets { 75 + secretValues[i] = s.Value 76 + } 77 + wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid, secretValues) 71 78 if err != nil { 72 79 l.Warn("failed to setup step logger; logs will not be persisted", "error", err) 73 80 wfLogger = nil ··· 99 106 if errors.Is(err, ErrTimedOut) { 100 107 dbErr := db.StatusTimeout(wid, n) 101 108 if dbErr != nil { 102 - return dbErr 109 + l.Error("failed to set workflow status to timeout", "wid", wid, "err", dbErr) 103 110 } 104 111 } else { 105 112 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 106 113 if dbErr != nil { 107 - return dbErr 114 + l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 108 115 } 109 116 } 110 - 111 - return fmt.Errorf("starting steps image: %w", err) 117 + return 112 118 } 113 119 } 114 120 115 121 err = db.StatusSuccess(wid, n) 116 122 if err != nil { 117 - return err 123 + l.Error("failed to set workflow status to success", "wid", wid, "err", err) 118 124 } 119 - 120 - return nil 121 - }) 125 + }() 122 126 } 123 127 } 124 128 125 - if err := eg.Wait(); err != nil { 126 - l.Error("failed to run one or more workflows", "err", err) 127 - } else { 128 - l.Info("successfully ran full pipeline") 129 - } 129 + wg.Wait() 130 + l.Info("all workflows completed") 130 131 }
+5 -3
spindle/engines/nixery/engine.go
··· 294 294 workflowEnvs.AddEnv(s.Key, s.Value) 295 295 } 296 296 297 - step := w.Steps[idx].(Step) 297 + step := w.Steps[idx] 298 298 299 299 select { 300 300 case <-ctx.Done(): ··· 303 303 } 304 304 305 305 envs := append(EnvVars(nil), workflowEnvs...) 306 - for k, v := range step.environment { 307 - envs.AddEnv(k, v) 306 + if nixStep, ok := step.(Step); ok { 307 + for k, v := range nixStep.environment { 308 + envs.AddEnv(k, v) 309 + } 308 310 } 309 311 envs.AddEnv("HOME", homeDir) 310 312
+6 -1
spindle/models/logger.go
··· 12 12 type WorkflowLogger struct { 13 13 file *os.File 14 14 encoder *json.Encoder 15 + mask *SecretMask 15 16 } 16 17 17 - func NewWorkflowLogger(baseDir string, wid WorkflowId) (*WorkflowLogger, error) { 18 + func NewWorkflowLogger(baseDir string, wid WorkflowId, secretValues []string) (*WorkflowLogger, error) { 18 19 path := LogFilePath(baseDir, wid) 19 20 20 21 file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) ··· 25 26 return &WorkflowLogger{ 26 27 file: file, 27 28 encoder: json.NewEncoder(file), 29 + mask: NewSecretMask(secretValues), 28 30 }, nil 29 31 } 30 32 ··· 62 64 63 65 func (w *dataWriter) Write(p []byte) (int, error) { 64 66 line := strings.TrimRight(string(p), "\r\n") 67 + if w.logger.mask != nil { 68 + line = w.logger.mask.Mask(line) 69 + } 65 70 entry := NewDataLogLine(w.idx, line, w.stream) 66 71 if err := w.logger.encoder.Encode(entry); err != nil { 67 72 return 0, err
+51
spindle/models/secret_mask.go
··· 1 + package models 2 + 3 + import ( 4 + "encoding/base64" 5 + "strings" 6 + ) 7 + 8 + // SecretMask replaces secret values in strings with "***". 9 + type SecretMask struct { 10 + replacer *strings.Replacer 11 + } 12 + 13 + // NewSecretMask creates a mask for the given secret values. 14 + // Also registers base64-encoded variants of each secret. 15 + func NewSecretMask(values []string) *SecretMask { 16 + var pairs []string 17 + 18 + for _, value := range values { 19 + if value == "" { 20 + continue 21 + } 22 + 23 + pairs = append(pairs, value, "***") 24 + 25 + b64 := base64.StdEncoding.EncodeToString([]byte(value)) 26 + if b64 != value { 27 + pairs = append(pairs, b64, "***") 28 + } 29 + 30 + b64NoPad := strings.TrimRight(b64, "=") 31 + if b64NoPad != b64 && b64NoPad != value { 32 + pairs = append(pairs, b64NoPad, "***") 33 + } 34 + } 35 + 36 + if len(pairs) == 0 { 37 + return nil 38 + } 39 + 40 + return &SecretMask{ 41 + replacer: strings.NewReplacer(pairs...), 42 + } 43 + } 44 + 45 + // Mask replaces all registered secret values with "***". 46 + func (m *SecretMask) Mask(input string) string { 47 + if m == nil || m.replacer == nil { 48 + return input 49 + } 50 + return m.replacer.Replace(input) 51 + }
+135
spindle/models/secret_mask_test.go
··· 1 + package models 2 + 3 + import ( 4 + "encoding/base64" 5 + "testing" 6 + ) 7 + 8 + func TestSecretMask_BasicMasking(t *testing.T) { 9 + mask := NewSecretMask([]string{"mysecret123"}) 10 + 11 + input := "The password is mysecret123 in this log" 12 + expected := "The password is *** in this log" 13 + 14 + result := mask.Mask(input) 15 + if result != expected { 16 + t.Errorf("expected %q, got %q", expected, result) 17 + } 18 + } 19 + 20 + func TestSecretMask_Base64Encoded(t *testing.T) { 21 + secret := "mysecret123" 22 + mask := NewSecretMask([]string{secret}) 23 + 24 + b64 := base64.StdEncoding.EncodeToString([]byte(secret)) 25 + input := "Encoded: " + b64 26 + expected := "Encoded: ***" 27 + 28 + result := mask.Mask(input) 29 + if result != expected { 30 + t.Errorf("expected %q, got %q", expected, result) 31 + } 32 + } 33 + 34 + func TestSecretMask_Base64NoPadding(t *testing.T) { 35 + // "test" encodes to "dGVzdA==" with padding 36 + secret := "test" 37 + mask := NewSecretMask([]string{secret}) 38 + 39 + b64NoPad := "dGVzdA" // base64 without padding 40 + input := "Token: " + b64NoPad 41 + expected := "Token: ***" 42 + 43 + result := mask.Mask(input) 44 + if result != expected { 45 + t.Errorf("expected %q, got %q", expected, result) 46 + } 47 + } 48 + 49 + func TestSecretMask_MultipleSecrets(t *testing.T) { 50 + mask := NewSecretMask([]string{"password1", "apikey123"}) 51 + 52 + input := "Using password1 and apikey123 for auth" 53 + expected := "Using *** and *** for auth" 54 + 55 + result := mask.Mask(input) 56 + if result != expected { 57 + t.Errorf("expected %q, got %q", expected, result) 58 + } 59 + } 60 + 61 + func TestSecretMask_MultipleOccurrences(t *testing.T) { 62 + mask := NewSecretMask([]string{"secret"}) 63 + 64 + input := "secret appears twice: secret" 65 + expected := "*** appears twice: ***" 66 + 67 + result := mask.Mask(input) 68 + if result != expected { 69 + t.Errorf("expected %q, got %q", expected, result) 70 + } 71 + } 72 + 73 + func TestSecretMask_ShortValues(t *testing.T) { 74 + mask := NewSecretMask([]string{"abc", "xy", ""}) 75 + 76 + if mask == nil { 77 + t.Fatal("expected non-nil mask") 78 + } 79 + 80 + input := "abc xy test" 81 + expected := "*** *** test" 82 + result := mask.Mask(input) 83 + if result != expected { 84 + t.Errorf("expected %q, got %q", expected, result) 85 + } 86 + } 87 + 88 + func TestSecretMask_NilMask(t *testing.T) { 89 + var mask *SecretMask 90 + 91 + input := "some input text" 92 + result := mask.Mask(input) 93 + if result != input { 94 + t.Errorf("expected %q, got %q", input, result) 95 + } 96 + } 97 + 98 + func TestSecretMask_EmptyInput(t *testing.T) { 99 + mask := NewSecretMask([]string{"secret"}) 100 + 101 + result := mask.Mask("") 102 + if result != "" { 103 + t.Errorf("expected empty string, got %q", result) 104 + } 105 + } 106 + 107 + func TestSecretMask_NoMatch(t *testing.T) { 108 + mask := NewSecretMask([]string{"secretvalue"}) 109 + 110 + input := "nothing to mask here" 111 + result := mask.Mask(input) 112 + if result != input { 113 + t.Errorf("expected %q, got %q", input, result) 114 + } 115 + } 116 + 117 + func TestSecretMask_EmptySecretsList(t *testing.T) { 118 + mask := NewSecretMask([]string{}) 119 + 120 + if mask != nil { 121 + t.Error("expected nil mask for empty secrets list") 122 + } 123 + } 124 + 125 + func TestSecretMask_EmptySecretsFiltered(t *testing.T) { 126 + mask := NewSecretMask([]string{"ab", "validpassword", "", "xyz"}) 127 + 128 + input := "Using validpassword here" 129 + expected := "Using *** here" 130 + 131 + result := mask.Mask(input) 132 + if result != expected { 133 + t.Errorf("expected %q, got %q", expected, result) 134 + } 135 + }
+1 -1
spindle/motd
··· 20 20 ** 21 21 ******** 22 22 23 - This is a spindle server. More info at https://tangled.sh/@tangled.sh/core/tree/master/docs/spindle 23 + This is a spindle server. More info at https://docs.tangled.org/spindles.html#spindles 24 24 25 25 Most API routes are under /xrpc/
+31 -13
spindle/server.go
··· 8 8 "log/slog" 9 9 "maps" 10 10 "net/http" 11 + "sync" 11 12 12 13 "github.com/go-chi/chi/v5" 13 14 "tangled.org/core/api/tangled" ··· 30 31 ) 31 32 32 33 //go:embed motd 33 - var motd []byte 34 + var defaultMotd []byte 34 35 35 36 const ( 36 37 rbacDomain = "thisserver" 37 38 ) 38 39 39 40 type Spindle struct { 40 - jc *jetstream.JetstreamClient 41 - db *db.DB 42 - e *rbac.Enforcer 43 - l *slog.Logger 44 - n *notifier.Notifier 45 - engs map[string]models.Engine 46 - jq *queue.Queue 47 - cfg *config.Config 48 - ks *eventconsumer.Consumer 49 - res *idresolver.Resolver 50 - vault secrets.Manager 41 + jc *jetstream.JetstreamClient 42 + db *db.DB 43 + e *rbac.Enforcer 44 + l *slog.Logger 45 + n *notifier.Notifier 46 + engs map[string]models.Engine 47 + jq *queue.Queue 48 + cfg *config.Config 49 + ks *eventconsumer.Consumer 50 + res *idresolver.Resolver 51 + vault secrets.Manager 52 + motd []byte 53 + motdMu sync.RWMutex 51 54 } 52 55 53 56 // New creates a new Spindle server with the provided configuration and engines. ··· 128 131 cfg: cfg, 129 132 res: resolver, 130 133 vault: vault, 134 + motd: defaultMotd, 131 135 } 132 136 133 137 err = e.AddSpindle(rbacDomain) ··· 201 205 return s.e 202 206 } 203 207 208 + // SetMotdContent sets custom MOTD content, replacing the embedded default. 209 + func (s *Spindle) SetMotdContent(content []byte) { 210 + s.motdMu.Lock() 211 + defer s.motdMu.Unlock() 212 + s.motd = content 213 + } 214 + 215 + // GetMotdContent returns the current MOTD content. 216 + func (s *Spindle) GetMotdContent() []byte { 217 + s.motdMu.RLock() 218 + defer s.motdMu.RUnlock() 219 + return s.motd 220 + } 221 + 204 222 // Start starts the Spindle server (blocking). 205 223 func (s *Spindle) Start(ctx context.Context) error { 206 224 // starts a job queue runner in the background ··· 246 264 mux := chi.NewRouter() 247 265 248 266 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 249 - w.Write(motd) 267 + w.Write(s.GetMotdContent()) 250 268 }) 251 269 mux.HandleFunc("/events", s.Events) 252 270 mux.HandleFunc("/logs/{knot}/{rkey}/{name}", s.Logs)
+1 -1
tailwind.config.js
··· 2 2 const colors = require("tailwindcss/colors"); 3 3 4 4 module.exports = { 5 - content: ["./appview/pages/templates/**/*.html", "./appview/pages/chroma.go"], 5 + content: ["./appview/pages/templates/**/*.html", "./appview/pages/chroma.go", "./docs/*.html"], 6 6 darkMode: "media", 7 7 theme: { 8 8 container: {
+6 -1
types/commit.go
··· 174 174 175 175 func (commit Commit) CoAuthors() []object.Signature { 176 176 var coAuthors []object.Signature 177 - 177 + seen := make(map[string]bool) 178 178 matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1) 179 179 180 180 for _, match := range matches { 181 181 if len(match) >= 3 { 182 182 name := strings.TrimSpace(match[1]) 183 183 email := strings.TrimSpace(match[2]) 184 + 185 + if seen[email] { 186 + continue 187 + } 188 + seen[email] = true 184 189 185 190 coAuthors = append(coAuthors, object.Signature{ 186 191 Name: name,
+3
types/diff.go
··· 74 74 75 75 // used by html elements as a unique ID for hrefs 76 76 func (d *Diff) Id() string { 77 + if d.IsDelete { 78 + return d.Name.Old 79 + } 77 80 return d.Name.New 78 81 } 79 82
+112
types/diff_test.go
··· 1 + package types 2 + 3 + import "testing" 4 + 5 + func TestDiffId(t *testing.T) { 6 + tests := []struct { 7 + name string 8 + diff Diff 9 + expected string 10 + }{ 11 + { 12 + name: "regular file uses new name", 13 + diff: Diff{ 14 + Name: struct { 15 + Old string `json:"old"` 16 + New string `json:"new"` 17 + }{Old: "", New: "src/main.go"}, 18 + }, 19 + expected: "src/main.go", 20 + }, 21 + { 22 + name: "new file uses new name", 23 + diff: Diff{ 24 + Name: struct { 25 + Old string `json:"old"` 26 + New string `json:"new"` 27 + }{Old: "", New: "src/new.go"}, 28 + IsNew: true, 29 + }, 30 + expected: "src/new.go", 31 + }, 32 + { 33 + name: "deleted file uses old name", 34 + diff: Diff{ 35 + Name: struct { 36 + Old string `json:"old"` 37 + New string `json:"new"` 38 + }{Old: "src/deleted.go", New: ""}, 39 + IsDelete: true, 40 + }, 41 + expected: "src/deleted.go", 42 + }, 43 + { 44 + name: "renamed file uses new name", 45 + diff: Diff{ 46 + Name: struct { 47 + Old string `json:"old"` 48 + New string `json:"new"` 49 + }{Old: "src/old.go", New: "src/renamed.go"}, 50 + IsRename: true, 51 + }, 52 + expected: "src/renamed.go", 53 + }, 54 + } 55 + 56 + for _, tt := range tests { 57 + t.Run(tt.name, func(t *testing.T) { 58 + if got := tt.diff.Id(); got != tt.expected { 59 + t.Errorf("Diff.Id() = %q, want %q", got, tt.expected) 60 + } 61 + }) 62 + } 63 + } 64 + 65 + func TestChangedFilesMatchesDiffId(t *testing.T) { 66 + // ChangedFiles() must return values matching each Diff's Id() 67 + // so that sidebar links point to the correct anchors. 68 + // Tests existing, deleted, new, and renamed files. 69 + nd := NiceDiff{ 70 + Diff: []Diff{ 71 + { 72 + Name: struct { 73 + Old string `json:"old"` 74 + New string `json:"new"` 75 + }{Old: "", New: "src/modified.go"}, 76 + }, 77 + { 78 + Name: struct { 79 + Old string `json:"old"` 80 + New string `json:"new"` 81 + }{Old: "src/deleted.go", New: ""}, 82 + IsDelete: true, 83 + }, 84 + { 85 + Name: struct { 86 + Old string `json:"old"` 87 + New string `json:"new"` 88 + }{Old: "", New: "src/new.go"}, 89 + IsNew: true, 90 + }, 91 + { 92 + Name: struct { 93 + Old string `json:"old"` 94 + New string `json:"new"` 95 + }{Old: "src/old.go", New: "src/renamed.go"}, 96 + IsRename: true, 97 + }, 98 + }, 99 + } 100 + 101 + changedFiles := nd.ChangedFiles() 102 + 103 + if len(changedFiles) != len(nd.Diff) { 104 + t.Fatalf("ChangedFiles() returned %d items, want %d", len(changedFiles), len(nd.Diff)) 105 + } 106 + 107 + for i, diff := range nd.Diff { 108 + if changedFiles[i] != diff.Id() { 109 + t.Errorf("ChangedFiles()[%d] = %q, but Diff.Id() = %q", i, changedFiles[i], diff.Id()) 110 + } 111 + } 112 + }