forked from tangled.org/core
this repo has no description

Compare changes

Choose any two refs to compare.

Changed files
+15566 -3342
.air
api
appview
avatar
camo
cmd
appview
genjwks
docker
docs
knotserver
lexicons
patchutil
scripts
types
+1 -1
.air/appview.toml
··· 1 1 [build] 2 2 cmd = "tailwindcss -i input.css -o ./appview/pages/static/tw.css && go build -o .bin/app ./cmd/appview/main.go" 3 - bin = ".bin/app" 3 + bin = ";set -o allexport && source .env && set +o allexport; .bin/app" 4 4 root = "." 5 5 6 6 exclude_regex = [".*_templ.go"]
+7
.gitignore
··· 6 6 appview/pages/static/* 7 7 result 8 8 !.gitkeep 9 + out/ 10 + ./camo/node_modules/* 11 + ./avatar/node_modules/* 12 + patches 13 + *.qcow2 14 + .DS_Store 15 + .env
+31
api/tangled/actorprofile.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.actor.profile 6 + 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + const ( 12 + ActorProfileNSID = "sh.tangled.actor.profile" 13 + ) 14 + 15 + func init() { 16 + util.RegisterType("sh.tangled.actor.profile", &ActorProfile{}) 17 + } // 18 + // RECORDTYPE: ActorProfile 19 + type ActorProfile struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"` 21 + // bluesky: Include link to this account on Bluesky. 22 + Bluesky bool `json:"bluesky" cborgen:"bluesky"` 23 + // description: Free-form profile description text. 24 + Description *string `json:"description,omitempty" cborgen:"description,omitempty"` 25 + Links []string `json:"links,omitempty" cborgen:"links,omitempty"` 26 + // location: Free-form location text. 27 + Location *string `json:"location,omitempty" cborgen:"location,omitempty"` 28 + // pinnedRepositories: Any ATURI, it is up to appviews to validate these fields. 29 + PinnedRepositories []string `json:"pinnedRepositories,omitempty" cborgen:"pinnedRepositories,omitempty"` 30 + Stats []string `json:"stats,omitempty" cborgen:"stats,omitempty"` 31 + }
+990 -447
api/tangled/cbor_gen.go
··· 8 8 "math" 9 9 "sort" 10 10 11 + util "github.com/bluesky-social/indigo/lex/util" 11 12 cid "github.com/ipfs/go-cid" 12 13 cbg "github.com/whyrusleeping/cbor-gen" 13 14 xerrors "golang.org/x/xerrors" ··· 353 354 } 354 355 355 356 cw := cbg.NewCborWriter(w) 356 - fieldCount := 4 357 357 358 - if t.AddedAt == nil { 359 - fieldCount-- 360 - } 361 - 362 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 358 + if _, err := cw.Write([]byte{164}); err != nil { 363 359 return err 364 360 } 365 361 ··· 405 401 return err 406 402 } 407 403 408 - // t.Member (string) (string) 409 - if len("member") > 1000000 { 410 - return xerrors.Errorf("Value in field \"member\" was too long") 404 + // t.Subject (string) (string) 405 + if len("subject") > 1000000 { 406 + return xerrors.Errorf("Value in field \"subject\" was too long") 411 407 } 412 408 413 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("member"))); err != nil { 409 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { 414 410 return err 415 411 } 416 - if _, err := cw.WriteString(string("member")); err != nil { 412 + if _, err := cw.WriteString(string("subject")); err != nil { 417 413 return err 418 414 } 419 415 420 - if len(t.Member) > 1000000 { 421 - return xerrors.Errorf("Value in field t.Member was too long") 416 + if len(t.Subject) > 1000000 { 417 + return xerrors.Errorf("Value in field t.Subject was too long") 422 418 } 423 419 424 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Member))); err != nil { 420 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { 425 421 return err 426 422 } 427 - if _, err := cw.WriteString(string(t.Member)); err != nil { 423 + if _, err := cw.WriteString(string(t.Subject)); err != nil { 428 424 return err 429 425 } 430 426 431 - // t.AddedAt (string) (string) 432 - if t.AddedAt != nil { 427 + // t.CreatedAt (string) (string) 428 + if len("createdAt") > 1000000 { 429 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 430 + } 433 431 434 - if len("addedAt") > 1000000 { 435 - return xerrors.Errorf("Value in field \"addedAt\" was too long") 436 - } 432 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 433 + return err 434 + } 435 + if _, err := cw.WriteString(string("createdAt")); err != nil { 436 + return err 437 + } 437 438 438 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("addedAt"))); err != nil { 439 - return err 440 - } 441 - if _, err := cw.WriteString(string("addedAt")); err != nil { 442 - return err 443 - } 439 + if len(t.CreatedAt) > 1000000 { 440 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 441 + } 444 442 445 - if t.AddedAt == nil { 446 - if _, err := cw.Write(cbg.CborNull); err != nil { 447 - return err 448 - } 449 - } else { 450 - if len(*t.AddedAt) > 1000000 { 451 - return xerrors.Errorf("Value in field t.AddedAt was too long") 452 - } 453 - 454 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.AddedAt))); err != nil { 455 - return err 456 - } 457 - if _, err := cw.WriteString(string(*t.AddedAt)); err != nil { 458 - return err 459 - } 460 - } 443 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 444 + return err 445 + } 446 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 447 + return err 461 448 } 462 449 return nil 463 450 } ··· 487 474 488 475 n := extra 489 476 490 - nameBuf := make([]byte, 7) 477 + nameBuf := make([]byte, 9) 491 478 for i := uint64(0); i < n; i++ { 492 479 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 493 480 if err != nil { ··· 525 512 526 513 t.Domain = string(sval) 527 514 } 528 - // t.Member (string) (string) 529 - case "member": 515 + // t.Subject (string) (string) 516 + case "subject": 530 517 531 518 { 532 519 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 534 521 return err 535 522 } 536 523 537 - t.Member = string(sval) 524 + t.Subject = string(sval) 538 525 } 539 - // t.AddedAt (string) (string) 540 - case "addedAt": 526 + // t.CreatedAt (string) (string) 527 + case "createdAt": 541 528 542 529 { 543 - b, err := cr.ReadByte() 530 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 544 531 if err != nil { 545 532 return err 546 533 } 547 - if b != cbg.CborNull[0] { 548 - if err := cr.UnreadByte(); err != nil { 549 - return err 550 - } 551 534 552 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 553 - if err != nil { 554 - return err 555 - } 556 - 557 - t.AddedAt = (*string)(&sval) 558 - } 535 + t.CreatedAt = string(sval) 559 536 } 560 537 561 538 default: ··· 645 622 return err 646 623 } 647 624 648 - // t.Created (string) (string) 649 - if len("created") > 1000000 { 650 - return xerrors.Errorf("Value in field \"created\" was too long") 625 + // t.CreatedAt (string) (string) 626 + if len("createdAt") > 1000000 { 627 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 651 628 } 652 629 653 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("created"))); err != nil { 630 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 654 631 return err 655 632 } 656 - if _, err := cw.WriteString(string("created")); err != nil { 633 + if _, err := cw.WriteString(string("createdAt")); err != nil { 657 634 return err 658 635 } 659 636 660 - if len(t.Created) > 1000000 { 661 - return xerrors.Errorf("Value in field t.Created was too long") 637 + if len(t.CreatedAt) > 1000000 { 638 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 662 639 } 663 640 664 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Created))); err != nil { 641 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 665 642 return err 666 643 } 667 - if _, err := cw.WriteString(string(t.Created)); err != nil { 644 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 668 645 return err 669 646 } 670 647 return nil ··· 695 672 696 673 n := extra 697 674 698 - nameBuf := make([]byte, 7) 675 + nameBuf := make([]byte, 9) 699 676 for i := uint64(0); i < n; i++ { 700 677 nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 701 678 if err != nil { ··· 744 721 745 722 t.LexiconTypeID = string(sval) 746 723 } 747 - // t.Created (string) (string) 748 - case "created": 724 + // t.CreatedAt (string) (string) 725 + case "createdAt": 749 726 750 727 { 751 728 sval, err := cbg.ReadStringWithMax(cr, 1000000) ··· 753 730 return err 754 731 } 755 732 756 - t.Created = string(sval) 733 + t.CreatedAt = string(sval) 757 734 } 758 735 759 736 default: ··· 775 752 cw := cbg.NewCborWriter(w) 776 753 fieldCount := 7 777 754 778 - if t.Body == nil { 779 - fieldCount-- 780 - } 781 - 782 755 if t.CommentId == nil { 783 - fieldCount-- 784 - } 785 - 786 - if t.CreatedAt == nil { 787 756 fieldCount-- 788 757 } 789 758 ··· 800 769 } 801 770 802 771 // t.Body (string) (string) 803 - if t.Body != nil { 804 - 805 - if len("body") > 1000000 { 806 - return xerrors.Errorf("Value in field \"body\" was too long") 807 - } 772 + if len("body") > 1000000 { 773 + return xerrors.Errorf("Value in field \"body\" was too long") 774 + } 808 775 809 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 810 - return err 811 - } 812 - if _, err := cw.WriteString(string("body")); err != nil { 813 - return err 814 - } 776 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 777 + return err 778 + } 779 + if _, err := cw.WriteString(string("body")); err != nil { 780 + return err 781 + } 815 782 816 - if t.Body == nil { 817 - if _, err := cw.Write(cbg.CborNull); err != nil { 818 - return err 819 - } 820 - } else { 821 - if len(*t.Body) > 1000000 { 822 - return xerrors.Errorf("Value in field t.Body was too long") 823 - } 783 + if len(t.Body) > 1000000 { 784 + return xerrors.Errorf("Value in field t.Body was too long") 785 + } 824 786 825 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Body))); err != nil { 826 - return err 827 - } 828 - if _, err := cw.WriteString(string(*t.Body)); err != nil { 829 - return err 830 - } 831 - } 787 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil { 788 + return err 789 + } 790 + if _, err := cw.WriteString(string(t.Body)); err != nil { 791 + return err 832 792 } 833 793 834 794 // t.Repo (string) (string) ··· 970 930 } 971 931 972 932 // t.CreatedAt (string) (string) 973 - if t.CreatedAt != nil { 974 - 975 - if len("createdAt") > 1000000 { 976 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 977 - } 933 + if len("createdAt") > 1000000 { 934 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 935 + } 978 936 979 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 980 - return err 981 - } 982 - if _, err := cw.WriteString(string("createdAt")); err != nil { 983 - return err 984 - } 937 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 938 + return err 939 + } 940 + if _, err := cw.WriteString(string("createdAt")); err != nil { 941 + return err 942 + } 985 943 986 - if t.CreatedAt == nil { 987 - if _, err := cw.Write(cbg.CborNull); err != nil { 988 - return err 989 - } 990 - } else { 991 - if len(*t.CreatedAt) > 1000000 { 992 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 993 - } 944 + if len(t.CreatedAt) > 1000000 { 945 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 946 + } 994 947 995 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil { 996 - return err 997 - } 998 - if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil { 999 - return err 1000 - } 1001 - } 948 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 949 + return err 950 + } 951 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 952 + return err 1002 953 } 1003 954 return nil 1004 955 } ··· 1048 999 case "body": 1049 1000 1050 1001 { 1051 - b, err := cr.ReadByte() 1002 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1052 1003 if err != nil { 1053 1004 return err 1054 1005 } 1055 - if b != cbg.CborNull[0] { 1056 - if err := cr.UnreadByte(); err != nil { 1057 - return err 1058 - } 1059 1006 1060 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1061 - if err != nil { 1062 - return err 1063 - } 1064 - 1065 - t.Body = (*string)(&sval) 1066 - } 1007 + t.Body = string(sval) 1067 1008 } 1068 1009 // t.Repo (string) (string) 1069 1010 case "repo": ··· 1169 1110 case "createdAt": 1170 1111 1171 1112 { 1172 - b, err := cr.ReadByte() 1113 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1173 1114 if err != nil { 1174 1115 return err 1175 1116 } 1176 - if b != cbg.CborNull[0] { 1177 - if err := cr.UnreadByte(); err != nil { 1178 - return err 1179 - } 1180 1117 1181 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1182 - if err != nil { 1183 - return err 1184 - } 1185 - 1186 - t.CreatedAt = (*string)(&sval) 1187 - } 1118 + t.CreatedAt = string(sval) 1188 1119 } 1189 1120 1190 1121 default: ··· 1204 1135 } 1205 1136 1206 1137 cw := cbg.NewCborWriter(w) 1207 - fieldCount := 3 1208 1138 1209 - if t.State == nil { 1210 - fieldCount-- 1211 - } 1212 - 1213 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 1139 + if _, err := cw.Write([]byte{163}); err != nil { 1214 1140 return err 1215 1141 } 1216 1142 ··· 1257 1183 } 1258 1184 1259 1185 // t.State (string) (string) 1260 - if t.State != nil { 1186 + if len("state") > 1000000 { 1187 + return xerrors.Errorf("Value in field \"state\" was too long") 1188 + } 1261 1189 1262 - if len("state") > 1000000 { 1263 - return xerrors.Errorf("Value in field \"state\" was too long") 1264 - } 1265 - 1266 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("state"))); err != nil { 1267 - return err 1268 - } 1269 - if _, err := cw.WriteString(string("state")); err != nil { 1270 - return err 1271 - } 1190 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("state"))); err != nil { 1191 + return err 1192 + } 1193 + if _, err := cw.WriteString(string("state")); err != nil { 1194 + return err 1195 + } 1272 1196 1273 - if t.State == nil { 1274 - if _, err := cw.Write(cbg.CborNull); err != nil { 1275 - return err 1276 - } 1277 - } else { 1278 - if len(*t.State) > 1000000 { 1279 - return xerrors.Errorf("Value in field t.State was too long") 1280 - } 1197 + if len(t.State) > 1000000 { 1198 + return xerrors.Errorf("Value in field t.State was too long") 1199 + } 1281 1200 1282 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.State))); err != nil { 1283 - return err 1284 - } 1285 - if _, err := cw.WriteString(string(*t.State)); err != nil { 1286 - return err 1287 - } 1288 - } 1201 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.State))); err != nil { 1202 + return err 1203 + } 1204 + if _, err := cw.WriteString(string(t.State)); err != nil { 1205 + return err 1289 1206 } 1290 1207 return nil 1291 1208 } ··· 1357 1274 case "state": 1358 1275 1359 1276 { 1360 - b, err := cr.ReadByte() 1277 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1361 1278 if err != nil { 1362 1279 return err 1363 1280 } 1364 - if b != cbg.CborNull[0] { 1365 - if err := cr.UnreadByte(); err != nil { 1366 - return err 1367 - } 1368 1281 1369 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1370 - if err != nil { 1371 - return err 1372 - } 1373 - 1374 - t.State = (*string)(&sval) 1375 - } 1282 + t.State = string(sval) 1376 1283 } 1377 1284 1378 1285 default: ··· 1395 1302 fieldCount := 7 1396 1303 1397 1304 if t.Body == nil { 1398 - fieldCount-- 1399 - } 1400 - 1401 - if t.CreatedAt == nil { 1402 1305 fieldCount-- 1403 1306 } 1404 1307 ··· 1549 1452 } 1550 1453 1551 1454 // t.CreatedAt (string) (string) 1552 - if t.CreatedAt != nil { 1553 - 1554 - if len("createdAt") > 1000000 { 1555 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 1556 - } 1455 + if len("createdAt") > 1000000 { 1456 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 1457 + } 1557 1458 1558 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 1559 - return err 1560 - } 1561 - if _, err := cw.WriteString(string("createdAt")); err != nil { 1562 - return err 1563 - } 1459 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 1460 + return err 1461 + } 1462 + if _, err := cw.WriteString(string("createdAt")); err != nil { 1463 + return err 1464 + } 1564 1465 1565 - if t.CreatedAt == nil { 1566 - if _, err := cw.Write(cbg.CborNull); err != nil { 1567 - return err 1568 - } 1569 - } else { 1570 - if len(*t.CreatedAt) > 1000000 { 1571 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 1572 - } 1466 + if len(t.CreatedAt) > 1000000 { 1467 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 1468 + } 1573 1469 1574 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil { 1575 - return err 1576 - } 1577 - if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil { 1578 - return err 1579 - } 1580 - } 1470 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 1471 + return err 1472 + } 1473 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 1474 + return err 1581 1475 } 1582 1476 return nil 1583 1477 } ··· 1718 1612 case "createdAt": 1719 1613 1720 1614 { 1721 - b, err := cr.ReadByte() 1615 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1722 1616 if err != nil { 1723 1617 return err 1724 1618 } 1725 - if b != cbg.CborNull[0] { 1726 - if err := cr.UnreadByte(); err != nil { 1727 - return err 1728 - } 1729 1619 1730 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 1731 - if err != nil { 1732 - return err 1733 - } 1734 - 1735 - t.CreatedAt = (*string)(&sval) 1736 - } 1620 + t.CreatedAt = string(sval) 1737 1621 } 1738 1622 1739 1623 default: ··· 1754 1638 1755 1639 cw := cbg.NewCborWriter(w) 1756 1640 fieldCount := 7 1757 - 1758 - if t.AddedAt == nil { 1759 - fieldCount-- 1760 - } 1761 1641 1762 1642 if t.Description == nil { 1763 1643 fieldCount-- ··· 1891 1771 } 1892 1772 } 1893 1773 1894 - // t.AddedAt (string) (string) 1895 - if t.AddedAt != nil { 1774 + // t.CreatedAt (string) (string) 1775 + if len("createdAt") > 1000000 { 1776 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 1777 + } 1896 1778 1897 - if len("addedAt") > 1000000 { 1898 - return xerrors.Errorf("Value in field \"addedAt\" was too long") 1899 - } 1779 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 1780 + return err 1781 + } 1782 + if _, err := cw.WriteString(string("createdAt")); err != nil { 1783 + return err 1784 + } 1900 1785 1901 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("addedAt"))); err != nil { 1902 - return err 1903 - } 1904 - if _, err := cw.WriteString(string("addedAt")); err != nil { 1905 - return err 1906 - } 1907 - 1908 - if t.AddedAt == nil { 1909 - if _, err := cw.Write(cbg.CborNull); err != nil { 1910 - return err 1911 - } 1912 - } else { 1913 - if len(*t.AddedAt) > 1000000 { 1914 - return xerrors.Errorf("Value in field t.AddedAt was too long") 1915 - } 1786 + if len(t.CreatedAt) > 1000000 { 1787 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 1788 + } 1916 1789 1917 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.AddedAt))); err != nil { 1918 - return err 1919 - } 1920 - if _, err := cw.WriteString(string(*t.AddedAt)); err != nil { 1921 - return err 1922 - } 1923 - } 1790 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 1791 + return err 1792 + } 1793 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 1794 + return err 1924 1795 } 1925 1796 1926 1797 // t.Description (string) (string) ··· 2063 1934 t.Source = (*string)(&sval) 2064 1935 } 2065 1936 } 2066 - // t.AddedAt (string) (string) 2067 - case "addedAt": 1937 + // t.CreatedAt (string) (string) 1938 + case "createdAt": 2068 1939 2069 1940 { 2070 - b, err := cr.ReadByte() 1941 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2071 1942 if err != nil { 2072 1943 return err 2073 1944 } 2074 - if b != cbg.CborNull[0] { 2075 - if err := cr.UnreadByte(); err != nil { 2076 - return err 2077 - } 2078 1945 2079 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2080 - if err != nil { 2081 - return err 2082 - } 2083 - 2084 - t.AddedAt = (*string)(&sval) 2085 - } 1946 + t.CreatedAt = string(sval) 2086 1947 } 2087 1948 // t.Description (string) (string) 2088 1949 case "description": ··· 2126 1987 fieldCount := 9 2127 1988 2128 1989 if t.Body == nil { 2129 - fieldCount-- 2130 - } 2131 - 2132 - if t.CreatedAt == nil { 2133 1990 fieldCount-- 2134 1991 } 2135 1992 ··· 2280 2137 } 2281 2138 2282 2139 // t.CreatedAt (string) (string) 2283 - if t.CreatedAt != nil { 2140 + if len("createdAt") > 1000000 { 2141 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 2142 + } 2284 2143 2285 - if len("createdAt") > 1000000 { 2286 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 2287 - } 2288 - 2289 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 2290 - return err 2291 - } 2292 - if _, err := cw.WriteString(string("createdAt")); err != nil { 2293 - return err 2294 - } 2144 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 2145 + return err 2146 + } 2147 + if _, err := cw.WriteString(string("createdAt")); err != nil { 2148 + return err 2149 + } 2295 2150 2296 - if t.CreatedAt == nil { 2297 - if _, err := cw.Write(cbg.CborNull); err != nil { 2298 - return err 2299 - } 2300 - } else { 2301 - if len(*t.CreatedAt) > 1000000 { 2302 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 2303 - } 2151 + if len(t.CreatedAt) > 1000000 { 2152 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 2153 + } 2304 2154 2305 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil { 2306 - return err 2307 - } 2308 - if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil { 2309 - return err 2310 - } 2311 - } 2155 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 2156 + return err 2157 + } 2158 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 2159 + return err 2312 2160 } 2313 2161 2314 2162 // t.TargetRepo (string) (string) ··· 2504 2352 case "createdAt": 2505 2353 2506 2354 { 2507 - b, err := cr.ReadByte() 2355 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2508 2356 if err != nil { 2509 2357 return err 2510 2358 } 2511 - if b != cbg.CborNull[0] { 2512 - if err := cr.UnreadByte(); err != nil { 2513 - return err 2514 - } 2515 - 2516 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2517 - if err != nil { 2518 - return err 2519 - } 2520 2359 2521 - t.CreatedAt = (*string)(&sval) 2522 - } 2360 + t.CreatedAt = string(sval) 2523 2361 } 2524 2362 // t.TargetRepo (string) (string) 2525 2363 case "targetRepo": ··· 2719 2557 } 2720 2558 2721 2559 cw := cbg.NewCborWriter(w) 2722 - fieldCount := 3 2723 2560 2724 - if t.Status == nil { 2725 - fieldCount-- 2726 - } 2727 - 2728 - if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2561 + if _, err := cw.Write([]byte{163}); err != nil { 2729 2562 return err 2730 2563 } 2731 2564 ··· 2772 2605 } 2773 2606 2774 2607 // t.Status (string) (string) 2775 - if t.Status != nil { 2608 + if len("status") > 1000000 { 2609 + return xerrors.Errorf("Value in field \"status\" was too long") 2610 + } 2776 2611 2777 - if len("status") > 1000000 { 2778 - return xerrors.Errorf("Value in field \"status\" was too long") 2779 - } 2612 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { 2613 + return err 2614 + } 2615 + if _, err := cw.WriteString(string("status")); err != nil { 2616 + return err 2617 + } 2780 2618 2781 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("status"))); err != nil { 2782 - return err 2783 - } 2784 - if _, err := cw.WriteString(string("status")); err != nil { 2785 - return err 2786 - } 2619 + if len(t.Status) > 1000000 { 2620 + return xerrors.Errorf("Value in field t.Status was too long") 2621 + } 2787 2622 2788 - if t.Status == nil { 2789 - if _, err := cw.Write(cbg.CborNull); err != nil { 2790 - return err 2791 - } 2792 - } else { 2793 - if len(*t.Status) > 1000000 { 2794 - return xerrors.Errorf("Value in field t.Status was too long") 2795 - } 2796 - 2797 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Status))); err != nil { 2798 - return err 2799 - } 2800 - if _, err := cw.WriteString(string(*t.Status)); err != nil { 2801 - return err 2802 - } 2803 - } 2623 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Status))); err != nil { 2624 + return err 2625 + } 2626 + if _, err := cw.WriteString(string(t.Status)); err != nil { 2627 + return err 2804 2628 } 2805 2629 return nil 2806 2630 } ··· 2872 2696 case "status": 2873 2697 2874 2698 { 2875 - b, err := cr.ReadByte() 2699 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2876 2700 if err != nil { 2877 2701 return err 2878 2702 } 2879 - if b != cbg.CborNull[0] { 2880 - if err := cr.UnreadByte(); err != nil { 2881 - return err 2882 - } 2883 2703 2884 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2885 - if err != nil { 2886 - return err 2887 - } 2888 - 2889 - t.Status = (*string)(&sval) 2890 - } 2704 + t.Status = string(sval) 2891 2705 } 2892 2706 2893 2707 default: ··· 2909 2723 cw := cbg.NewCborWriter(w) 2910 2724 fieldCount := 7 2911 2725 2912 - if t.Body == nil { 2913 - fieldCount-- 2914 - } 2915 - 2916 2726 if t.CommentId == nil { 2917 - fieldCount-- 2918 - } 2919 - 2920 - if t.CreatedAt == nil { 2921 2727 fieldCount-- 2922 2728 } 2923 2729 ··· 2934 2740 } 2935 2741 2936 2742 // t.Body (string) (string) 2937 - if t.Body != nil { 2938 - 2939 - if len("body") > 1000000 { 2940 - return xerrors.Errorf("Value in field \"body\" was too long") 2941 - } 2743 + if len("body") > 1000000 { 2744 + return xerrors.Errorf("Value in field \"body\" was too long") 2745 + } 2942 2746 2943 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 2944 - return err 2945 - } 2946 - if _, err := cw.WriteString(string("body")); err != nil { 2947 - return err 2948 - } 2747 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("body"))); err != nil { 2748 + return err 2749 + } 2750 + if _, err := cw.WriteString(string("body")); err != nil { 2751 + return err 2752 + } 2949 2753 2950 - if t.Body == nil { 2951 - if _, err := cw.Write(cbg.CborNull); err != nil { 2952 - return err 2953 - } 2954 - } else { 2955 - if len(*t.Body) > 1000000 { 2956 - return xerrors.Errorf("Value in field t.Body was too long") 2957 - } 2754 + if len(t.Body) > 1000000 { 2755 + return xerrors.Errorf("Value in field t.Body was too long") 2756 + } 2958 2757 2959 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Body))); err != nil { 2960 - return err 2961 - } 2962 - if _, err := cw.WriteString(string(*t.Body)); err != nil { 2963 - return err 2964 - } 2965 - } 2758 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Body))); err != nil { 2759 + return err 2760 + } 2761 + if _, err := cw.WriteString(string(t.Body)); err != nil { 2762 + return err 2966 2763 } 2967 2764 2968 2765 // t.Pull (string) (string) ··· 3104 2901 } 3105 2902 3106 2903 // t.CreatedAt (string) (string) 3107 - if t.CreatedAt != nil { 2904 + if len("createdAt") > 1000000 { 2905 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 2906 + } 3108 2907 3109 - if len("createdAt") > 1000000 { 3110 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 3111 - } 2908 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 2909 + return err 2910 + } 2911 + if _, err := cw.WriteString(string("createdAt")); err != nil { 2912 + return err 2913 + } 3112 2914 3113 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 3114 - return err 3115 - } 3116 - if _, err := cw.WriteString(string("createdAt")); err != nil { 3117 - return err 3118 - } 2915 + if len(t.CreatedAt) > 1000000 { 2916 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 2917 + } 3119 2918 3120 - if t.CreatedAt == nil { 3121 - if _, err := cw.Write(cbg.CborNull); err != nil { 3122 - return err 3123 - } 3124 - } else { 3125 - if len(*t.CreatedAt) > 1000000 { 3126 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 3127 - } 3128 - 3129 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil { 3130 - return err 3131 - } 3132 - if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil { 3133 - return err 3134 - } 3135 - } 2919 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 2920 + return err 2921 + } 2922 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 2923 + return err 3136 2924 } 3137 2925 return nil 3138 2926 } ··· 3182 2970 case "body": 3183 2971 3184 2972 { 3185 - b, err := cr.ReadByte() 2973 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3186 2974 if err != nil { 3187 2975 return err 3188 2976 } 3189 - if b != cbg.CborNull[0] { 3190 - if err := cr.UnreadByte(); err != nil { 3191 - return err 3192 - } 3193 2977 3194 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 3195 - if err != nil { 3196 - return err 3197 - } 3198 - 3199 - t.Body = (*string)(&sval) 3200 - } 2978 + t.Body = string(sval) 3201 2979 } 3202 2980 // t.Pull (string) (string) 3203 2981 case "pull": ··· 3303 3081 case "createdAt": 3304 3082 3305 3083 { 3084 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3085 + if err != nil { 3086 + return err 3087 + } 3088 + 3089 + t.CreatedAt = string(sval) 3090 + } 3091 + 3092 + default: 3093 + // Field doesn't exist on this type, so ignore it 3094 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 3095 + return err 3096 + } 3097 + } 3098 + } 3099 + 3100 + return nil 3101 + } 3102 + func (t *RepoArtifact) MarshalCBOR(w io.Writer) error { 3103 + if t == nil { 3104 + _, err := w.Write(cbg.CborNull) 3105 + return err 3106 + } 3107 + 3108 + cw := cbg.NewCborWriter(w) 3109 + fieldCount := 6 3110 + 3111 + if t.Tag == nil { 3112 + fieldCount-- 3113 + } 3114 + 3115 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 3116 + return err 3117 + } 3118 + 3119 + // t.Tag (util.LexBytes) (slice) 3120 + if t.Tag != nil { 3121 + 3122 + if len("tag") > 1000000 { 3123 + return xerrors.Errorf("Value in field \"tag\" was too long") 3124 + } 3125 + 3126 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("tag"))); err != nil { 3127 + return err 3128 + } 3129 + if _, err := cw.WriteString(string("tag")); err != nil { 3130 + return err 3131 + } 3132 + 3133 + if len(t.Tag) > 2097152 { 3134 + return xerrors.Errorf("Byte array in field t.Tag was too long") 3135 + } 3136 + 3137 + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Tag))); err != nil { 3138 + return err 3139 + } 3140 + 3141 + if _, err := cw.Write(t.Tag); err != nil { 3142 + return err 3143 + } 3144 + 3145 + } 3146 + 3147 + // t.Name (string) (string) 3148 + if len("name") > 1000000 { 3149 + return xerrors.Errorf("Value in field \"name\" was too long") 3150 + } 3151 + 3152 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("name"))); err != nil { 3153 + return err 3154 + } 3155 + if _, err := cw.WriteString(string("name")); err != nil { 3156 + return err 3157 + } 3158 + 3159 + if len(t.Name) > 1000000 { 3160 + return xerrors.Errorf("Value in field t.Name was too long") 3161 + } 3162 + 3163 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Name))); err != nil { 3164 + return err 3165 + } 3166 + if _, err := cw.WriteString(string(t.Name)); err != nil { 3167 + return err 3168 + } 3169 + 3170 + // t.Repo (string) (string) 3171 + if len("repo") > 1000000 { 3172 + return xerrors.Errorf("Value in field \"repo\" was too long") 3173 + } 3174 + 3175 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 3176 + return err 3177 + } 3178 + if _, err := cw.WriteString(string("repo")); err != nil { 3179 + return err 3180 + } 3181 + 3182 + if len(t.Repo) > 1000000 { 3183 + return xerrors.Errorf("Value in field t.Repo was too long") 3184 + } 3185 + 3186 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repo))); err != nil { 3187 + return err 3188 + } 3189 + if _, err := cw.WriteString(string(t.Repo)); err != nil { 3190 + return err 3191 + } 3192 + 3193 + // t.LexiconTypeID (string) (string) 3194 + if len("$type") > 1000000 { 3195 + return xerrors.Errorf("Value in field \"$type\" was too long") 3196 + } 3197 + 3198 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 3199 + return err 3200 + } 3201 + if _, err := cw.WriteString(string("$type")); err != nil { 3202 + return err 3203 + } 3204 + 3205 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.repo.artifact"))); err != nil { 3206 + return err 3207 + } 3208 + if _, err := cw.WriteString(string("sh.tangled.repo.artifact")); err != nil { 3209 + return err 3210 + } 3211 + 3212 + // t.Artifact (util.LexBlob) (struct) 3213 + if len("artifact") > 1000000 { 3214 + return xerrors.Errorf("Value in field \"artifact\" was too long") 3215 + } 3216 + 3217 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("artifact"))); err != nil { 3218 + return err 3219 + } 3220 + if _, err := cw.WriteString(string("artifact")); err != nil { 3221 + return err 3222 + } 3223 + 3224 + if err := t.Artifact.MarshalCBOR(cw); err != nil { 3225 + return err 3226 + } 3227 + 3228 + // t.CreatedAt (string) (string) 3229 + if len("createdAt") > 1000000 { 3230 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 3231 + } 3232 + 3233 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 3234 + return err 3235 + } 3236 + if _, err := cw.WriteString(string("createdAt")); err != nil { 3237 + return err 3238 + } 3239 + 3240 + if len(t.CreatedAt) > 1000000 { 3241 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 3242 + } 3243 + 3244 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { 3245 + return err 3246 + } 3247 + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { 3248 + return err 3249 + } 3250 + return nil 3251 + } 3252 + 3253 + func (t *RepoArtifact) UnmarshalCBOR(r io.Reader) (err error) { 3254 + *t = RepoArtifact{} 3255 + 3256 + cr := cbg.NewCborReader(r) 3257 + 3258 + maj, extra, err := cr.ReadHeader() 3259 + if err != nil { 3260 + return err 3261 + } 3262 + defer func() { 3263 + if err == io.EOF { 3264 + err = io.ErrUnexpectedEOF 3265 + } 3266 + }() 3267 + 3268 + if maj != cbg.MajMap { 3269 + return fmt.Errorf("cbor input should be of type map") 3270 + } 3271 + 3272 + if extra > cbg.MaxLength { 3273 + return fmt.Errorf("RepoArtifact: map struct too large (%d)", extra) 3274 + } 3275 + 3276 + n := extra 3277 + 3278 + nameBuf := make([]byte, 9) 3279 + for i := uint64(0); i < n; i++ { 3280 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 3281 + if err != nil { 3282 + return err 3283 + } 3284 + 3285 + if !ok { 3286 + // Field doesn't exist on this type, so ignore it 3287 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 3288 + return err 3289 + } 3290 + continue 3291 + } 3292 + 3293 + switch string(nameBuf[:nameLen]) { 3294 + // t.Tag (util.LexBytes) (slice) 3295 + case "tag": 3296 + 3297 + maj, extra, err = cr.ReadHeader() 3298 + if err != nil { 3299 + return err 3300 + } 3301 + 3302 + if extra > 2097152 { 3303 + return fmt.Errorf("t.Tag: byte array too large (%d)", extra) 3304 + } 3305 + if maj != cbg.MajByteString { 3306 + return fmt.Errorf("expected byte array") 3307 + } 3308 + 3309 + if extra > 0 { 3310 + t.Tag = make([]uint8, extra) 3311 + } 3312 + 3313 + if _, err := io.ReadFull(cr, t.Tag); err != nil { 3314 + return err 3315 + } 3316 + 3317 + // t.Name (string) (string) 3318 + case "name": 3319 + 3320 + { 3321 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3322 + if err != nil { 3323 + return err 3324 + } 3325 + 3326 + t.Name = string(sval) 3327 + } 3328 + // t.Repo (string) (string) 3329 + case "repo": 3330 + 3331 + { 3332 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3333 + if err != nil { 3334 + return err 3335 + } 3336 + 3337 + t.Repo = string(sval) 3338 + } 3339 + // t.LexiconTypeID (string) (string) 3340 + case "$type": 3341 + 3342 + { 3343 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3344 + if err != nil { 3345 + return err 3346 + } 3347 + 3348 + t.LexiconTypeID = string(sval) 3349 + } 3350 + // t.Artifact (util.LexBlob) (struct) 3351 + case "artifact": 3352 + 3353 + { 3354 + 3355 + b, err := cr.ReadByte() 3356 + if err != nil { 3357 + return err 3358 + } 3359 + if b != cbg.CborNull[0] { 3360 + if err := cr.UnreadByte(); err != nil { 3361 + return err 3362 + } 3363 + t.Artifact = new(util.LexBlob) 3364 + if err := t.Artifact.UnmarshalCBOR(cr); err != nil { 3365 + return xerrors.Errorf("unmarshaling t.Artifact pointer: %w", err) 3366 + } 3367 + } 3368 + 3369 + } 3370 + // t.CreatedAt (string) (string) 3371 + case "createdAt": 3372 + 3373 + { 3374 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3375 + if err != nil { 3376 + return err 3377 + } 3378 + 3379 + t.CreatedAt = string(sval) 3380 + } 3381 + 3382 + default: 3383 + // Field doesn't exist on this type, so ignore it 3384 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 3385 + return err 3386 + } 3387 + } 3388 + } 3389 + 3390 + return nil 3391 + } 3392 + func (t *ActorProfile) MarshalCBOR(w io.Writer) error { 3393 + if t == nil { 3394 + _, err := w.Write(cbg.CborNull) 3395 + return err 3396 + } 3397 + 3398 + cw := cbg.NewCborWriter(w) 3399 + fieldCount := 7 3400 + 3401 + if t.Description == nil { 3402 + fieldCount-- 3403 + } 3404 + 3405 + if t.Links == nil { 3406 + fieldCount-- 3407 + } 3408 + 3409 + if t.Location == nil { 3410 + fieldCount-- 3411 + } 3412 + 3413 + if t.PinnedRepositories == nil { 3414 + fieldCount-- 3415 + } 3416 + 3417 + if t.Stats == nil { 3418 + fieldCount-- 3419 + } 3420 + 3421 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 3422 + return err 3423 + } 3424 + 3425 + // t.LexiconTypeID (string) (string) 3426 + if len("$type") > 1000000 { 3427 + return xerrors.Errorf("Value in field \"$type\" was too long") 3428 + } 3429 + 3430 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { 3431 + return err 3432 + } 3433 + if _, err := cw.WriteString(string("$type")); err != nil { 3434 + return err 3435 + } 3436 + 3437 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.actor.profile"))); err != nil { 3438 + return err 3439 + } 3440 + if _, err := cw.WriteString(string("sh.tangled.actor.profile")); err != nil { 3441 + return err 3442 + } 3443 + 3444 + // t.Links ([]string) (slice) 3445 + if t.Links != nil { 3446 + 3447 + if len("links") > 1000000 { 3448 + return xerrors.Errorf("Value in field \"links\" was too long") 3449 + } 3450 + 3451 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("links"))); err != nil { 3452 + return err 3453 + } 3454 + if _, err := cw.WriteString(string("links")); err != nil { 3455 + return err 3456 + } 3457 + 3458 + if len(t.Links) > 8192 { 3459 + return xerrors.Errorf("Slice value in field t.Links was too long") 3460 + } 3461 + 3462 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Links))); err != nil { 3463 + return err 3464 + } 3465 + for _, v := range t.Links { 3466 + if len(v) > 1000000 { 3467 + return xerrors.Errorf("Value in field v was too long") 3468 + } 3469 + 3470 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 3471 + return err 3472 + } 3473 + if _, err := cw.WriteString(string(v)); err != nil { 3474 + return err 3475 + } 3476 + 3477 + } 3478 + } 3479 + 3480 + // t.Stats ([]string) (slice) 3481 + if t.Stats != nil { 3482 + 3483 + if len("stats") > 1000000 { 3484 + return xerrors.Errorf("Value in field \"stats\" was too long") 3485 + } 3486 + 3487 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("stats"))); err != nil { 3488 + return err 3489 + } 3490 + if _, err := cw.WriteString(string("stats")); err != nil { 3491 + return err 3492 + } 3493 + 3494 + if len(t.Stats) > 8192 { 3495 + return xerrors.Errorf("Slice value in field t.Stats was too long") 3496 + } 3497 + 3498 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Stats))); err != nil { 3499 + return err 3500 + } 3501 + for _, v := range t.Stats { 3502 + if len(v) > 1000000 { 3503 + return xerrors.Errorf("Value in field v was too long") 3504 + } 3505 + 3506 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 3507 + return err 3508 + } 3509 + if _, err := cw.WriteString(string(v)); err != nil { 3510 + return err 3511 + } 3512 + 3513 + } 3514 + } 3515 + 3516 + // t.Bluesky (bool) (bool) 3517 + if len("bluesky") > 1000000 { 3518 + return xerrors.Errorf("Value in field \"bluesky\" was too long") 3519 + } 3520 + 3521 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("bluesky"))); err != nil { 3522 + return err 3523 + } 3524 + if _, err := cw.WriteString(string("bluesky")); err != nil { 3525 + return err 3526 + } 3527 + 3528 + if err := cbg.WriteBool(w, t.Bluesky); err != nil { 3529 + return err 3530 + } 3531 + 3532 + // t.Location (string) (string) 3533 + if t.Location != nil { 3534 + 3535 + if len("location") > 1000000 { 3536 + return xerrors.Errorf("Value in field \"location\" was too long") 3537 + } 3538 + 3539 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("location"))); err != nil { 3540 + return err 3541 + } 3542 + if _, err := cw.WriteString(string("location")); err != nil { 3543 + return err 3544 + } 3545 + 3546 + if t.Location == nil { 3547 + if _, err := cw.Write(cbg.CborNull); err != nil { 3548 + return err 3549 + } 3550 + } else { 3551 + if len(*t.Location) > 1000000 { 3552 + return xerrors.Errorf("Value in field t.Location was too long") 3553 + } 3554 + 3555 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Location))); err != nil { 3556 + return err 3557 + } 3558 + if _, err := cw.WriteString(string(*t.Location)); err != nil { 3559 + return err 3560 + } 3561 + } 3562 + } 3563 + 3564 + // t.Description (string) (string) 3565 + if t.Description != nil { 3566 + 3567 + if len("description") > 1000000 { 3568 + return xerrors.Errorf("Value in field \"description\" was too long") 3569 + } 3570 + 3571 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("description"))); err != nil { 3572 + return err 3573 + } 3574 + if _, err := cw.WriteString(string("description")); err != nil { 3575 + return err 3576 + } 3577 + 3578 + if t.Description == nil { 3579 + if _, err := cw.Write(cbg.CborNull); err != nil { 3580 + return err 3581 + } 3582 + } else { 3583 + if len(*t.Description) > 1000000 { 3584 + return xerrors.Errorf("Value in field t.Description was too long") 3585 + } 3586 + 3587 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Description))); err != nil { 3588 + return err 3589 + } 3590 + if _, err := cw.WriteString(string(*t.Description)); err != nil { 3591 + return err 3592 + } 3593 + } 3594 + } 3595 + 3596 + // t.PinnedRepositories ([]string) (slice) 3597 + if t.PinnedRepositories != nil { 3598 + 3599 + if len("pinnedRepositories") > 1000000 { 3600 + return xerrors.Errorf("Value in field \"pinnedRepositories\" was too long") 3601 + } 3602 + 3603 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("pinnedRepositories"))); err != nil { 3604 + return err 3605 + } 3606 + if _, err := cw.WriteString(string("pinnedRepositories")); err != nil { 3607 + return err 3608 + } 3609 + 3610 + if len(t.PinnedRepositories) > 8192 { 3611 + return xerrors.Errorf("Slice value in field t.PinnedRepositories was too long") 3612 + } 3613 + 3614 + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PinnedRepositories))); err != nil { 3615 + return err 3616 + } 3617 + for _, v := range t.PinnedRepositories { 3618 + if len(v) > 1000000 { 3619 + return xerrors.Errorf("Value in field v was too long") 3620 + } 3621 + 3622 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil { 3623 + return err 3624 + } 3625 + if _, err := cw.WriteString(string(v)); err != nil { 3626 + return err 3627 + } 3628 + 3629 + } 3630 + } 3631 + return nil 3632 + } 3633 + 3634 + func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) { 3635 + *t = ActorProfile{} 3636 + 3637 + cr := cbg.NewCborReader(r) 3638 + 3639 + maj, extra, err := cr.ReadHeader() 3640 + if err != nil { 3641 + return err 3642 + } 3643 + defer func() { 3644 + if err == io.EOF { 3645 + err = io.ErrUnexpectedEOF 3646 + } 3647 + }() 3648 + 3649 + if maj != cbg.MajMap { 3650 + return fmt.Errorf("cbor input should be of type map") 3651 + } 3652 + 3653 + if extra > cbg.MaxLength { 3654 + return fmt.Errorf("ActorProfile: map struct too large (%d)", extra) 3655 + } 3656 + 3657 + n := extra 3658 + 3659 + nameBuf := make([]byte, 18) 3660 + for i := uint64(0); i < n; i++ { 3661 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 3662 + if err != nil { 3663 + return err 3664 + } 3665 + 3666 + if !ok { 3667 + // Field doesn't exist on this type, so ignore it 3668 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 3669 + return err 3670 + } 3671 + continue 3672 + } 3673 + 3674 + switch string(nameBuf[:nameLen]) { 3675 + // t.LexiconTypeID (string) (string) 3676 + case "$type": 3677 + 3678 + { 3679 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3680 + if err != nil { 3681 + return err 3682 + } 3683 + 3684 + t.LexiconTypeID = string(sval) 3685 + } 3686 + // t.Links ([]string) (slice) 3687 + case "links": 3688 + 3689 + maj, extra, err = cr.ReadHeader() 3690 + if err != nil { 3691 + return err 3692 + } 3693 + 3694 + if extra > 8192 { 3695 + return fmt.Errorf("t.Links: array too large (%d)", extra) 3696 + } 3697 + 3698 + if maj != cbg.MajArray { 3699 + return fmt.Errorf("expected cbor array") 3700 + } 3701 + 3702 + if extra > 0 { 3703 + t.Links = make([]string, extra) 3704 + } 3705 + 3706 + for i := 0; i < int(extra); i++ { 3707 + { 3708 + var maj byte 3709 + var extra uint64 3710 + var err error 3711 + _ = maj 3712 + _ = extra 3713 + _ = err 3714 + 3715 + { 3716 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3717 + if err != nil { 3718 + return err 3719 + } 3720 + 3721 + t.Links[i] = string(sval) 3722 + } 3723 + 3724 + } 3725 + } 3726 + // t.Stats ([]string) (slice) 3727 + case "stats": 3728 + 3729 + maj, extra, err = cr.ReadHeader() 3730 + if err != nil { 3731 + return err 3732 + } 3733 + 3734 + if extra > 8192 { 3735 + return fmt.Errorf("t.Stats: array too large (%d)", extra) 3736 + } 3737 + 3738 + if maj != cbg.MajArray { 3739 + return fmt.Errorf("expected cbor array") 3740 + } 3741 + 3742 + if extra > 0 { 3743 + t.Stats = make([]string, extra) 3744 + } 3745 + 3746 + for i := 0; i < int(extra); i++ { 3747 + { 3748 + var maj byte 3749 + var extra uint64 3750 + var err error 3751 + _ = maj 3752 + _ = extra 3753 + _ = err 3754 + 3755 + { 3756 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3757 + if err != nil { 3758 + return err 3759 + } 3760 + 3761 + t.Stats[i] = string(sval) 3762 + } 3763 + 3764 + } 3765 + } 3766 + // t.Bluesky (bool) (bool) 3767 + case "bluesky": 3768 + 3769 + maj, extra, err = cr.ReadHeader() 3770 + if err != nil { 3771 + return err 3772 + } 3773 + if maj != cbg.MajOther { 3774 + return fmt.Errorf("booleans must be major type 7") 3775 + } 3776 + switch extra { 3777 + case 20: 3778 + t.Bluesky = false 3779 + case 21: 3780 + t.Bluesky = true 3781 + default: 3782 + return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra) 3783 + } 3784 + // t.Location (string) (string) 3785 + case "location": 3786 + 3787 + { 3306 3788 b, err := cr.ReadByte() 3307 3789 if err != nil { 3308 3790 return err ··· 3317 3799 return err 3318 3800 } 3319 3801 3320 - t.CreatedAt = (*string)(&sval) 3802 + t.Location = (*string)(&sval) 3803 + } 3804 + } 3805 + // t.Description (string) (string) 3806 + case "description": 3807 + 3808 + { 3809 + b, err := cr.ReadByte() 3810 + if err != nil { 3811 + return err 3812 + } 3813 + if b != cbg.CborNull[0] { 3814 + if err := cr.UnreadByte(); err != nil { 3815 + return err 3816 + } 3817 + 3818 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3819 + if err != nil { 3820 + return err 3821 + } 3822 + 3823 + t.Description = (*string)(&sval) 3824 + } 3825 + } 3826 + // t.PinnedRepositories ([]string) (slice) 3827 + case "pinnedRepositories": 3828 + 3829 + maj, extra, err = cr.ReadHeader() 3830 + if err != nil { 3831 + return err 3832 + } 3833 + 3834 + if extra > 8192 { 3835 + return fmt.Errorf("t.PinnedRepositories: array too large (%d)", extra) 3836 + } 3837 + 3838 + if maj != cbg.MajArray { 3839 + return fmt.Errorf("expected cbor array") 3840 + } 3841 + 3842 + if extra > 0 { 3843 + t.PinnedRepositories = make([]string, extra) 3844 + } 3845 + 3846 + for i := 0; i < int(extra); i++ { 3847 + { 3848 + var maj byte 3849 + var extra uint64 3850 + var err error 3851 + _ = maj 3852 + _ = extra 3853 + _ = err 3854 + 3855 + { 3856 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3857 + if err != nil { 3858 + return err 3859 + } 3860 + 3861 + t.PinnedRepositories[i] = string(sval) 3862 + } 3863 + 3321 3864 } 3322 3865 } 3323 3866
+2 -2
api/tangled/issuecomment.go
··· 18 18 // RECORDTYPE: RepoIssueComment 19 19 type RepoIssueComment struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 21 + Body string `json:"body" cborgen:"body"` 22 22 CommentId *int64 `json:"commentId,omitempty" cborgen:"commentId,omitempty"` 23 - CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"` 23 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 24 24 Issue string `json:"issue" cborgen:"issue"` 25 25 Owner *string `json:"owner,omitempty" cborgen:"owner,omitempty"` 26 26 Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"`
+1 -1
api/tangled/issuestate.go
··· 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.state" cborgen:"$type,const=sh.tangled.repo.issue.state"` 21 21 Issue string `json:"issue" cborgen:"issue"` 22 22 // state: state of the issue 23 - State *string `json:"state,omitempty" cborgen:"state,omitempty"` 23 + State string `json:"state" cborgen:"state"` 24 24 }
+4 -4
api/tangled/knotmember.go
··· 17 17 } // 18 18 // RECORDTYPE: KnotMember 19 19 type KnotMember struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.knot.member" cborgen:"$type,const=sh.tangled.knot.member"` 21 - AddedAt *string `json:"addedAt,omitempty" cborgen:"addedAt,omitempty"` 20 + LexiconTypeID string `json:"$type,const=sh.tangled.knot.member" cborgen:"$type,const=sh.tangled.knot.member"` 21 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 22 // domain: domain that this member now belongs to 23 - Domain string `json:"domain" cborgen:"domain"` 24 - Member string `json:"member" cborgen:"member"` 23 + Domain string `json:"domain" cborgen:"domain"` 24 + Subject string `json:"subject" cborgen:"subject"` 25 25 }
+2 -2
api/tangled/pullcomment.go
··· 18 18 // RECORDTYPE: RepoPullComment 19 19 type RepoPullComment struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 21 + Body string `json:"body" cborgen:"body"` 22 22 CommentId *int64 `json:"commentId,omitempty" cborgen:"commentId,omitempty"` 23 - CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"` 23 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 24 24 Owner *string `json:"owner,omitempty" cborgen:"owner,omitempty"` 25 25 Pull string `json:"pull" cborgen:"pull"` 26 26 Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"`
+1 -1
api/tangled/pullstatus.go
··· 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.status" cborgen:"$type,const=sh.tangled.repo.pull.status"` 21 21 Pull string `json:"pull" cborgen:"pull"` 22 22 // status: status of the pull request 23 - Status *string `json:"status,omitempty" cborgen:"status,omitempty"` 23 + Status string `json:"status" cborgen:"status"` 24 24 }
+31
api/tangled/repoartifact.go
··· 1 + // Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. 2 + 3 + package tangled 4 + 5 + // schema: sh.tangled.repo.artifact 6 + 7 + import ( 8 + "github.com/bluesky-social/indigo/lex/util" 9 + ) 10 + 11 + const ( 12 + RepoArtifactNSID = "sh.tangled.repo.artifact" 13 + ) 14 + 15 + func init() { 16 + util.RegisterType("sh.tangled.repo.artifact", &RepoArtifact{}) 17 + } // 18 + // RECORDTYPE: RepoArtifact 19 + type RepoArtifact struct { 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.artifact" cborgen:"$type,const=sh.tangled.repo.artifact"` 21 + // artifact: the artifact 22 + Artifact *util.LexBlob `json:"artifact" cborgen:"artifact"` 23 + // createdAt: time of creation of this artifact 24 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 25 + // name: name of the artifact 26 + Name string `json:"name" cborgen:"name"` 27 + // repo: repo that this artifact is being uploaded to 28 + Repo string `json:"repo" cborgen:"repo"` 29 + // tag: hash of the tag object that this artifact is attached to (only annotated tags are supported) 30 + Tag util.LexBytes `json:"tag,omitempty" cborgen:"tag,omitempty"` 31 + }
+1 -1
api/tangled/repoissue.go
··· 19 19 type RepoIssue struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"` 21 21 Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 23 IssueId int64 `json:"issueId" cborgen:"issueId"` 24 24 Owner string `json:"owner" cborgen:"owner"` 25 25 Repo string `json:"repo" cborgen:"repo"`
+1 -1
api/tangled/repopull.go
··· 19 19 type RepoPull struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 21 Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"` 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 23 Patch string `json:"patch" cborgen:"patch"` 24 24 PullId int64 `json:"pullId" cborgen:"pullId"` 25 25 Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
+2 -2
api/tangled/tangledpublicKey.go
··· 18 18 // RECORDTYPE: PublicKey 19 19 type PublicKey struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.publicKey" cborgen:"$type,const=sh.tangled.publicKey"` 21 - // created: key upload timestamp 22 - Created string `json:"created" cborgen:"created"` 21 + // createdAt: key upload timestamp 22 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 23 23 // key: public key contents 24 24 Key string `json:"key" cborgen:"key"` 25 25 // name: human-readable name for this key
+1 -1
api/tangled/tangledrepo.go
··· 18 18 // RECORDTYPE: Repo 19 19 type Repo struct { 20 20 LexiconTypeID string `json:"$type,const=sh.tangled.repo" cborgen:"$type,const=sh.tangled.repo"` 21 - AddedAt *string `json:"addedAt,omitempty" cborgen:"addedAt,omitempty"` 21 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` 22 22 Description *string `json:"description,omitempty" cborgen:"description,omitempty"` 23 23 // knot: knot where the repo was created 24 24 Knot string `json:"knot" cborgen:"knot"`
-217
appview/auth/auth.go
··· 1 - package auth 2 - 3 - import ( 4 - "context" 5 - "fmt" 6 - "net/http" 7 - "time" 8 - 9 - comatproto "github.com/bluesky-social/indigo/api/atproto" 10 - "github.com/bluesky-social/indigo/atproto/identity" 11 - "github.com/bluesky-social/indigo/xrpc" 12 - "github.com/gorilla/sessions" 13 - "tangled.sh/tangled.sh/core/appview" 14 - ) 15 - 16 - type Auth struct { 17 - Store *sessions.CookieStore 18 - } 19 - 20 - type AtSessionCreate struct { 21 - comatproto.ServerCreateSession_Output 22 - PDSEndpoint string 23 - } 24 - 25 - type AtSessionRefresh struct { 26 - comatproto.ServerRefreshSession_Output 27 - PDSEndpoint string 28 - } 29 - 30 - func Make(secret string) (*Auth, error) { 31 - store := sessions.NewCookieStore([]byte(secret)) 32 - return &Auth{store}, nil 33 - } 34 - 35 - func (a *Auth) CreateInitialSession(ctx context.Context, resolved *identity.Identity, appPassword string) (*comatproto.ServerCreateSession_Output, error) { 36 - 37 - pdsUrl := resolved.PDSEndpoint() 38 - client := xrpc.Client{ 39 - Host: pdsUrl, 40 - } 41 - 42 - atSession, err := comatproto.ServerCreateSession(ctx, &client, &comatproto.ServerCreateSession_Input{ 43 - Identifier: resolved.DID.String(), 44 - Password: appPassword, 45 - }) 46 - if err != nil { 47 - return nil, fmt.Errorf("invalid app password") 48 - } 49 - 50 - return atSession, nil 51 - } 52 - 53 - // Sessionish is an interface that provides access to the common fields of both types. 54 - type Sessionish interface { 55 - GetAccessJwt() string 56 - GetActive() *bool 57 - GetDid() string 58 - GetDidDoc() *interface{} 59 - GetHandle() string 60 - GetRefreshJwt() string 61 - GetStatus() *string 62 - } 63 - 64 - // Create a wrapper type for ServerRefreshSession_Output 65 - type RefreshSessionWrapper struct { 66 - *comatproto.ServerRefreshSession_Output 67 - } 68 - 69 - func (s *RefreshSessionWrapper) GetAccessJwt() string { 70 - return s.AccessJwt 71 - } 72 - 73 - func (s *RefreshSessionWrapper) GetActive() *bool { 74 - return s.Active 75 - } 76 - 77 - func (s *RefreshSessionWrapper) GetDid() string { 78 - return s.Did 79 - } 80 - 81 - func (s *RefreshSessionWrapper) GetDidDoc() *interface{} { 82 - return s.DidDoc 83 - } 84 - 85 - func (s *RefreshSessionWrapper) GetHandle() string { 86 - return s.Handle 87 - } 88 - 89 - func (s *RefreshSessionWrapper) GetRefreshJwt() string { 90 - return s.RefreshJwt 91 - } 92 - 93 - func (s *RefreshSessionWrapper) GetStatus() *string { 94 - return s.Status 95 - } 96 - 97 - // Create a wrapper type for ServerRefreshSession_Output 98 - type CreateSessionWrapper struct { 99 - *comatproto.ServerCreateSession_Output 100 - } 101 - 102 - func (s *CreateSessionWrapper) GetAccessJwt() string { 103 - return s.AccessJwt 104 - } 105 - 106 - func (s *CreateSessionWrapper) GetActive() *bool { 107 - return s.Active 108 - } 109 - 110 - func (s *CreateSessionWrapper) GetDid() string { 111 - return s.Did 112 - } 113 - 114 - func (s *CreateSessionWrapper) GetDidDoc() *interface{} { 115 - return s.DidDoc 116 - } 117 - 118 - func (s *CreateSessionWrapper) GetHandle() string { 119 - return s.Handle 120 - } 121 - 122 - func (s *CreateSessionWrapper) GetRefreshJwt() string { 123 - return s.RefreshJwt 124 - } 125 - 126 - func (s *CreateSessionWrapper) GetStatus() *string { 127 - return s.Status 128 - } 129 - 130 - func (a *Auth) ClearSession(r *http.Request, w http.ResponseWriter) error { 131 - clientSession, err := a.Store.Get(r, appview.SessionName) 132 - if err != nil { 133 - return fmt.Errorf("invalid session", err) 134 - } 135 - if clientSession.IsNew { 136 - return fmt.Errorf("invalid session") 137 - } 138 - clientSession.Options.MaxAge = -1 139 - return clientSession.Save(r, w) 140 - } 141 - 142 - func (a *Auth) StoreSession(r *http.Request, w http.ResponseWriter, atSessionish Sessionish, pdsEndpoint string) error { 143 - clientSession, _ := a.Store.Get(r, appview.SessionName) 144 - clientSession.Values[appview.SessionHandle] = atSessionish.GetHandle() 145 - clientSession.Values[appview.SessionDid] = atSessionish.GetDid() 146 - clientSession.Values[appview.SessionPds] = pdsEndpoint 147 - clientSession.Values[appview.SessionAccessJwt] = atSessionish.GetAccessJwt() 148 - clientSession.Values[appview.SessionRefreshJwt] = atSessionish.GetRefreshJwt() 149 - clientSession.Values[appview.SessionExpiry] = time.Now().Add(time.Minute * 15).Format(time.RFC3339) 150 - clientSession.Values[appview.SessionAuthenticated] = true 151 - return clientSession.Save(r, w) 152 - } 153 - 154 - func (a *Auth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) { 155 - clientSession, err := a.Store.Get(r, "appview-session") 156 - if err != nil || clientSession.IsNew { 157 - return nil, err 158 - } 159 - 160 - did := clientSession.Values["did"].(string) 161 - pdsUrl := clientSession.Values["pds"].(string) 162 - accessJwt := clientSession.Values["accessJwt"].(string) 163 - refreshJwt := clientSession.Values["refreshJwt"].(string) 164 - 165 - client := &xrpc.Client{ 166 - Host: pdsUrl, 167 - Auth: &xrpc.AuthInfo{ 168 - AccessJwt: accessJwt, 169 - RefreshJwt: refreshJwt, 170 - Did: did, 171 - }, 172 - } 173 - 174 - return client, nil 175 - } 176 - 177 - func (a *Auth) GetSession(r *http.Request) (*sessions.Session, error) { 178 - return a.Store.Get(r, appview.SessionName) 179 - } 180 - 181 - func (a *Auth) GetDid(r *http.Request) string { 182 - clientSession, err := a.Store.Get(r, appview.SessionName) 183 - if err != nil || clientSession.IsNew { 184 - return "" 185 - } 186 - 187 - return clientSession.Values[appview.SessionDid].(string) 188 - } 189 - 190 - func (a *Auth) GetHandle(r *http.Request) string { 191 - clientSession, err := a.Store.Get(r, appview.SessionName) 192 - if err != nil || clientSession.IsNew { 193 - return "" 194 - } 195 - 196 - return clientSession.Values[appview.SessionHandle].(string) 197 - } 198 - 199 - type User struct { 200 - Handle string 201 - Did string 202 - Pds string 203 - } 204 - 205 - func (a *Auth) GetUser(r *http.Request) *User { 206 - clientSession, err := a.Store.Get(r, appview.SessionName) 207 - 208 - if err != nil || clientSession.IsNew { 209 - return nil 210 - } 211 - 212 - return &User{ 213 - Handle: clientSession.Values[appview.SessionHandle].(string), 214 - Did: clientSession.Values[appview.SessionDid].(string), 215 - Pds: clientSession.Values[appview.SessionPds].(string), 216 - } 217 - }
+36 -6
appview/config.go
··· 6 6 "github.com/sethvargo/go-envconfig" 7 7 ) 8 8 9 + type CoreConfig struct { 10 + CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"` 11 + DbPath string `env:"DB_PATH, default=appview.db"` 12 + ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"` 13 + AppviewHost string `env:"APPVIEW_HOST, default=https://tangled.sh"` 14 + Dev bool `env:"DEV, default=false"` 15 + } 16 + 17 + type OAuthConfig struct { 18 + Jwks string `env:"JWKS"` 19 + } 20 + 21 + type JetstreamConfig struct { 22 + Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"` 23 + } 24 + 25 + type ResendConfig struct { 26 + ApiKey string `env:"API_KEY"` 27 + } 28 + 29 + type CamoConfig struct { 30 + Host string `env:"HOST, default=https://camo.tangled.sh"` 31 + SharedSecret string `env:"SHARED_SECRET"` 32 + } 33 + 34 + type AvatarConfig struct { 35 + Host string `env:"HOST, default=https://avatar.tangled.sh"` 36 + SharedSecret string `env:"SHARED_SECRET"` 37 + } 38 + 9 39 type Config struct { 10 - CookieSecret string `env:"TANGLED_COOKIE_SECRET, default=00000000000000000000000000000000"` 11 - DbPath string `env:"TANGLED_DB_PATH, default=appview.db"` 12 - ListenAddr string `env:"TANGLED_LISTEN_ADDR, default=0.0.0.0:3000"` 13 - Dev bool `env:"TANGLED_DEV, default=false"` 14 - JetstreamEndpoint string `env:"TANGLED_JETSTREAM_ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"` 15 - ResendApiKey string `env:"TANGLED_RESEND_API_KEY"` 40 + Core CoreConfig `env:",prefix=TANGLED_"` 41 + Jetstream JetstreamConfig `env:",prefix=TANGLED_JETSTREAM_"` 42 + Resend ResendConfig `env:",prefix=TANGLED_RESEND_"` 43 + Camo CamoConfig `env:",prefix=TANGLED_CAMO_"` 44 + Avatar AvatarConfig `env:",prefix=TANGLED_AVATAR_"` 45 + OAuth OAuthConfig `env:",prefix=TANGLED_OAUTH_"` 16 46 } 17 47 18 48 func LoadConfig(ctx context.Context) (*Config, error) {
+3
appview/consts.go
··· 9 9 SessionRefreshJwt = "refreshJwt" 10 10 SessionExpiry = "expiry" 11 11 SessionAuthenticated = "authenticated" 12 + 13 + SessionDpopPrivateJwk = "dpopPrivateJwk" 14 + SessionDpopAuthServerNonce = "dpopAuthServerNonce" 12 15 )
+150
appview/db/artifact.go
··· 1 + package db 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + "time" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "github.com/go-git/go-git/v5/plumbing" 10 + "github.com/ipfs/go-cid" 11 + "tangled.sh/tangled.sh/core/api/tangled" 12 + ) 13 + 14 + type Artifact struct { 15 + Id uint64 16 + Did string 17 + Rkey string 18 + 19 + RepoAt syntax.ATURI 20 + Tag plumbing.Hash 21 + CreatedAt time.Time 22 + 23 + BlobCid cid.Cid 24 + Name string 25 + Size uint64 26 + MimeType string 27 + } 28 + 29 + func (a *Artifact) ArtifactAt() syntax.ATURI { 30 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", a.Did, tangled.RepoPullNSID, a.Rkey)) 31 + } 32 + 33 + func AddArtifact(e Execer, artifact Artifact) error { 34 + _, err := e.Exec( 35 + `insert or ignore into artifacts ( 36 + did, 37 + rkey, 38 + repo_at, 39 + tag, 40 + created, 41 + blob_cid, 42 + name, 43 + size, 44 + mimetype 45 + ) 46 + values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 47 + artifact.Did, 48 + artifact.Rkey, 49 + artifact.RepoAt, 50 + artifact.Tag[:], 51 + artifact.CreatedAt.Format(time.RFC3339), 52 + artifact.BlobCid.String(), 53 + artifact.Name, 54 + artifact.Size, 55 + artifact.MimeType, 56 + ) 57 + return err 58 + } 59 + 60 + func GetArtifact(e Execer, filters ...filter) ([]Artifact, error) { 61 + var artifacts []Artifact 62 + 63 + var conditions []string 64 + var args []any 65 + for _, filter := range filters { 66 + conditions = append(conditions, filter.Condition()) 67 + args = append(args, filter.arg) 68 + } 69 + 70 + whereClause := "" 71 + if conditions != nil { 72 + whereClause = " where " + strings.Join(conditions, " and ") 73 + } 74 + 75 + query := fmt.Sprintf(`select 76 + did, 77 + rkey, 78 + repo_at, 79 + tag, 80 + created, 81 + blob_cid, 82 + name, 83 + size, 84 + mimetype 85 + from artifacts %s`, 86 + whereClause, 87 + ) 88 + 89 + rows, err := e.Query(query, args...) 90 + 91 + if err != nil { 92 + return nil, err 93 + } 94 + defer rows.Close() 95 + 96 + for rows.Next() { 97 + var artifact Artifact 98 + var createdAt string 99 + var tag []byte 100 + var blobCid string 101 + 102 + if err := rows.Scan( 103 + &artifact.Did, 104 + &artifact.Rkey, 105 + &artifact.RepoAt, 106 + &tag, 107 + &createdAt, 108 + &blobCid, 109 + &artifact.Name, 110 + &artifact.Size, 111 + &artifact.MimeType, 112 + ); err != nil { 113 + return nil, err 114 + } 115 + 116 + artifact.CreatedAt, err = time.Parse(time.RFC3339, createdAt) 117 + if err != nil { 118 + artifact.CreatedAt = time.Now() 119 + } 120 + artifact.Tag = plumbing.Hash(tag) 121 + artifact.BlobCid = cid.MustParse(blobCid) 122 + 123 + artifacts = append(artifacts, artifact) 124 + } 125 + 126 + if err := rows.Err(); err != nil { 127 + return nil, err 128 + } 129 + 130 + return artifacts, nil 131 + } 132 + 133 + func DeleteArtifact(e Execer, filters ...filter) error { 134 + var conditions []string 135 + var args []any 136 + for _, filter := range filters { 137 + conditions = append(conditions, filter.Condition()) 138 + args = append(args, filter.arg) 139 + } 140 + 141 + whereClause := "" 142 + if conditions != nil { 143 + whereClause = " where " + strings.Join(conditions, " and ") 144 + } 145 + 146 + query := fmt.Sprintf(`delete from artifacts %s`, whereClause) 147 + 148 + _, err := e.Exec(query, args...) 149 + return err 150 + }
+122
appview/db/db.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 + "fmt" 6 7 "log" 7 8 8 9 _ "github.com/mattn/go-sqlite3" ··· 208 209 unique(did, email) 209 210 ); 210 211 212 + create table if not exists artifacts ( 213 + -- id 214 + id integer primary key autoincrement, 215 + did text not null, 216 + rkey text not null, 217 + 218 + -- meta 219 + repo_at text not null, 220 + tag binary(20) not null, 221 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 222 + 223 + -- data 224 + blob_cid text not null, 225 + name text not null, 226 + size integer not null default 0, 227 + mimetype string not null default "*/*", 228 + 229 + -- constraints 230 + unique(did, rkey), -- record must be unique 231 + unique(repo_at, tag, name), -- for a given tag object, each file must be unique 232 + foreign key (repo_at) references repos(at_uri) on delete cascade 233 + ); 234 + 235 + create table if not exists profile ( 236 + -- id 237 + id integer primary key autoincrement, 238 + did text not null, 239 + 240 + -- data 241 + description text not null, 242 + include_bluesky integer not null default 0, 243 + location text, 244 + 245 + -- constraints 246 + unique(did) 247 + ); 248 + create table if not exists profile_links ( 249 + -- id 250 + id integer primary key autoincrement, 251 + did text not null, 252 + 253 + -- data 254 + link text not null, 255 + 256 + -- constraints 257 + foreign key (did) references profile(did) on delete cascade 258 + ); 259 + create table if not exists profile_stats ( 260 + -- id 261 + id integer primary key autoincrement, 262 + did text not null, 263 + 264 + -- data 265 + kind text not null check (kind in ( 266 + "merged-pull-request-count", 267 + "closed-pull-request-count", 268 + "open-pull-request-count", 269 + "open-issue-count", 270 + "closed-issue-count", 271 + "repository-count" 272 + )), 273 + 274 + -- constraints 275 + foreign key (did) references profile(did) on delete cascade 276 + ); 277 + create table if not exists profile_pinned_repositories ( 278 + -- id 279 + id integer primary key autoincrement, 280 + did text not null, 281 + 282 + -- data 283 + at_uri text not null, 284 + 285 + -- constraints 286 + unique(did, at_uri), 287 + foreign key (did) references profile(did) on delete cascade, 288 + foreign key (at_uri) references repos(at_uri) on delete cascade 289 + ); 290 + 291 + create table if not exists oauth_requests ( 292 + id integer primary key autoincrement, 293 + auth_server_iss text not null, 294 + state text not null, 295 + did text not null, 296 + handle text not null, 297 + pds_url text not null, 298 + pkce_verifier text not null, 299 + dpop_auth_server_nonce text not null, 300 + dpop_private_jwk text not null 301 + ); 302 + 303 + create table if not exists oauth_sessions ( 304 + id integer primary key autoincrement, 305 + did text not null, 306 + handle text not null, 307 + pds_url text not null, 308 + auth_server_iss text not null, 309 + access_jwt text not null, 310 + refresh_jwt text not null, 311 + dpop_pds_nonce text, 312 + dpop_auth_server_nonce text not null, 313 + dpop_private_jwk text not null, 314 + expiry text not null 315 + ); 316 + 211 317 create table if not exists migrations ( 212 318 id integer primary key autoincrement, 213 319 name text unique ··· 325 431 326 432 return nil 327 433 } 434 + 435 + type filter struct { 436 + key string 437 + arg any 438 + } 439 + 440 + func Filter(key string, arg any) filter { 441 + return filter{ 442 + key: key, 443 + arg: arg, 444 + } 445 + } 446 + 447 + func (f filter) Condition() string { 448 + return fmt.Sprintf("%s = ?", f.key) 449 + }
+6
appview/db/follow.go
··· 47 47 return err 48 48 } 49 49 50 + // Remove a follow 51 + func DeleteFollowByRkey(e Execer, userDid, rkey string) error { 52 + _, err := e.Exec(`delete from follows where user_did = ? and rkey = ?`, userDid, rkey) 53 + return err 54 + } 55 + 50 56 func GetFollowerFollowing(e Execer, did string) (int, int, error) { 51 57 followers, following := 0, 0 52 58 err := e.QueryRow(
+35 -20
appview/db/issues.go
··· 5 5 "time" 6 6 7 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 + "tangled.sh/tangled.sh/core/appview/pagination" 8 9 ) 9 10 10 11 type Issue struct { ··· 102 103 return ownerDid, err 103 104 } 104 105 105 - func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool) ([]Issue, error) { 106 + func GetIssues(e Execer, repoAt syntax.ATURI, isOpen bool, page pagination.Page) ([]Issue, error) { 106 107 var issues []Issue 107 108 openValue := 0 108 109 if isOpen { ··· 110 111 } 111 112 112 113 rows, err := e.Query( 113 - `select 114 - i.owner_did, 115 - i.issue_id, 116 - i.created, 117 - i.title, 118 - i.body, 119 - i.open, 120 - count(c.id) 121 - from 122 - issues i 123 - left join 124 - comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 125 - where 126 - i.repo_at = ? and i.open = ? 127 - group by 128 - i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 129 - order by 130 - i.created desc`, 131 - repoAt, openValue) 114 + ` 115 + with numbered_issue as ( 116 + select 117 + i.owner_did, 118 + i.issue_id, 119 + i.created, 120 + i.title, 121 + i.body, 122 + i.open, 123 + count(c.id) as comment_count, 124 + row_number() over (order by i.created desc) as row_num 125 + from 126 + issues i 127 + left join 128 + comments c on i.repo_at = c.repo_at and i.issue_id = c.issue_id 129 + where 130 + i.repo_at = ? and i.open = ? 131 + group by 132 + i.id, i.owner_did, i.issue_id, i.created, i.title, i.body, i.open 133 + ) 134 + select 135 + owner_did, 136 + issue_id, 137 + created, 138 + title, 139 + body, 140 + open, 141 + comment_count 142 + from 143 + numbered_issue 144 + where 145 + row_num between ? and ?`, 146 + repoAt, openValue, page.Offset+1, page.Offset+page.Limit) 132 147 if err != nil { 133 148 return nil, err 134 149 }
+173
appview/db/oauth.go
··· 1 + package db 2 + 3 + type OAuthRequest struct { 4 + ID uint 5 + AuthserverIss string 6 + Handle string 7 + State string 8 + Did string 9 + PdsUrl string 10 + PkceVerifier string 11 + DpopAuthserverNonce string 12 + DpopPrivateJwk string 13 + } 14 + 15 + func SaveOAuthRequest(e Execer, oauthRequest OAuthRequest) error { 16 + _, err := e.Exec(` 17 + insert into oauth_requests ( 18 + auth_server_iss, 19 + state, 20 + handle, 21 + did, 22 + pds_url, 23 + pkce_verifier, 24 + dpop_auth_server_nonce, 25 + dpop_private_jwk 26 + ) values (?, ?, ?, ?, ?, ?, ?, ?)`, 27 + oauthRequest.AuthserverIss, 28 + oauthRequest.State, 29 + oauthRequest.Handle, 30 + oauthRequest.Did, 31 + oauthRequest.PdsUrl, 32 + oauthRequest.PkceVerifier, 33 + oauthRequest.DpopAuthserverNonce, 34 + oauthRequest.DpopPrivateJwk, 35 + ) 36 + return err 37 + } 38 + 39 + func GetOAuthRequestByState(e Execer, state string) (OAuthRequest, error) { 40 + var req OAuthRequest 41 + err := e.QueryRow(` 42 + select 43 + id, 44 + auth_server_iss, 45 + handle, 46 + state, 47 + did, 48 + pds_url, 49 + pkce_verifier, 50 + dpop_auth_server_nonce, 51 + dpop_private_jwk 52 + from oauth_requests 53 + where state = ?`, state).Scan( 54 + &req.ID, 55 + &req.AuthserverIss, 56 + &req.Handle, 57 + &req.State, 58 + &req.Did, 59 + &req.PdsUrl, 60 + &req.PkceVerifier, 61 + &req.DpopAuthserverNonce, 62 + &req.DpopPrivateJwk, 63 + ) 64 + return req, err 65 + } 66 + 67 + func DeleteOAuthRequestByState(e Execer, state string) error { 68 + _, err := e.Exec(` 69 + delete from oauth_requests 70 + where state = ?`, state) 71 + return err 72 + } 73 + 74 + type OAuthSession struct { 75 + ID uint 76 + Handle string 77 + Did string 78 + PdsUrl string 79 + AccessJwt string 80 + RefreshJwt string 81 + AuthServerIss string 82 + DpopPdsNonce string 83 + DpopAuthserverNonce string 84 + DpopPrivateJwk string 85 + Expiry string 86 + } 87 + 88 + func SaveOAuthSession(e Execer, session OAuthSession) error { 89 + _, err := e.Exec(` 90 + insert into oauth_sessions ( 91 + did, 92 + handle, 93 + pds_url, 94 + access_jwt, 95 + refresh_jwt, 96 + auth_server_iss, 97 + dpop_auth_server_nonce, 98 + dpop_private_jwk, 99 + expiry 100 + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?)`, 101 + session.Did, 102 + session.Handle, 103 + session.PdsUrl, 104 + session.AccessJwt, 105 + session.RefreshJwt, 106 + session.AuthServerIss, 107 + session.DpopAuthserverNonce, 108 + session.DpopPrivateJwk, 109 + session.Expiry, 110 + ) 111 + return err 112 + } 113 + 114 + func RefreshOAuthSession(e Execer, did string, accessJwt, refreshJwt, expiry string) error { 115 + _, err := e.Exec(` 116 + update oauth_sessions 117 + set access_jwt = ?, refresh_jwt = ?, expiry = ? 118 + where did = ?`, 119 + accessJwt, 120 + refreshJwt, 121 + expiry, 122 + did, 123 + ) 124 + return err 125 + } 126 + 127 + func GetOAuthSessionByDid(e Execer, did string) (*OAuthSession, error) { 128 + var session OAuthSession 129 + err := e.QueryRow(` 130 + select 131 + id, 132 + did, 133 + handle, 134 + pds_url, 135 + access_jwt, 136 + refresh_jwt, 137 + auth_server_iss, 138 + dpop_auth_server_nonce, 139 + dpop_private_jwk, 140 + expiry 141 + from oauth_sessions 142 + where did = ?`, did).Scan( 143 + &session.ID, 144 + &session.Did, 145 + &session.Handle, 146 + &session.PdsUrl, 147 + &session.AccessJwt, 148 + &session.RefreshJwt, 149 + &session.AuthServerIss, 150 + &session.DpopAuthserverNonce, 151 + &session.DpopPrivateJwk, 152 + &session.Expiry, 153 + ) 154 + return &session, err 155 + } 156 + 157 + func DeleteOAuthSessionByDid(e Execer, did string) error { 158 + _, err := e.Exec(` 159 + delete from oauth_sessions 160 + where did = ?`, did) 161 + return err 162 + } 163 + 164 + func UpdateDpopPdsNonce(e Execer, did string, dpopPdsNonce string) error { 165 + _, err := e.Exec(` 166 + update oauth_sessions 167 + set dpop_pds_nonce = ? 168 + where did = ?`, 169 + dpopPdsNonce, 170 + did, 171 + ) 172 + return err 173 + }
+370 -4
appview/db/profile.go
··· 1 1 package db 2 2 3 3 import ( 4 + "database/sql" 4 5 "fmt" 6 + "log" 7 + "net/url" 8 + "slices" 9 + "strings" 5 10 "time" 11 + 12 + "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.sh/tangled.sh/core/api/tangled" 6 14 ) 7 15 8 16 type RepoEvent struct { ··· 81 89 Merged int 82 90 } 83 91 84 - const TimeframeMonths = 3 92 + const TimeframeMonths = 7 85 93 86 94 func MakeProfileTimeline(e Execer, forDid string) (*ProfileTimeline, error) { 87 95 timeline := ProfileTimeline{ ··· 99 107 for _, pull := range pulls { 100 108 pullMonth := pull.Created.Month() 101 109 102 - if currentMonth-pullMonth > TimeframeMonths { 110 + if currentMonth-pullMonth >= TimeframeMonths { 103 111 // shouldn't happen; but times are weird 104 112 continue 105 113 } ··· 118 126 for _, issue := range issues { 119 127 issueMonth := issue.Created.Month() 120 128 121 - if currentMonth-issueMonth > TimeframeMonths { 129 + if currentMonth-issueMonth >= TimeframeMonths { 122 130 // shouldn't happen; but times are weird 123 131 continue 124 132 } ··· 146 154 147 155 repoMonth := repo.Created.Month() 148 156 149 - if currentMonth-repoMonth > TimeframeMonths { 157 + if currentMonth-repoMonth >= TimeframeMonths { 150 158 // shouldn't happen; but times are weird 151 159 continue 152 160 } ··· 162 170 163 171 return &timeline, nil 164 172 } 173 + 174 + type Profile struct { 175 + // ids 176 + ID int 177 + Did string 178 + 179 + // data 180 + Description string 181 + IncludeBluesky bool 182 + Location string 183 + Links [5]string 184 + Stats [2]VanityStat 185 + PinnedRepos [6]syntax.ATURI 186 + } 187 + 188 + func (p Profile) IsLinksEmpty() bool { 189 + for _, l := range p.Links { 190 + if l != "" { 191 + return false 192 + } 193 + } 194 + return true 195 + } 196 + 197 + func (p Profile) IsStatsEmpty() bool { 198 + for _, s := range p.Stats { 199 + if s.Kind != "" { 200 + return false 201 + } 202 + } 203 + return true 204 + } 205 + 206 + func (p Profile) IsPinnedReposEmpty() bool { 207 + for _, r := range p.PinnedRepos { 208 + if r != "" { 209 + return false 210 + } 211 + } 212 + return true 213 + } 214 + 215 + type VanityStatKind string 216 + 217 + const ( 218 + VanityStatMergedPRCount VanityStatKind = "merged-pull-request-count" 219 + VanityStatClosedPRCount VanityStatKind = "closed-pull-request-count" 220 + VanityStatOpenPRCount VanityStatKind = "open-pull-request-count" 221 + VanityStatOpenIssueCount VanityStatKind = "open-issue-count" 222 + VanityStatClosedIssueCount VanityStatKind = "closed-issue-count" 223 + VanityStatRepositoryCount VanityStatKind = "repository-count" 224 + ) 225 + 226 + func (v VanityStatKind) String() string { 227 + switch v { 228 + case VanityStatMergedPRCount: 229 + return "Merged PRs" 230 + case VanityStatClosedPRCount: 231 + return "Closed PRs" 232 + case VanityStatOpenPRCount: 233 + return "Open PRs" 234 + case VanityStatOpenIssueCount: 235 + return "Open Issues" 236 + case VanityStatClosedIssueCount: 237 + return "Closed Issues" 238 + case VanityStatRepositoryCount: 239 + return "Repositories" 240 + } 241 + return "" 242 + } 243 + 244 + type VanityStat struct { 245 + Kind VanityStatKind 246 + Value uint64 247 + } 248 + 249 + func (p *Profile) ProfileAt() syntax.ATURI { 250 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.ActorProfileNSID, "self")) 251 + } 252 + 253 + func UpsertProfile(tx *sql.Tx, profile *Profile) error { 254 + defer tx.Rollback() 255 + 256 + // update links 257 + _, err := tx.Exec(`delete from profile_links where did = ?`, profile.Did) 258 + if err != nil { 259 + return err 260 + } 261 + // update vanity stats 262 + _, err = tx.Exec(`delete from profile_stats where did = ?`, profile.Did) 263 + if err != nil { 264 + return err 265 + } 266 + 267 + // update pinned repos 268 + _, err = tx.Exec(`delete from profile_pinned_repositories where did = ?`, profile.Did) 269 + if err != nil { 270 + return err 271 + } 272 + 273 + includeBskyValue := 0 274 + if profile.IncludeBluesky { 275 + includeBskyValue = 1 276 + } 277 + 278 + _, err = tx.Exec( 279 + `insert or replace into profile ( 280 + did, 281 + description, 282 + include_bluesky, 283 + location 284 + ) 285 + values (?, ?, ?, ?)`, 286 + profile.Did, 287 + profile.Description, 288 + includeBskyValue, 289 + profile.Location, 290 + ) 291 + 292 + if err != nil { 293 + log.Println("profile", "err", err) 294 + return err 295 + } 296 + 297 + for _, link := range profile.Links { 298 + if link == "" { 299 + continue 300 + } 301 + 302 + _, err := tx.Exec( 303 + `insert into profile_links (did, link) values (?, ?)`, 304 + profile.Did, 305 + link, 306 + ) 307 + 308 + if err != nil { 309 + log.Println("profile_links", "err", err) 310 + return err 311 + } 312 + } 313 + 314 + for _, v := range profile.Stats { 315 + if v.Kind == "" { 316 + continue 317 + } 318 + 319 + _, err := tx.Exec( 320 + `insert into profile_stats (did, kind) values (?, ?)`, 321 + profile.Did, 322 + v.Kind, 323 + ) 324 + 325 + if err != nil { 326 + log.Println("profile_stats", "err", err) 327 + return err 328 + } 329 + } 330 + 331 + for _, pin := range profile.PinnedRepos { 332 + if pin == "" { 333 + continue 334 + } 335 + 336 + _, err := tx.Exec( 337 + `insert into profile_pinned_repositories (did, at_uri) values (?, ?)`, 338 + profile.Did, 339 + pin, 340 + ) 341 + 342 + if err != nil { 343 + log.Println("profile_pinned_repositories", "err", err) 344 + return err 345 + } 346 + } 347 + 348 + return tx.Commit() 349 + } 350 + 351 + func GetProfile(e Execer, did string) (*Profile, error) { 352 + var profile Profile 353 + profile.Did = did 354 + 355 + includeBluesky := 0 356 + err := e.QueryRow( 357 + `select description, include_bluesky, location from profile where did = ?`, 358 + did, 359 + ).Scan(&profile.Description, &includeBluesky, &profile.Location) 360 + if err == sql.ErrNoRows { 361 + profile := Profile{} 362 + profile.Did = did 363 + return &profile, nil 364 + } 365 + 366 + if err != nil { 367 + return nil, err 368 + } 369 + 370 + if includeBluesky != 0 { 371 + profile.IncludeBluesky = true 372 + } 373 + 374 + rows, err := e.Query(`select link from profile_links where did = ?`, did) 375 + if err != nil { 376 + return nil, err 377 + } 378 + defer rows.Close() 379 + i := 0 380 + for rows.Next() { 381 + if err := rows.Scan(&profile.Links[i]); err != nil { 382 + return nil, err 383 + } 384 + i++ 385 + } 386 + 387 + rows, err = e.Query(`select kind from profile_stats where did = ?`, did) 388 + if err != nil { 389 + return nil, err 390 + } 391 + defer rows.Close() 392 + i = 0 393 + for rows.Next() { 394 + if err := rows.Scan(&profile.Stats[i].Kind); err != nil { 395 + return nil, err 396 + } 397 + value, err := GetVanityStat(e, profile.Did, profile.Stats[i].Kind) 398 + if err != nil { 399 + return nil, err 400 + } 401 + profile.Stats[i].Value = value 402 + i++ 403 + } 404 + 405 + rows, err = e.Query(`select at_uri from profile_pinned_repositories where did = ?`, did) 406 + if err != nil { 407 + return nil, err 408 + } 409 + defer rows.Close() 410 + i = 0 411 + for rows.Next() { 412 + if err := rows.Scan(&profile.PinnedRepos[i]); err != nil { 413 + return nil, err 414 + } 415 + i++ 416 + } 417 + 418 + return &profile, nil 419 + } 420 + 421 + func GetVanityStat(e Execer, did string, stat VanityStatKind) (uint64, error) { 422 + query := "" 423 + var args []any 424 + switch stat { 425 + case VanityStatMergedPRCount: 426 + query = `select count(id) from pulls where owner_did = ? and state = ?` 427 + args = append(args, did, PullMerged) 428 + case VanityStatClosedPRCount: 429 + query = `select count(id) from pulls where owner_did = ? and state = ?` 430 + args = append(args, did, PullClosed) 431 + case VanityStatOpenPRCount: 432 + query = `select count(id) from pulls where owner_did = ? and state = ?` 433 + args = append(args, did, PullOpen) 434 + case VanityStatOpenIssueCount: 435 + query = `select count(id) from issues where owner_did = ? and open = 1` 436 + args = append(args, did) 437 + case VanityStatClosedIssueCount: 438 + query = `select count(id) from issues where owner_did = ? and open = 0` 439 + args = append(args, did) 440 + case VanityStatRepositoryCount: 441 + query = `select count(id) from repos where did = ?` 442 + args = append(args, did) 443 + } 444 + 445 + var result uint64 446 + err := e.QueryRow(query, args...).Scan(&result) 447 + if err != nil { 448 + return 0, err 449 + } 450 + 451 + return result, nil 452 + } 453 + 454 + func ValidateProfile(e Execer, profile *Profile) error { 455 + // ensure description is not too long 456 + if len(profile.Description) > 256 { 457 + return fmt.Errorf("Entered bio is too long.") 458 + } 459 + 460 + // ensure description is not too long 461 + if len(profile.Location) > 40 { 462 + return fmt.Errorf("Entered location is too long.") 463 + } 464 + 465 + // ensure links are in order 466 + err := validateLinks(profile) 467 + if err != nil { 468 + return err 469 + } 470 + 471 + // ensure all pinned repos are either own repos or collaborating repos 472 + repos, err := GetAllReposByDid(e, profile.Did) 473 + if err != nil { 474 + log.Printf("getting repos for %s: %s", profile.Did, err) 475 + } 476 + 477 + collaboratingRepos, err := CollaboratingIn(e, profile.Did) 478 + if err != nil { 479 + log.Printf("getting collaborating repos for %s: %s", profile.Did, err) 480 + } 481 + 482 + var validRepos []syntax.ATURI 483 + for _, r := range repos { 484 + validRepos = append(validRepos, r.RepoAt()) 485 + } 486 + for _, r := range collaboratingRepos { 487 + validRepos = append(validRepos, r.RepoAt()) 488 + } 489 + 490 + for _, pinned := range profile.PinnedRepos { 491 + if pinned == "" { 492 + continue 493 + } 494 + if !slices.Contains(validRepos, pinned) { 495 + return fmt.Errorf("Invalid pinned repo: `%s, does not belong to own or collaborating repos", pinned) 496 + } 497 + } 498 + 499 + return nil 500 + } 501 + 502 + func validateLinks(profile *Profile) error { 503 + for i, link := range profile.Links { 504 + if link == "" { 505 + continue 506 + } 507 + 508 + parsedURL, err := url.Parse(link) 509 + if err != nil { 510 + return fmt.Errorf("Invalid URL '%s': %v\n", link, err) 511 + } 512 + 513 + if parsedURL.Scheme == "" { 514 + if strings.HasPrefix(link, "//") { 515 + profile.Links[i] = "https:" + link 516 + } else { 517 + profile.Links[i] = "https://" + link 518 + } 519 + continue 520 + } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { 521 + return fmt.Errorf("Warning: URL '%s' has unusual scheme: %s\n", link, parsedURL.Scheme) 522 + } 523 + 524 + // catch relative paths 525 + if parsedURL.Host == "" { 526 + return fmt.Errorf("Warning: URL '%s' appears to be a relative path\n", link) 527 + } 528 + } 529 + return nil 530 + }
+9 -1
appview/db/pubkeys.go
··· 13 13 return err 14 14 } 15 15 16 - func RemovePublicKey(e Execer, did, name, key string) error { 16 + func DeletePublicKey(e Execer, did, name, key string) error { 17 17 _, err := e.Exec(` 18 18 delete from public_keys 19 19 where did = ? and name = ? and key = ?`, 20 20 did, name, key) 21 + return err 22 + } 23 + 24 + func DeletePublicKeyByRkey(e Execer, did, rkey string) error { 25 + _, err := e.Exec(` 26 + delete from public_keys 27 + where did = ? and rkey = ?`, 28 + did, rkey) 21 29 return err 22 30 } 23 31
+23 -24
appview/db/pulls.go
··· 10 10 11 11 "github.com/bluekeyes/go-gitdiff/gitdiff" 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 + "tangled.sh/tangled.sh/core/api/tangled" 13 14 "tangled.sh/tangled.sh/core/patchutil" 14 15 "tangled.sh/tangled.sh/core/types" 15 16 ) ··· 54 55 RepoAt syntax.ATURI 55 56 OwnerDid string 56 57 Rkey string 57 - PullAt syntax.ATURI 58 58 59 59 // content 60 60 Title string ··· 118 118 func (p *Pull) LatestPatch() string { 119 119 latestSubmission := p.Submissions[p.LastRoundNumber()] 120 120 return latestSubmission.Patch 121 + } 122 + 123 + func (p *Pull) PullAt() syntax.ATURI { 124 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.OwnerDid, tangled.RepoPullNSID, p.Rkey)) 121 125 } 122 126 123 127 func (p *Pull) LastRoundNumber() int { ··· 231 235 } 232 236 233 237 func NewPull(tx *sql.Tx, pull *Pull) error { 234 - defer tx.Rollback() 235 - 236 238 _, err := tx.Exec(` 237 239 insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 238 240 values (?, 1) ··· 287 289 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 288 290 values (?, ?, ?, ?, ?) 289 291 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 290 - if err != nil { 291 - return err 292 - } 293 - 294 - if err := tx.Commit(); err != nil { 295 - return err 296 - } 297 - 298 - return nil 299 - } 300 - 301 - func SetPullAt(e Execer, repoAt syntax.ATURI, pullId int, pullAt string) error { 302 - _, err := e.Exec(`update pulls set pull_at = ? where repo_at = ? and pull_id = ?`, pullAt, repoAt, pullId) 303 292 return err 304 293 } 305 294 306 - func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (string, error) { 307 - var pullAt string 308 - err := e.QueryRow(`select pull_at from pulls where repo_at = ? and pull_id = ?`, repoAt, pullId).Scan(&pullAt) 309 - return pullAt, err 295 + func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) { 296 + pull, err := GetPull(e, repoAt, pullId) 297 + if err != nil { 298 + return "", err 299 + } 300 + return pull.PullAt(), err 310 301 } 311 302 312 303 func NextPullId(e Execer, repoAt syntax.ATURI) (int, error) { ··· 326 317 title, 327 318 state, 328 319 target_branch, 329 - pull_at, 330 320 body, 331 321 rkey, 332 322 source_branch, ··· 351 341 &pull.Title, 352 342 &pull.State, 353 343 &pull.TargetBranch, 354 - &pull.PullAt, 355 344 &pull.Body, 356 345 &pull.Rkey, 357 346 &sourceBranch, ··· 487 476 title, 488 477 state, 489 478 target_branch, 490 - pull_at, 491 479 repo_at, 492 480 body, 493 481 rkey, ··· 510 498 &pull.Title, 511 499 &pull.State, 512 500 &pull.TargetBranch, 513 - &pull.PullAt, 514 501 &pull.RepoAt, 515 502 &pull.Body, 516 503 &pull.Rkey, ··· 652 639 } 653 640 if err = commentsRows.Err(); err != nil { 654 641 return nil, err 642 + } 643 + 644 + var pullSourceRepo *Repo 645 + if pull.PullSource != nil { 646 + if pull.PullSource.RepoAt != nil { 647 + pullSourceRepo, err = GetRepoByAtUri(e, pull.PullSource.RepoAt.String()) 648 + if err != nil { 649 + log.Printf("failed to get repo by at uri: %v", err) 650 + } else { 651 + pull.PullSource.Repo = pullSourceRepo 652 + } 653 + } 655 654 } 656 655 657 656 pull.Submissions = make([]*PullSubmission, len(submissionsMap))
+12
appview/db/repos.go
··· 2 2 3 3 import ( 4 4 "database/sql" 5 + "fmt" 5 6 "time" 6 7 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 + securejoin "github.com/cyphar/filepath-securejoin" 10 + "tangled.sh/tangled.sh/core/api/tangled" 8 11 ) 9 12 10 13 type Repo struct { ··· 21 24 22 25 // optional 23 26 Source string 27 + } 28 + 29 + func (r Repo) RepoAt() syntax.ATURI { 30 + return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey)) 31 + } 32 + 33 + func (r Repo) DidSlashRepo() string { 34 + p, _ := securejoin.SecureJoin(r.Did, r.Name) 35 + return p 24 36 } 25 37 26 38 func GetAllRepos(e Execer, limit int) ([]Repo, error) {
+6
appview/db/star.go
··· 69 69 return err 70 70 } 71 71 72 + // Remove a star 73 + func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error { 74 + _, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey) 75 + return err 76 + } 77 + 72 78 func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) { 73 79 stars := 0 74 80 err := e.QueryRow(
+62
appview/filetree/filetree.go
··· 1 + package filetree 2 + 3 + import ( 4 + "path/filepath" 5 + "sort" 6 + "strings" 7 + ) 8 + 9 + type FileTreeNode struct { 10 + Name string 11 + Path string 12 + IsDirectory bool 13 + Children map[string]*FileTreeNode 14 + } 15 + 16 + // NewNode creates a new node 17 + func newNode(name, path string, isDir bool) *FileTreeNode { 18 + return &FileTreeNode{ 19 + Name: name, 20 + Path: path, 21 + IsDirectory: isDir, 22 + Children: make(map[string]*FileTreeNode), 23 + } 24 + } 25 + 26 + func FileTree(files []string) *FileTreeNode { 27 + rootNode := newNode("", "", true) 28 + 29 + sort.Strings(files) 30 + 31 + for _, file := range files { 32 + if file == "" { 33 + continue 34 + } 35 + 36 + parts := strings.Split(filepath.Clean(file), "/") 37 + if len(parts) == 0 { 38 + continue 39 + } 40 + 41 + currentNode := rootNode 42 + currentPath := "" 43 + 44 + for i, part := range parts { 45 + if currentPath == "" { 46 + currentPath = part 47 + } else { 48 + currentPath = filepath.Join(currentPath, part) 49 + } 50 + 51 + isDir := i < len(parts)-1 52 + 53 + if _, exists := currentNode.Children[part]; !exists { 54 + currentNode.Children[part] = newNode(part, currentPath, isDir) 55 + } 56 + 57 + currentNode = currentNode.Children[part] 58 + } 59 + } 60 + 61 + return rootNode 62 + }
+287
appview/ingester.go
··· 1 + package appview 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "log" 8 + "time" 9 + 10 + "github.com/bluesky-social/indigo/atproto/syntax" 11 + "github.com/bluesky-social/jetstream/pkg/models" 12 + "github.com/go-git/go-git/v5/plumbing" 13 + "github.com/ipfs/go-cid" 14 + "tangled.sh/tangled.sh/core/api/tangled" 15 + "tangled.sh/tangled.sh/core/appview/db" 16 + "tangled.sh/tangled.sh/core/rbac" 17 + ) 18 + 19 + type Ingester func(ctx context.Context, e *models.Event) error 20 + 21 + func Ingest(d db.DbWrapper, enforcer *rbac.Enforcer) Ingester { 22 + return func(ctx context.Context, e *models.Event) error { 23 + var err error 24 + defer func() { 25 + eventTime := e.TimeUS 26 + lastTimeUs := eventTime + 1 27 + if err := d.SaveLastTimeUs(lastTimeUs); err != nil { 28 + err = fmt.Errorf("(deferred) failed to save last time us: %w", err) 29 + } 30 + }() 31 + 32 + if e.Kind != models.EventKindCommit { 33 + return nil 34 + } 35 + 36 + switch e.Commit.Collection { 37 + case tangled.GraphFollowNSID: 38 + ingestFollow(&d, e) 39 + case tangled.FeedStarNSID: 40 + ingestStar(&d, e) 41 + case tangled.PublicKeyNSID: 42 + ingestPublicKey(&d, e) 43 + case tangled.RepoArtifactNSID: 44 + ingestArtifact(&d, e, enforcer) 45 + case tangled.ActorProfileNSID: 46 + ingestProfile(&d, e) 47 + } 48 + 49 + return err 50 + } 51 + } 52 + 53 + func ingestStar(d *db.DbWrapper, e *models.Event) error { 54 + var err error 55 + did := e.Did 56 + 57 + switch e.Commit.Operation { 58 + case models.CommitOperationCreate, models.CommitOperationUpdate: 59 + var subjectUri syntax.ATURI 60 + 61 + raw := json.RawMessage(e.Commit.Record) 62 + record := tangled.FeedStar{} 63 + err := json.Unmarshal(raw, &record) 64 + if err != nil { 65 + log.Println("invalid record") 66 + return err 67 + } 68 + 69 + subjectUri, err = syntax.ParseATURI(record.Subject) 70 + if err != nil { 71 + log.Println("invalid record") 72 + return err 73 + } 74 + err = db.AddStar(d, did, subjectUri, e.Commit.RKey) 75 + case models.CommitOperationDelete: 76 + err = db.DeleteStarByRkey(d, did, e.Commit.RKey) 77 + } 78 + 79 + if err != nil { 80 + return fmt.Errorf("failed to %s star record: %w", e.Commit.Operation, err) 81 + } 82 + 83 + return nil 84 + } 85 + 86 + func ingestFollow(d *db.DbWrapper, e *models.Event) error { 87 + var err error 88 + did := e.Did 89 + 90 + switch e.Commit.Operation { 91 + case models.CommitOperationCreate, models.CommitOperationUpdate: 92 + raw := json.RawMessage(e.Commit.Record) 93 + record := tangled.GraphFollow{} 94 + err = json.Unmarshal(raw, &record) 95 + if err != nil { 96 + log.Println("invalid record") 97 + return err 98 + } 99 + 100 + subjectDid := record.Subject 101 + err = db.AddFollow(d, did, subjectDid, e.Commit.RKey) 102 + case models.CommitOperationDelete: 103 + err = db.DeleteFollowByRkey(d, did, e.Commit.RKey) 104 + } 105 + 106 + if err != nil { 107 + return fmt.Errorf("failed to %s follow record: %w", e.Commit.Operation, err) 108 + } 109 + 110 + return nil 111 + } 112 + 113 + func ingestPublicKey(d *db.DbWrapper, e *models.Event) error { 114 + did := e.Did 115 + var err error 116 + 117 + switch e.Commit.Operation { 118 + case models.CommitOperationCreate, models.CommitOperationUpdate: 119 + log.Println("processing add of pubkey") 120 + raw := json.RawMessage(e.Commit.Record) 121 + record := tangled.PublicKey{} 122 + err = json.Unmarshal(raw, &record) 123 + if err != nil { 124 + log.Printf("invalid record: %s", err) 125 + return err 126 + } 127 + 128 + name := record.Name 129 + key := record.Key 130 + err = db.AddPublicKey(d, did, name, key, e.Commit.RKey) 131 + case models.CommitOperationDelete: 132 + log.Println("processing delete of pubkey") 133 + err = db.DeletePublicKeyByRkey(d, did, e.Commit.RKey) 134 + } 135 + 136 + if err != nil { 137 + return fmt.Errorf("failed to %s pubkey record: %w", e.Commit.Operation, err) 138 + } 139 + 140 + return nil 141 + } 142 + 143 + func ingestArtifact(d *db.DbWrapper, e *models.Event, enforcer *rbac.Enforcer) error { 144 + did := e.Did 145 + var err error 146 + 147 + switch e.Commit.Operation { 148 + case models.CommitOperationCreate, models.CommitOperationUpdate: 149 + raw := json.RawMessage(e.Commit.Record) 150 + record := tangled.RepoArtifact{} 151 + err = json.Unmarshal(raw, &record) 152 + if err != nil { 153 + log.Printf("invalid record: %s", err) 154 + return err 155 + } 156 + 157 + repoAt, err := syntax.ParseATURI(record.Repo) 158 + if err != nil { 159 + return err 160 + } 161 + 162 + repo, err := db.GetRepoByAtUri(d, repoAt.String()) 163 + if err != nil { 164 + return err 165 + } 166 + 167 + ok, err := enforcer.E.Enforce(did, repo.Knot, repo.DidSlashRepo(), "repo:push") 168 + if err != nil || !ok { 169 + return err 170 + } 171 + 172 + createdAt, err := time.Parse(time.RFC3339, record.CreatedAt) 173 + if err != nil { 174 + createdAt = time.Now() 175 + } 176 + 177 + artifact := db.Artifact{ 178 + Did: did, 179 + Rkey: e.Commit.RKey, 180 + RepoAt: repoAt, 181 + Tag: plumbing.Hash(record.Tag), 182 + CreatedAt: createdAt, 183 + BlobCid: cid.Cid(record.Artifact.Ref), 184 + Name: record.Name, 185 + Size: uint64(record.Artifact.Size), 186 + MimeType: record.Artifact.MimeType, 187 + } 188 + 189 + err = db.AddArtifact(d, artifact) 190 + case models.CommitOperationDelete: 191 + err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey)) 192 + } 193 + 194 + if err != nil { 195 + return fmt.Errorf("failed to %s artifact record: %w", e.Commit.Operation, err) 196 + } 197 + 198 + return nil 199 + } 200 + 201 + func ingestProfile(d *db.DbWrapper, e *models.Event) error { 202 + did := e.Did 203 + var err error 204 + 205 + if e.Commit.RKey != "self" { 206 + return fmt.Errorf("ingestProfile only ingests `self` record") 207 + } 208 + 209 + switch e.Commit.Operation { 210 + case models.CommitOperationCreate, models.CommitOperationUpdate: 211 + raw := json.RawMessage(e.Commit.Record) 212 + record := tangled.ActorProfile{} 213 + err = json.Unmarshal(raw, &record) 214 + if err != nil { 215 + log.Printf("invalid record: %s", err) 216 + return err 217 + } 218 + 219 + description := "" 220 + if record.Description != nil { 221 + description = *record.Description 222 + } 223 + 224 + includeBluesky := record.Bluesky 225 + 226 + location := "" 227 + if record.Location != nil { 228 + location = *record.Location 229 + } 230 + 231 + var links [5]string 232 + for i, l := range record.Links { 233 + if i < 5 { 234 + links[i] = l 235 + } 236 + } 237 + 238 + var stats [2]db.VanityStat 239 + for i, s := range record.Stats { 240 + if i < 2 { 241 + stats[i].Kind = db.VanityStatKind(s) 242 + } 243 + } 244 + 245 + var pinned [6]syntax.ATURI 246 + for i, r := range record.PinnedRepositories { 247 + if i < 6 { 248 + pinned[i] = syntax.ATURI(r) 249 + } 250 + } 251 + 252 + profile := db.Profile{ 253 + Did: did, 254 + Description: description, 255 + IncludeBluesky: includeBluesky, 256 + Location: location, 257 + Links: links, 258 + Stats: stats, 259 + PinnedRepos: pinned, 260 + } 261 + 262 + ddb, ok := d.Execer.(*db.DB) 263 + if !ok { 264 + return fmt.Errorf("failed to index profile record, invalid db cast") 265 + } 266 + 267 + tx, err := ddb.Begin() 268 + if err != nil { 269 + return fmt.Errorf("failed to start transaction") 270 + } 271 + 272 + err = db.ValidateProfile(tx, &profile) 273 + if err != nil { 274 + return fmt.Errorf("invalid profile record") 275 + } 276 + 277 + err = db.UpsertProfile(tx, &profile) 278 + case models.CommitOperationDelete: 279 + err = db.DeleteArtifact(d, db.Filter("did", did), db.Filter("rkey", e.Commit.RKey)) 280 + } 281 + 282 + if err != nil { 283 + return fmt.Errorf("failed to %s profile record: %w", e.Commit.Operation, err) 284 + } 285 + 286 + return nil 287 + }
+489
appview/knotclient/signer.go
··· 1 + package knotclient 2 + 3 + import ( 4 + "bytes" 5 + "crypto/hmac" 6 + "crypto/sha256" 7 + "encoding/hex" 8 + "encoding/json" 9 + "fmt" 10 + "io" 11 + "log" 12 + "net/http" 13 + "net/url" 14 + "strconv" 15 + "time" 16 + 17 + "tangled.sh/tangled.sh/core/types" 18 + ) 19 + 20 + type SignerTransport struct { 21 + Secret string 22 + } 23 + 24 + func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) { 25 + timestamp := time.Now().Format(time.RFC3339) 26 + mac := hmac.New(sha256.New, []byte(s.Secret)) 27 + message := req.Method + req.URL.Path + timestamp 28 + mac.Write([]byte(message)) 29 + signature := hex.EncodeToString(mac.Sum(nil)) 30 + req.Header.Set("X-Signature", signature) 31 + req.Header.Set("X-Timestamp", timestamp) 32 + return http.DefaultTransport.RoundTrip(req) 33 + } 34 + 35 + type SignedClient struct { 36 + Secret string 37 + Url *url.URL 38 + client *http.Client 39 + } 40 + 41 + func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) { 42 + client := &http.Client{ 43 + Timeout: 5 * time.Second, 44 + Transport: SignerTransport{ 45 + Secret: secret, 46 + }, 47 + } 48 + 49 + scheme := "https" 50 + if dev { 51 + scheme = "http" 52 + } 53 + url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 54 + if err != nil { 55 + return nil, err 56 + } 57 + 58 + signedClient := &SignedClient{ 59 + Secret: secret, 60 + client: client, 61 + Url: url, 62 + } 63 + 64 + return signedClient, nil 65 + } 66 + 67 + func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) { 68 + return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body)) 69 + } 70 + 71 + func (s *SignedClient) Init(did string) (*http.Response, error) { 72 + const ( 73 + Method = "POST" 74 + Endpoint = "/init" 75 + ) 76 + 77 + body, _ := json.Marshal(map[string]any{ 78 + "did": did, 79 + }) 80 + 81 + req, err := s.newRequest(Method, Endpoint, body) 82 + if err != nil { 83 + return nil, err 84 + } 85 + 86 + return s.client.Do(req) 87 + } 88 + 89 + func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) { 90 + const ( 91 + Method = "PUT" 92 + Endpoint = "/repo/new" 93 + ) 94 + 95 + body, _ := json.Marshal(map[string]any{ 96 + "did": did, 97 + "name": repoName, 98 + "default_branch": defaultBranch, 99 + }) 100 + 101 + req, err := s.newRequest(Method, Endpoint, body) 102 + if err != nil { 103 + return nil, err 104 + } 105 + 106 + return s.client.Do(req) 107 + } 108 + 109 + func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) { 110 + const ( 111 + Method = "POST" 112 + Endpoint = "/repo/fork" 113 + ) 114 + 115 + body, _ := json.Marshal(map[string]any{ 116 + "did": ownerDid, 117 + "source": source, 118 + "name": name, 119 + }) 120 + 121 + req, err := s.newRequest(Method, Endpoint, body) 122 + if err != nil { 123 + return nil, err 124 + } 125 + 126 + return s.client.Do(req) 127 + } 128 + 129 + func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) { 130 + const ( 131 + Method = "DELETE" 132 + Endpoint = "/repo" 133 + ) 134 + 135 + body, _ := json.Marshal(map[string]any{ 136 + "did": did, 137 + "name": repoName, 138 + }) 139 + 140 + req, err := s.newRequest(Method, Endpoint, body) 141 + if err != nil { 142 + return nil, err 143 + } 144 + 145 + return s.client.Do(req) 146 + } 147 + 148 + func (s *SignedClient) AddMember(did string) (*http.Response, error) { 149 + const ( 150 + Method = "PUT" 151 + Endpoint = "/member/add" 152 + ) 153 + 154 + body, _ := json.Marshal(map[string]any{ 155 + "did": did, 156 + }) 157 + 158 + req, err := s.newRequest(Method, Endpoint, body) 159 + if err != nil { 160 + return nil, err 161 + } 162 + 163 + return s.client.Do(req) 164 + } 165 + 166 + func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) { 167 + const ( 168 + Method = "PUT" 169 + ) 170 + endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 171 + 172 + body, _ := json.Marshal(map[string]any{ 173 + "branch": branch, 174 + }) 175 + 176 + req, err := s.newRequest(Method, endpoint, body) 177 + if err != nil { 178 + return nil, err 179 + } 180 + 181 + return s.client.Do(req) 182 + } 183 + 184 + func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) { 185 + const ( 186 + Method = "POST" 187 + ) 188 + endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 189 + 190 + body, _ := json.Marshal(map[string]any{ 191 + "did": memberDid, 192 + }) 193 + 194 + req, err := s.newRequest(Method, endpoint, body) 195 + if err != nil { 196 + return nil, err 197 + } 198 + 199 + return s.client.Do(req) 200 + } 201 + 202 + func (s *SignedClient) Merge( 203 + patch []byte, 204 + ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 205 + ) (*http.Response, error) { 206 + const ( 207 + Method = "POST" 208 + ) 209 + endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 210 + 211 + mr := types.MergeRequest{ 212 + Branch: branch, 213 + CommitMessage: commitMessage, 214 + CommitBody: commitBody, 215 + AuthorName: authorName, 216 + AuthorEmail: authorEmail, 217 + Patch: string(patch), 218 + } 219 + 220 + body, _ := json.Marshal(mr) 221 + 222 + req, err := s.newRequest(Method, endpoint, body) 223 + if err != nil { 224 + return nil, err 225 + } 226 + 227 + return s.client.Do(req) 228 + } 229 + 230 + func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 231 + const ( 232 + Method = "POST" 233 + ) 234 + endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo) 235 + 236 + body, _ := json.Marshal(map[string]any{ 237 + "patch": string(patch), 238 + "branch": branch, 239 + }) 240 + 241 + req, err := s.newRequest(Method, endpoint, body) 242 + if err != nil { 243 + return nil, err 244 + } 245 + 246 + return s.client.Do(req) 247 + } 248 + 249 + func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) { 250 + const ( 251 + Method = "POST" 252 + ) 253 + endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, url.PathEscape(forkBranch), url.PathEscape(remoteBranch)) 254 + 255 + req, err := s.newRequest(Method, endpoint, nil) 256 + if err != nil { 257 + return nil, err 258 + } 259 + 260 + return s.client.Do(req) 261 + } 262 + 263 + type UnsignedClient struct { 264 + Url *url.URL 265 + client *http.Client 266 + } 267 + 268 + func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) { 269 + client := &http.Client{ 270 + Timeout: 5 * time.Second, 271 + } 272 + 273 + scheme := "https" 274 + if dev { 275 + scheme = "http" 276 + } 277 + url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 278 + if err != nil { 279 + return nil, err 280 + } 281 + 282 + unsignedClient := &UnsignedClient{ 283 + client: client, 284 + Url: url, 285 + } 286 + 287 + return unsignedClient, nil 288 + } 289 + 290 + func (us *UnsignedClient) newRequest(method, endpoint string, query url.Values, body []byte) (*http.Request, error) { 291 + reqUrl := us.Url.JoinPath(endpoint) 292 + 293 + // add query parameters 294 + if query != nil { 295 + reqUrl.RawQuery = query.Encode() 296 + } 297 + 298 + return http.NewRequest(method, reqUrl.String(), bytes.NewReader(body)) 299 + } 300 + 301 + func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) { 302 + const ( 303 + Method = "GET" 304 + ) 305 + 306 + endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref) 307 + if ref == "" { 308 + endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName) 309 + } 310 + 311 + req, err := us.newRequest(Method, endpoint, nil, nil) 312 + if err != nil { 313 + return nil, err 314 + } 315 + 316 + return us.client.Do(req) 317 + } 318 + 319 + func (us *UnsignedClient) Log(ownerDid, repoName, ref string, page int) (*http.Response, error) { 320 + const ( 321 + Method = "GET" 322 + ) 323 + 324 + endpoint := fmt.Sprintf("/%s/%s/log/%s", ownerDid, repoName, url.PathEscape(ref)) 325 + 326 + query := url.Values{} 327 + query.Add("page", strconv.Itoa(page)) 328 + query.Add("per_page", strconv.Itoa(60)) 329 + 330 + req, err := us.newRequest(Method, endpoint, query, nil) 331 + if err != nil { 332 + return nil, err 333 + } 334 + 335 + return us.client.Do(req) 336 + } 337 + 338 + func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) { 339 + const ( 340 + Method = "GET" 341 + ) 342 + 343 + endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName) 344 + 345 + req, err := us.newRequest(Method, endpoint, nil, nil) 346 + if err != nil { 347 + return nil, err 348 + } 349 + 350 + return us.client.Do(req) 351 + } 352 + 353 + func (us *UnsignedClient) Tags(ownerDid, repoName string) (*types.RepoTagsResponse, error) { 354 + const ( 355 + Method = "GET" 356 + ) 357 + 358 + endpoint := fmt.Sprintf("/%s/%s/tags", ownerDid, repoName) 359 + 360 + req, err := us.newRequest(Method, endpoint, nil, nil) 361 + if err != nil { 362 + return nil, err 363 + } 364 + 365 + resp, err := us.client.Do(req) 366 + if err != nil { 367 + return nil, err 368 + } 369 + 370 + body, err := io.ReadAll(resp.Body) 371 + if err != nil { 372 + return nil, err 373 + } 374 + 375 + var result types.RepoTagsResponse 376 + err = json.Unmarshal(body, &result) 377 + if err != nil { 378 + return nil, err 379 + } 380 + 381 + return &result, nil 382 + } 383 + 384 + func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) { 385 + const ( 386 + Method = "GET" 387 + ) 388 + 389 + endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch)) 390 + 391 + req, err := us.newRequest(Method, endpoint, nil, nil) 392 + if err != nil { 393 + return nil, err 394 + } 395 + 396 + return us.client.Do(req) 397 + } 398 + 399 + func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*types.RepoDefaultBranchResponse, error) { 400 + const ( 401 + Method = "GET" 402 + ) 403 + 404 + endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 405 + 406 + req, err := us.newRequest(Method, endpoint, nil, nil) 407 + if err != nil { 408 + return nil, err 409 + } 410 + 411 + resp, err := us.client.Do(req) 412 + if err != nil { 413 + return nil, err 414 + } 415 + defer resp.Body.Close() 416 + 417 + var defaultBranch types.RepoDefaultBranchResponse 418 + if err := json.NewDecoder(resp.Body).Decode(&defaultBranch); err != nil { 419 + return nil, err 420 + } 421 + 422 + return &defaultBranch, nil 423 + } 424 + 425 + func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) { 426 + const ( 427 + Method = "GET" 428 + Endpoint = "/capabilities" 429 + ) 430 + 431 + req, err := us.newRequest(Method, Endpoint, nil, nil) 432 + if err != nil { 433 + return nil, err 434 + } 435 + 436 + resp, err := us.client.Do(req) 437 + if err != nil { 438 + return nil, err 439 + } 440 + defer resp.Body.Close() 441 + 442 + var capabilities types.Capabilities 443 + if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil { 444 + return nil, err 445 + } 446 + 447 + return &capabilities, nil 448 + } 449 + 450 + func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) { 451 + const ( 452 + Method = "GET" 453 + ) 454 + 455 + endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2)) 456 + 457 + req, err := us.newRequest(Method, endpoint, nil, nil) 458 + if err != nil { 459 + return nil, fmt.Errorf("Failed to create request.") 460 + } 461 + 462 + compareResp, err := us.client.Do(req) 463 + if err != nil { 464 + return nil, fmt.Errorf("Failed to create request.") 465 + } 466 + defer compareResp.Body.Close() 467 + 468 + switch compareResp.StatusCode { 469 + case 404: 470 + case 400: 471 + return nil, fmt.Errorf("Branch comparisons not supported on this knot.") 472 + } 473 + 474 + respBody, err := io.ReadAll(compareResp.Body) 475 + if err != nil { 476 + log.Println("failed to compare across branches") 477 + return nil, fmt.Errorf("Failed to compare branches.") 478 + } 479 + defer compareResp.Body.Close() 480 + 481 + var formatPatchResponse types.RepoFormatPatchResponse 482 + err = json.Unmarshal(respBody, &formatPatchResponse) 483 + if err != nil { 484 + log.Println("failed to unmarshal format-patch response", err) 485 + return nil, fmt.Errorf("failed to compare branches.") 486 + } 487 + 488 + return &formatPatchResponse, nil 489 + }
+73
appview/middleware/middleware.go
··· 1 + package middleware 2 + 3 + import ( 4 + "context" 5 + "log" 6 + "net/http" 7 + "strconv" 8 + 9 + "tangled.sh/tangled.sh/core/appview/oauth" 10 + "tangled.sh/tangled.sh/core/appview/pagination" 11 + ) 12 + 13 + type Middleware func(http.Handler) http.Handler 14 + 15 + func AuthMiddleware(a *oauth.OAuth) Middleware { 16 + return func(next http.Handler) http.Handler { 17 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 18 + redirectFunc := func(w http.ResponseWriter, r *http.Request) { 19 + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 20 + } 21 + if r.Header.Get("HX-Request") == "true" { 22 + redirectFunc = func(w http.ResponseWriter, _ *http.Request) { 23 + w.Header().Set("HX-Redirect", "/login") 24 + w.WriteHeader(http.StatusOK) 25 + } 26 + } 27 + 28 + _, auth, err := a.GetSession(r) 29 + if err != nil { 30 + log.Printf("not logged in, redirecting") 31 + redirectFunc(w, r) 32 + return 33 + } 34 + 35 + if !auth { 36 + log.Printf("not logged in, redirecting") 37 + redirectFunc(w, r) 38 + return 39 + } 40 + 41 + next.ServeHTTP(w, r) 42 + }) 43 + } 44 + } 45 + 46 + func Paginate(next http.Handler) http.Handler { 47 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 48 + page := pagination.FirstPage() 49 + 50 + offsetVal := r.URL.Query().Get("offset") 51 + if offsetVal != "" { 52 + offset, err := strconv.Atoi(offsetVal) 53 + if err != nil { 54 + log.Println("invalid offset") 55 + } else { 56 + page.Offset = offset 57 + } 58 + } 59 + 60 + limitVal := r.URL.Query().Get("limit") 61 + if limitVal != "" { 62 + limit, err := strconv.Atoi(limitVal) 63 + if err != nil { 64 + log.Println("invalid limit") 65 + } else { 66 + page.Limit = limit 67 + } 68 + } 69 + 70 + ctx := context.WithValue(r.Context(), "page", page) 71 + next.ServeHTTP(w, r.WithContext(ctx)) 72 + }) 73 + }
+24
appview/oauth/client/oauth_client.go
··· 1 + package client 2 + 3 + import ( 4 + oauth "github.com/haileyok/atproto-oauth-golang" 5 + "github.com/haileyok/atproto-oauth-golang/helpers" 6 + ) 7 + 8 + type OAuthClient struct { 9 + *oauth.Client 10 + } 11 + 12 + func NewClient(clientId, clientJwk, redirectUri string) (*OAuthClient, error) { 13 + k, err := helpers.ParseJWKFromBytes([]byte(clientJwk)) 14 + if err != nil { 15 + return nil, err 16 + } 17 + 18 + cli, err := oauth.NewClient(oauth.ClientArgs{ 19 + ClientId: clientId, 20 + ClientJwk: k, 21 + RedirectUri: redirectUri, 22 + }) 23 + return &OAuthClient{cli}, err 24 + }
+309
appview/oauth/handler/handler.go
··· 1 + package oauth 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "log" 7 + "net/http" 8 + "net/url" 9 + "strings" 10 + 11 + "github.com/go-chi/chi/v5" 12 + "github.com/gorilla/sessions" 13 + "github.com/haileyok/atproto-oauth-golang/helpers" 14 + "github.com/lestrrat-go/jwx/v2/jwk" 15 + "tangled.sh/tangled.sh/core/appview" 16 + "tangled.sh/tangled.sh/core/appview/db" 17 + "tangled.sh/tangled.sh/core/appview/knotclient" 18 + "tangled.sh/tangled.sh/core/appview/middleware" 19 + "tangled.sh/tangled.sh/core/appview/oauth" 20 + "tangled.sh/tangled.sh/core/appview/oauth/client" 21 + "tangled.sh/tangled.sh/core/appview/pages" 22 + "tangled.sh/tangled.sh/core/rbac" 23 + ) 24 + 25 + const ( 26 + oauthScope = "atproto transition:generic" 27 + ) 28 + 29 + type OAuthHandler struct { 30 + Config *appview.Config 31 + Pages *pages.Pages 32 + Resolver *appview.Resolver 33 + Db *db.DB 34 + Store *sessions.CookieStore 35 + OAuth *oauth.OAuth 36 + Enforcer *rbac.Enforcer 37 + } 38 + 39 + func (o *OAuthHandler) Router() http.Handler { 40 + r := chi.NewRouter() 41 + 42 + r.Get("/login", o.login) 43 + r.Post("/login", o.login) 44 + 45 + r.With(middleware.AuthMiddleware(o.OAuth)).Post("/logout", o.logout) 46 + 47 + r.Get("/oauth/client-metadata.json", o.clientMetadata) 48 + r.Get("/oauth/jwks.json", o.jwks) 49 + r.Get("/oauth/callback", o.callback) 50 + return r 51 + } 52 + 53 + func (o *OAuthHandler) clientMetadata(w http.ResponseWriter, r *http.Request) { 54 + w.Header().Set("Content-Type", "application/json") 55 + w.WriteHeader(http.StatusOK) 56 + json.NewEncoder(w).Encode(o.OAuth.ClientMetadata()) 57 + } 58 + 59 + func (o *OAuthHandler) jwks(w http.ResponseWriter, r *http.Request) { 60 + jwks := o.Config.OAuth.Jwks 61 + pubKey, err := pubKeyFromJwk(jwks) 62 + if err != nil { 63 + log.Printf("error parsing public key: %v", err) 64 + http.Error(w, err.Error(), http.StatusInternalServerError) 65 + return 66 + } 67 + 68 + response := helpers.CreateJwksResponseObject(pubKey) 69 + 70 + w.Header().Set("Content-Type", "application/json") 71 + w.WriteHeader(http.StatusOK) 72 + json.NewEncoder(w).Encode(response) 73 + } 74 + 75 + func (o *OAuthHandler) login(w http.ResponseWriter, r *http.Request) { 76 + switch r.Method { 77 + case http.MethodGet: 78 + o.Pages.Login(w, pages.LoginParams{}) 79 + case http.MethodPost: 80 + handle := strings.TrimPrefix(r.FormValue("handle"), "@") 81 + 82 + resolved, err := o.Resolver.ResolveIdent(r.Context(), handle) 83 + if err != nil { 84 + log.Println("failed to resolve handle:", err) 85 + o.Pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 86 + return 87 + } 88 + self := o.OAuth.ClientMetadata() 89 + oauthClient, err := client.NewClient( 90 + self.ClientID, 91 + o.Config.OAuth.Jwks, 92 + self.RedirectURIs[0], 93 + ) 94 + 95 + if err != nil { 96 + log.Println("failed to create oauth client:", err) 97 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 98 + return 99 + } 100 + 101 + authServer, err := oauthClient.ResolvePdsAuthServer(r.Context(), resolved.PDSEndpoint()) 102 + if err != nil { 103 + log.Println("failed to resolve auth server:", err) 104 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 105 + return 106 + } 107 + 108 + authMeta, err := oauthClient.FetchAuthServerMetadata(r.Context(), authServer) 109 + if err != nil { 110 + log.Println("failed to fetch auth server metadata:", err) 111 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 112 + return 113 + } 114 + 115 + dpopKey, err := helpers.GenerateKey(nil) 116 + if err != nil { 117 + log.Println("failed to generate dpop key:", err) 118 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 119 + return 120 + } 121 + 122 + dpopKeyJson, err := json.Marshal(dpopKey) 123 + if err != nil { 124 + log.Println("failed to marshal dpop key:", err) 125 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 126 + return 127 + } 128 + 129 + parResp, err := oauthClient.SendParAuthRequest(r.Context(), authServer, authMeta, handle, oauthScope, dpopKey) 130 + if err != nil { 131 + log.Println("failed to send par auth request:", err) 132 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 133 + return 134 + } 135 + 136 + err = db.SaveOAuthRequest(o.Db, db.OAuthRequest{ 137 + Did: resolved.DID.String(), 138 + PdsUrl: resolved.PDSEndpoint(), 139 + Handle: handle, 140 + AuthserverIss: authMeta.Issuer, 141 + PkceVerifier: parResp.PkceVerifier, 142 + DpopAuthserverNonce: parResp.DpopAuthserverNonce, 143 + DpopPrivateJwk: string(dpopKeyJson), 144 + State: parResp.State, 145 + }) 146 + if err != nil { 147 + log.Println("failed to save oauth request:", err) 148 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 149 + return 150 + } 151 + 152 + u, _ := url.Parse(authMeta.AuthorizationEndpoint) 153 + query := url.Values{} 154 + query.Add("client_id", self.ClientID) 155 + query.Add("request_uri", parResp.RequestUri) 156 + u.RawQuery = query.Encode() 157 + o.Pages.HxRedirect(w, u.String()) 158 + } 159 + } 160 + 161 + func (o *OAuthHandler) callback(w http.ResponseWriter, r *http.Request) { 162 + state := r.FormValue("state") 163 + 164 + oauthRequest, err := db.GetOAuthRequestByState(o.Db, state) 165 + if err != nil { 166 + log.Println("failed to get oauth request:", err) 167 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 168 + return 169 + } 170 + 171 + defer func() { 172 + err := db.DeleteOAuthRequestByState(o.Db, state) 173 + if err != nil { 174 + log.Println("failed to delete oauth request for state:", state, err) 175 + } 176 + }() 177 + 178 + error := r.FormValue("error") 179 + errorDescription := r.FormValue("error_description") 180 + if error != "" || errorDescription != "" { 181 + log.Printf("error: %s, %s", error, errorDescription) 182 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 183 + return 184 + } 185 + 186 + code := r.FormValue("code") 187 + if code == "" { 188 + log.Println("missing code for state: ", state) 189 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 190 + return 191 + } 192 + 193 + iss := r.FormValue("iss") 194 + if iss == "" { 195 + log.Println("missing iss for state: ", state) 196 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 197 + return 198 + } 199 + 200 + self := o.OAuth.ClientMetadata() 201 + 202 + oauthClient, err := client.NewClient( 203 + self.ClientID, 204 + o.Config.OAuth.Jwks, 205 + self.RedirectURIs[0], 206 + ) 207 + 208 + if err != nil { 209 + log.Println("failed to create oauth client:", err) 210 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 211 + return 212 + } 213 + 214 + jwk, err := helpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk)) 215 + if err != nil { 216 + log.Println("failed to parse jwk:", err) 217 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 218 + return 219 + } 220 + 221 + tokenResp, err := oauthClient.InitialTokenRequest( 222 + r.Context(), 223 + code, 224 + oauthRequest.AuthserverIss, 225 + oauthRequest.PkceVerifier, 226 + oauthRequest.DpopAuthserverNonce, 227 + jwk, 228 + ) 229 + if err != nil { 230 + log.Println("failed to get token:", err) 231 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 232 + return 233 + } 234 + 235 + if tokenResp.Scope != oauthScope { 236 + log.Println("scope doesn't match:", tokenResp.Scope) 237 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 238 + return 239 + } 240 + 241 + err = o.OAuth.SaveSession(w, r, oauthRequest, tokenResp) 242 + if err != nil { 243 + log.Println("failed to save session:", err) 244 + o.Pages.Notice(w, "login-msg", "Failed to authenticate. Try again later.") 245 + return 246 + } 247 + 248 + log.Println("session saved successfully") 249 + go o.addToDefaultKnot(oauthRequest.Did) 250 + 251 + http.Redirect(w, r, "/", http.StatusFound) 252 + } 253 + 254 + func (o *OAuthHandler) logout(w http.ResponseWriter, r *http.Request) { 255 + err := o.OAuth.ClearSession(r, w) 256 + if err != nil { 257 + log.Println("failed to clear session:", err) 258 + http.Redirect(w, r, "/", http.StatusFound) 259 + return 260 + } 261 + 262 + log.Println("session cleared successfully") 263 + http.Redirect(w, r, "/", http.StatusFound) 264 + } 265 + 266 + func pubKeyFromJwk(jwks string) (jwk.Key, error) { 267 + k, err := helpers.ParseJWKFromBytes([]byte(jwks)) 268 + if err != nil { 269 + return nil, err 270 + } 271 + pubKey, err := k.PublicKey() 272 + if err != nil { 273 + return nil, err 274 + } 275 + return pubKey, nil 276 + } 277 + 278 + func (o *OAuthHandler) addToDefaultKnot(did string) { 279 + defaultKnot := "knot1.tangled.sh" 280 + 281 + log.Printf("adding %s to default knot", did) 282 + err := o.Enforcer.AddMember(defaultKnot, did) 283 + if err != nil { 284 + log.Println("failed to add user to knot1.tangled.sh: ", err) 285 + return 286 + } 287 + err = o.Enforcer.E.SavePolicy() 288 + if err != nil { 289 + log.Println("failed to add user to knot1.tangled.sh: ", err) 290 + return 291 + } 292 + 293 + secret, err := db.GetRegistrationKey(o.Db, defaultKnot) 294 + if err != nil { 295 + log.Println("failed to get registration key for knot1.tangled.sh") 296 + return 297 + } 298 + signedClient, err := knotclient.NewSignedClient(defaultKnot, secret, o.Config.Core.Dev) 299 + resp, err := signedClient.AddMember(did) 300 + if err != nil { 301 + log.Println("failed to add user to knot1.tangled.sh: ", err) 302 + return 303 + } 304 + 305 + if resp.StatusCode != http.StatusNoContent { 306 + log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 307 + return 308 + } 309 + }
+268
appview/oauth/oauth.go
··· 1 + package oauth 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net/http" 7 + "net/url" 8 + "time" 9 + 10 + "github.com/gorilla/sessions" 11 + oauth "github.com/haileyok/atproto-oauth-golang" 12 + "github.com/haileyok/atproto-oauth-golang/helpers" 13 + "tangled.sh/tangled.sh/core/appview" 14 + "tangled.sh/tangled.sh/core/appview/db" 15 + "tangled.sh/tangled.sh/core/appview/oauth/client" 16 + xrpc "tangled.sh/tangled.sh/core/appview/xrpcclient" 17 + ) 18 + 19 + type OAuthRequest struct { 20 + ID uint 21 + AuthserverIss string 22 + State string 23 + Did string 24 + PdsUrl string 25 + PkceVerifier string 26 + DpopAuthserverNonce string 27 + DpopPrivateJwk string 28 + } 29 + 30 + type OAuth struct { 31 + Store *sessions.CookieStore 32 + Db *db.DB 33 + Config *appview.Config 34 + } 35 + 36 + func NewOAuth(db *db.DB, config *appview.Config) *OAuth { 37 + return &OAuth{ 38 + Store: sessions.NewCookieStore([]byte(config.Core.CookieSecret)), 39 + Db: db, 40 + Config: config, 41 + } 42 + } 43 + 44 + func (o *OAuth) SaveSession(w http.ResponseWriter, r *http.Request, oreq db.OAuthRequest, oresp *oauth.TokenResponse) error { 45 + // first we save the did in the user session 46 + userSession, err := o.Store.Get(r, appview.SessionName) 47 + if err != nil { 48 + return err 49 + } 50 + 51 + userSession.Values[appview.SessionDid] = oreq.Did 52 + userSession.Values[appview.SessionHandle] = oreq.Handle 53 + userSession.Values[appview.SessionPds] = oreq.PdsUrl 54 + userSession.Values[appview.SessionAuthenticated] = true 55 + err = userSession.Save(r, w) 56 + if err != nil { 57 + return fmt.Errorf("error saving user session: %w", err) 58 + } 59 + 60 + // then save the whole thing in the db 61 + session := db.OAuthSession{ 62 + Did: oreq.Did, 63 + Handle: oreq.Handle, 64 + PdsUrl: oreq.PdsUrl, 65 + DpopAuthserverNonce: oreq.DpopAuthserverNonce, 66 + AuthServerIss: oreq.AuthserverIss, 67 + DpopPrivateJwk: oreq.DpopPrivateJwk, 68 + AccessJwt: oresp.AccessToken, 69 + RefreshJwt: oresp.RefreshToken, 70 + Expiry: time.Now().Add(time.Duration(oresp.ExpiresIn) * time.Second).Format(time.RFC3339), 71 + } 72 + 73 + return db.SaveOAuthSession(o.Db, session) 74 + } 75 + 76 + func (o *OAuth) ClearSession(r *http.Request, w http.ResponseWriter) error { 77 + userSession, err := o.Store.Get(r, appview.SessionName) 78 + if err != nil || userSession.IsNew { 79 + return fmt.Errorf("error getting user session (or new session?): %w", err) 80 + } 81 + 82 + did := userSession.Values[appview.SessionDid].(string) 83 + 84 + err = db.DeleteOAuthSessionByDid(o.Db, did) 85 + if err != nil { 86 + return fmt.Errorf("error deleting oauth session: %w", err) 87 + } 88 + 89 + userSession.Options.MaxAge = -1 90 + 91 + return userSession.Save(r, w) 92 + } 93 + 94 + func (o *OAuth) GetSession(r *http.Request) (*db.OAuthSession, bool, error) { 95 + userSession, err := o.Store.Get(r, appview.SessionName) 96 + if err != nil || userSession.IsNew { 97 + return nil, false, fmt.Errorf("error getting user session (or new session?): %w", err) 98 + } 99 + 100 + did := userSession.Values[appview.SessionDid].(string) 101 + auth := userSession.Values[appview.SessionAuthenticated].(bool) 102 + 103 + session, err := db.GetOAuthSessionByDid(o.Db, did) 104 + if err != nil { 105 + return nil, false, fmt.Errorf("error getting oauth session: %w", err) 106 + } 107 + 108 + expiry, err := time.Parse(time.RFC3339, session.Expiry) 109 + if err != nil { 110 + return nil, false, fmt.Errorf("error parsing expiry time: %w", err) 111 + } 112 + if expiry.Sub(time.Now()) <= 5*time.Minute { 113 + privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk)) 114 + if err != nil { 115 + return nil, false, err 116 + } 117 + 118 + self := o.ClientMetadata() 119 + 120 + oauthClient, err := client.NewClient( 121 + self.ClientID, 122 + o.Config.OAuth.Jwks, 123 + self.RedirectURIs[0], 124 + ) 125 + 126 + if err != nil { 127 + return nil, false, err 128 + } 129 + 130 + resp, err := oauthClient.RefreshTokenRequest(r.Context(), session.RefreshJwt, session.AuthServerIss, session.DpopAuthserverNonce, privateJwk) 131 + if err != nil { 132 + return nil, false, err 133 + } 134 + 135 + newExpiry := time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second).Format(time.RFC3339) 136 + err = db.RefreshOAuthSession(o.Db, did, resp.AccessToken, resp.RefreshToken, newExpiry) 137 + if err != nil { 138 + return nil, false, fmt.Errorf("error refreshing oauth session: %w", err) 139 + } 140 + 141 + // update the current session 142 + session.AccessJwt = resp.AccessToken 143 + session.RefreshJwt = resp.RefreshToken 144 + session.DpopAuthserverNonce = resp.DpopAuthserverNonce 145 + session.Expiry = newExpiry 146 + } 147 + 148 + return session, auth, nil 149 + } 150 + 151 + type User struct { 152 + Handle string 153 + Did string 154 + Pds string 155 + } 156 + 157 + func (a *OAuth) GetUser(r *http.Request) *User { 158 + clientSession, err := a.Store.Get(r, appview.SessionName) 159 + 160 + if err != nil || clientSession.IsNew { 161 + return nil 162 + } 163 + 164 + return &User{ 165 + Handle: clientSession.Values[appview.SessionHandle].(string), 166 + Did: clientSession.Values[appview.SessionDid].(string), 167 + Pds: clientSession.Values[appview.SessionPds].(string), 168 + } 169 + } 170 + 171 + func (a *OAuth) GetDid(r *http.Request) string { 172 + clientSession, err := a.Store.Get(r, appview.SessionName) 173 + 174 + if err != nil || clientSession.IsNew { 175 + return "" 176 + } 177 + 178 + return clientSession.Values[appview.SessionDid].(string) 179 + } 180 + 181 + func (o *OAuth) AuthorizedClient(r *http.Request) (*xrpc.Client, error) { 182 + session, auth, err := o.GetSession(r) 183 + if err != nil { 184 + return nil, fmt.Errorf("error getting session: %w", err) 185 + } 186 + if !auth { 187 + return nil, fmt.Errorf("not authorized") 188 + } 189 + 190 + client := &oauth.XrpcClient{ 191 + OnDpopPdsNonceChanged: func(did, newNonce string) { 192 + err := db.UpdateDpopPdsNonce(o.Db, did, newNonce) 193 + if err != nil { 194 + log.Printf("error updating dpop pds nonce: %v", err) 195 + } 196 + }, 197 + } 198 + 199 + privateJwk, err := helpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk)) 200 + if err != nil { 201 + return nil, fmt.Errorf("error parsing private jwk: %w", err) 202 + } 203 + 204 + xrpcClient := xrpc.NewClient(client, &oauth.XrpcAuthedRequestArgs{ 205 + Did: session.Did, 206 + PdsUrl: session.PdsUrl, 207 + DpopPdsNonce: session.PdsUrl, 208 + AccessToken: session.AccessJwt, 209 + Issuer: session.AuthServerIss, 210 + DpopPrivateJwk: privateJwk, 211 + }) 212 + 213 + return xrpcClient, nil 214 + } 215 + 216 + type ClientMetadata struct { 217 + ClientID string `json:"client_id"` 218 + ClientName string `json:"client_name"` 219 + SubjectType string `json:"subject_type"` 220 + ClientURI string `json:"client_uri"` 221 + RedirectURIs []string `json:"redirect_uris"` 222 + GrantTypes []string `json:"grant_types"` 223 + ResponseTypes []string `json:"response_types"` 224 + ApplicationType string `json:"application_type"` 225 + DpopBoundAccessTokens bool `json:"dpop_bound_access_tokens"` 226 + JwksURI string `json:"jwks_uri"` 227 + Scope string `json:"scope"` 228 + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` 229 + TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` 230 + } 231 + 232 + func (o *OAuth) ClientMetadata() ClientMetadata { 233 + makeRedirectURIs := func(c string) []string { 234 + return []string{fmt.Sprintf("%s/oauth/callback", c)} 235 + } 236 + 237 + clientURI := o.Config.Core.AppviewHost 238 + clientID := fmt.Sprintf("%s/oauth/client-metadata.json", clientURI) 239 + redirectURIs := makeRedirectURIs(clientURI) 240 + 241 + if o.Config.Core.Dev { 242 + clientURI = fmt.Sprintf("http://127.0.0.1:3000") 243 + redirectURIs = makeRedirectURIs(clientURI) 244 + 245 + query := url.Values{} 246 + query.Add("redirect_uri", redirectURIs[0]) 247 + query.Add("scope", "atproto transition:generic") 248 + clientID = fmt.Sprintf("http://localhost?%s", query.Encode()) 249 + } 250 + 251 + jwksURI := fmt.Sprintf("%s/oauth/jwks.json", clientURI) 252 + 253 + return ClientMetadata{ 254 + ClientID: clientID, 255 + ClientName: "Tangled", 256 + SubjectType: "public", 257 + ClientURI: clientURI, 258 + RedirectURIs: redirectURIs, 259 + GrantTypes: []string{"authorization_code", "refresh_token"}, 260 + ResponseTypes: []string{"code"}, 261 + ApplicationType: "web", 262 + DpopBoundAccessTokens: true, 263 + JwksURI: jwksURI, 264 + Scope: "atproto transition:generic", 265 + TokenEndpointAuthMethod: "private_key_jwt", 266 + TokenEndpointAuthSigningAlg: "ES256", 267 + } 268 + }
+5 -1
appview/pages/funcmap.go
··· 13 13 "time" 14 14 15 15 "github.com/dustin/go-humanize" 16 + "github.com/microcosm-cc/bluemonday" 17 + "tangled.sh/tangled.sh/core/appview/filetree" 16 18 "tangled.sh/tangled.sh/core/appview/pages/markup" 17 19 ) 18 20 ··· 142 144 return v.Slice(start, end).Interface() 143 145 }, 144 146 "markdown": func(text string) template.HTML { 145 - return template.HTML(markup.RenderMarkdown(text)) 147 + rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault} 148 + return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text))) 146 149 }, 147 150 "isNil": func(t any) bool { 148 151 // returns false for other "zero" values ··· 174 177 return template.HTML(data) 175 178 }, 176 179 "cssContentHash": CssContentHash, 180 + "fileTree": filetree.FileTree, 177 181 } 178 182 } 179 183
+31
appview/pages/markup/camo.go
··· 1 + package markup 2 + 3 + import ( 4 + "crypto/hmac" 5 + "crypto/sha256" 6 + "encoding/hex" 7 + "fmt" 8 + 9 + "github.com/yuin/goldmark/ast" 10 + ) 11 + 12 + func generateCamoURL(baseURL, secret, imageURL string) string { 13 + h := hmac.New(sha256.New, []byte(secret)) 14 + h.Write([]byte(imageURL)) 15 + signature := hex.EncodeToString(h.Sum(nil)) 16 + hexURL := hex.EncodeToString([]byte(imageURL)) 17 + return fmt.Sprintf("%s/%s/%s", baseURL, signature, hexURL) 18 + } 19 + 20 + func (rctx *RenderContext) camoImageLinkTransformer(img *ast.Image) { 21 + // don't camo on dev 22 + if rctx.IsDev { 23 + return 24 + } 25 + 26 + dst := string(img.Destination) 27 + 28 + if rctx.CamoUrl != "" && rctx.CamoSecret != "" { 29 + img.Destination = []byte(generateCamoURL(rctx.CamoUrl, rctx.CamoSecret, dst)) 30 + } 31 + }
+26
appview/pages/markup/format.go
··· 1 + package markup 2 + 3 + import "strings" 4 + 5 + type Format string 6 + 7 + const ( 8 + FormatMarkdown Format = "markdown" 9 + FormatText Format = "text" 10 + ) 11 + 12 + var FileTypes map[Format][]string = map[Format][]string{ 13 + FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"}, 14 + } 15 + 16 + func GetFormat(filename string) Format { 17 + for format, extensions := range FileTypes { 18 + for _, extension := range extensions { 19 + if strings.HasSuffix(filename, extension) { 20 + return format 21 + } 22 + } 23 + } 24 + // default format 25 + return FormatText 26 + }
+121 -1
appview/pages/markup/markdown.go
··· 3 3 4 4 import ( 5 5 "bytes" 6 + "net/url" 7 + "path" 6 8 7 9 "github.com/yuin/goldmark" 10 + "github.com/yuin/goldmark/ast" 8 11 "github.com/yuin/goldmark/extension" 9 12 "github.com/yuin/goldmark/parser" 13 + "github.com/yuin/goldmark/renderer/html" 14 + "github.com/yuin/goldmark/text" 15 + "github.com/yuin/goldmark/util" 16 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 10 17 ) 11 18 12 - func RenderMarkdown(source string) string { 19 + // RendererType defines the type of renderer to use based on context 20 + type RendererType int 21 + 22 + const ( 23 + // RendererTypeRepoMarkdown is for repository documentation markdown files 24 + RendererTypeRepoMarkdown RendererType = iota 25 + // RendererTypeDefault is non-repo markdown, like issues/pulls/comments. 26 + RendererTypeDefault 27 + ) 28 + 29 + // RenderContext holds the contextual data for rendering markdown. 30 + // It can be initialized empty, and that'll skip any transformations. 31 + type RenderContext struct { 32 + CamoUrl string 33 + CamoSecret string 34 + repoinfo.RepoInfo 35 + IsDev bool 36 + RendererType RendererType 37 + } 38 + 39 + func (rctx *RenderContext) RenderMarkdown(source string) string { 13 40 md := goldmark.New( 14 41 goldmark.WithExtensions(extension.GFM), 15 42 goldmark.WithParserOptions( 16 43 parser.WithAutoHeadingID(), 17 44 ), 45 + goldmark.WithRendererOptions(html.WithUnsafe()), 18 46 ) 47 + 48 + if rctx != nil { 49 + var transformers []util.PrioritizedValue 50 + 51 + transformers = append(transformers, util.Prioritized(&MarkdownTransformer{rctx: rctx}, 10000)) 52 + 53 + md.Parser().AddOptions( 54 + parser.WithASTTransformers(transformers...), 55 + ) 56 + } 57 + 19 58 var buf bytes.Buffer 20 59 if err := md.Convert([]byte(source), &buf); err != nil { 21 60 return source 22 61 } 23 62 return buf.String() 24 63 } 64 + 65 + type MarkdownTransformer struct { 66 + rctx *RenderContext 67 + } 68 + 69 + func (a *MarkdownTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { 70 + _ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 71 + if !entering { 72 + return ast.WalkContinue, nil 73 + } 74 + 75 + switch a.rctx.RendererType { 76 + case RendererTypeRepoMarkdown: 77 + switch n.(type) { 78 + case *ast.Link: 79 + a.rctx.relativeLinkTransformer(n.(*ast.Link)) 80 + case *ast.Image: 81 + a.rctx.imageFromKnotTransformer(n.(*ast.Image)) 82 + a.rctx.camoImageLinkTransformer(n.(*ast.Image)) 83 + } 84 + 85 + case RendererTypeDefault: 86 + switch n.(type) { 87 + case *ast.Image: 88 + a.rctx.imageFromKnotTransformer(n.(*ast.Image)) 89 + a.rctx.camoImageLinkTransformer(n.(*ast.Image)) 90 + } 91 + } 92 + 93 + return ast.WalkContinue, nil 94 + }) 95 + } 96 + 97 + func (rctx *RenderContext) relativeLinkTransformer(link *ast.Link) { 98 + dst := string(link.Destination) 99 + 100 + if isAbsoluteUrl(dst) { 101 + return 102 + } 103 + 104 + newPath := path.Join("/", rctx.RepoInfo.FullName(), "tree", rctx.RepoInfo.Ref, dst) 105 + link.Destination = []byte(newPath) 106 + } 107 + 108 + func (rctx *RenderContext) imageFromKnotTransformer(img *ast.Image) { 109 + dst := string(img.Destination) 110 + 111 + if isAbsoluteUrl(dst) { 112 + return 113 + } 114 + 115 + // strip leading './' 116 + if len(dst) >= 2 && dst[0:2] == "./" { 117 + dst = dst[2:] 118 + } 119 + 120 + scheme := "https" 121 + if rctx.IsDev { 122 + scheme = "http" 123 + } 124 + parsedURL := &url.URL{ 125 + Scheme: scheme, 126 + Host: rctx.Knot, 127 + Path: path.Join("/", 128 + rctx.RepoInfo.OwnerDid, 129 + rctx.RepoInfo.Name, 130 + "raw", 131 + url.PathEscape(rctx.RepoInfo.Ref), 132 + dst), 133 + } 134 + newPath := parsedURL.String() 135 + img.Destination = []byte(newPath) 136 + } 137 + 138 + func isAbsoluteUrl(link string) bool { 139 + parsed, err := url.Parse(link) 140 + if err != nil { 141 + return false 142 + } 143 + return parsed.IsAbs() 144 + }
-26
appview/pages/markup/readme.go
··· 1 - package markup 2 - 3 - import "strings" 4 - 5 - type Format string 6 - 7 - const ( 8 - FormatMarkdown Format = "markdown" 9 - FormatText Format = "text" 10 - ) 11 - 12 - var FileTypes map[Format][]string = map[Format][]string{ 13 - FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"}, 14 - } 15 - 16 - func GetFormat(filename string) Format { 17 - for format, extensions := range FileTypes { 18 - for _, extension := range extensions { 19 - if strings.HasSuffix(filename, extension) { 20 - return format 21 - } 22 - } 23 - } 24 - // default format 25 - return FormatText 26 - }
+276 -215
appview/pages/pages.go
··· 11 11 "io/fs" 12 12 "log" 13 13 "net/http" 14 - "path" 14 + "os" 15 15 "path/filepath" 16 - "slices" 17 16 "strings" 18 17 19 - "tangled.sh/tangled.sh/core/appview/auth" 18 + "tangled.sh/tangled.sh/core/appview" 20 19 "tangled.sh/tangled.sh/core/appview/db" 20 + "tangled.sh/tangled.sh/core/appview/oauth" 21 21 "tangled.sh/tangled.sh/core/appview/pages/markup" 22 - "tangled.sh/tangled.sh/core/appview/state/userutil" 22 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 + "tangled.sh/tangled.sh/core/appview/pagination" 23 24 "tangled.sh/tangled.sh/core/patchutil" 24 25 "tangled.sh/tangled.sh/core/types" 25 26 ··· 28 29 "github.com/alecthomas/chroma/v2/lexers" 29 30 "github.com/alecthomas/chroma/v2/styles" 30 31 "github.com/bluesky-social/indigo/atproto/syntax" 32 + "github.com/go-git/go-git/v5/plumbing" 33 + "github.com/go-git/go-git/v5/plumbing/object" 31 34 "github.com/microcosm-cc/bluemonday" 32 35 ) 33 36 ··· 35 38 var Files embed.FS 36 39 37 40 type Pages struct { 38 - t map[string]*template.Template 41 + t map[string]*template.Template 42 + dev bool 43 + embedFS embed.FS 44 + templateDir string // Path to templates on disk for dev mode 45 + rctx *markup.RenderContext 39 46 } 40 47 41 - func NewPages() *Pages { 42 - templates := make(map[string]*template.Template) 48 + func NewPages(config *appview.Config) *Pages { 49 + // initialized with safe defaults, can be overriden per use 50 + rctx := &markup.RenderContext{ 51 + IsDev: config.Core.Dev, 52 + CamoUrl: config.Camo.Host, 53 + CamoSecret: config.Camo.SharedSecret, 54 + } 43 55 56 + p := &Pages{ 57 + t: make(map[string]*template.Template), 58 + dev: config.Core.Dev, 59 + embedFS: Files, 60 + rctx: rctx, 61 + templateDir: "appview/pages", 62 + } 63 + 64 + // Initial load of all templates 65 + p.loadAllTemplates() 66 + 67 + return p 68 + } 69 + 70 + func (p *Pages) loadAllTemplates() { 71 + templates := make(map[string]*template.Template) 44 72 var fragmentPaths []string 73 + 74 + // Use embedded FS for initial loading 45 75 // First, collect all fragment paths 46 - err := fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { 76 + err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 47 77 if err != nil { 48 78 return err 49 79 } 50 - 51 80 if d.IsDir() { 52 81 return nil 53 82 } 54 - 55 83 if !strings.HasSuffix(path, ".html") { 56 84 return nil 57 85 } 58 - 59 86 if !strings.Contains(path, "fragments/") { 60 87 return nil 61 88 } 62 - 63 89 name := strings.TrimPrefix(path, "templates/") 64 90 name = strings.TrimSuffix(name, ".html") 65 - 66 91 tmpl, err := template.New(name). 67 92 Funcs(funcMap()). 68 - ParseFS(Files, path) 93 + ParseFS(p.embedFS, path) 69 94 if err != nil { 70 95 log.Fatalf("setting up fragment: %v", err) 71 96 } 72 - 73 97 templates[name] = tmpl 74 98 fragmentPaths = append(fragmentPaths, path) 75 99 log.Printf("loaded fragment: %s", name) ··· 80 104 } 81 105 82 106 // Then walk through and setup the rest of the templates 83 - err = fs.WalkDir(Files, "templates", func(path string, d fs.DirEntry, err error) error { 107 + err = fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error { 84 108 if err != nil { 85 109 return err 86 110 } 87 - 88 111 if d.IsDir() { 89 112 return nil 90 113 } 91 - 92 114 if !strings.HasSuffix(path, "html") { 93 115 return nil 94 116 } 95 - 96 117 // Skip fragments as they've already been loaded 97 118 if strings.Contains(path, "fragments/") { 98 119 return nil 99 120 } 100 - 101 121 // Skip layouts 102 122 if strings.Contains(path, "layouts/") { 103 123 return nil 104 124 } 105 - 106 125 name := strings.TrimPrefix(path, "templates/") 107 126 name = strings.TrimSuffix(name, ".html") 108 - 109 127 // Add the page template on top of the base 110 128 allPaths := []string{} 111 129 allPaths = append(allPaths, "templates/layouts/*.html") ··· 113 131 allPaths = append(allPaths, path) 114 132 tmpl, err := template.New(name). 115 133 Funcs(funcMap()). 116 - ParseFS(Files, allPaths...) 134 + ParseFS(p.embedFS, allPaths...) 117 135 if err != nil { 118 136 return fmt.Errorf("setting up template: %w", err) 119 137 } 120 - 121 138 templates[name] = tmpl 122 139 log.Printf("loaded template: %s", name) 123 140 return nil ··· 127 144 } 128 145 129 146 log.Printf("total templates loaded: %d", len(templates)) 147 + p.t = templates 148 + } 130 149 131 - return &Pages{ 132 - t: templates, 150 + // loadTemplateFromDisk loads a template from the filesystem in dev mode 151 + func (p *Pages) loadTemplateFromDisk(name string) error { 152 + if !p.dev { 153 + return nil 154 + } 155 + 156 + log.Printf("reloading template from disk: %s", name) 157 + 158 + // Find all fragments first 159 + var fragmentPaths []string 160 + err := filepath.WalkDir(filepath.Join(p.templateDir, "templates"), func(path string, d fs.DirEntry, err error) error { 161 + if err != nil { 162 + return err 163 + } 164 + if d.IsDir() { 165 + return nil 166 + } 167 + if !strings.HasSuffix(path, ".html") { 168 + return nil 169 + } 170 + if !strings.Contains(path, "fragments/") { 171 + return nil 172 + } 173 + fragmentPaths = append(fragmentPaths, path) 174 + return nil 175 + }) 176 + if err != nil { 177 + return fmt.Errorf("walking disk template dir for fragments: %w", err) 178 + } 179 + 180 + // Find the template path on disk 181 + templatePath := filepath.Join(p.templateDir, "templates", name+".html") 182 + if _, err := os.Stat(templatePath); os.IsNotExist(err) { 183 + return fmt.Errorf("template not found on disk: %s", name) 184 + } 185 + 186 + // Create a new template 187 + tmpl := template.New(name).Funcs(funcMap()) 188 + 189 + // Parse layouts 190 + layoutGlob := filepath.Join(p.templateDir, "templates", "layouts", "*.html") 191 + layouts, err := filepath.Glob(layoutGlob) 192 + if err != nil { 193 + return fmt.Errorf("finding layout templates: %w", err) 194 + } 195 + 196 + // Create paths for parsing 197 + allFiles := append(layouts, fragmentPaths...) 198 + allFiles = append(allFiles, templatePath) 199 + 200 + // Parse all templates 201 + tmpl, err = tmpl.ParseFiles(allFiles...) 202 + if err != nil { 203 + return fmt.Errorf("parsing template files: %w", err) 133 204 } 205 + 206 + // Update the template in the map 207 + p.t[name] = tmpl 208 + log.Printf("template reloaded from disk: %s", name) 209 + return nil 134 210 } 135 211 136 - type LoginParams struct { 212 + func (p *Pages) executeOrReload(templateName string, w io.Writer, base string, params any) error { 213 + // In dev mode, reload the template from disk before executing 214 + if p.dev { 215 + if err := p.loadTemplateFromDisk(templateName); err != nil { 216 + log.Printf("warning: failed to reload template %s from disk: %v", templateName, err) 217 + // Continue with the existing template 218 + } 219 + } 220 + 221 + tmpl, exists := p.t[templateName] 222 + if !exists { 223 + return fmt.Errorf("template not found: %s", templateName) 224 + } 225 + 226 + if base == "" { 227 + return tmpl.Execute(w, params) 228 + } else { 229 + return tmpl.ExecuteTemplate(w, base, params) 230 + } 137 231 } 138 232 139 233 func (p *Pages) execute(name string, w io.Writer, params any) error { 140 - return p.t[name].ExecuteTemplate(w, "layouts/base", params) 234 + return p.executeOrReload(name, w, "layouts/base", params) 141 235 } 142 236 143 237 func (p *Pages) executePlain(name string, w io.Writer, params any) error { 144 - return p.t[name].Execute(w, params) 238 + return p.executeOrReload(name, w, "", params) 145 239 } 146 240 147 241 func (p *Pages) executeRepo(name string, w io.Writer, params any) error { 148 - return p.t[name].ExecuteTemplate(w, "layouts/repobase", params) 242 + return p.executeOrReload(name, w, "layouts/repobase", params) 243 + } 244 + 245 + type LoginParams struct { 149 246 } 150 247 151 248 func (p *Pages) Login(w io.Writer, params LoginParams) error { ··· 153 250 } 154 251 155 252 type TimelineParams struct { 156 - LoggedInUser *auth.User 253 + LoggedInUser *oauth.User 157 254 Timeline []db.TimelineEvent 158 255 DidHandleMap map[string]string 159 256 } ··· 163 260 } 164 261 165 262 type SettingsParams struct { 166 - LoggedInUser *auth.User 263 + LoggedInUser *oauth.User 167 264 PubKeys []db.PublicKey 168 265 Emails []db.Email 169 266 } ··· 173 270 } 174 271 175 272 type KnotsParams struct { 176 - LoggedInUser *auth.User 273 + LoggedInUser *oauth.User 177 274 Registrations []db.Registration 178 275 } 179 276 ··· 182 279 } 183 280 184 281 type KnotParams struct { 185 - LoggedInUser *auth.User 282 + LoggedInUser *oauth.User 186 283 DidHandleMap map[string]string 187 284 Registration *db.Registration 188 285 Members []string ··· 194 291 } 195 292 196 293 type NewRepoParams struct { 197 - LoggedInUser *auth.User 294 + LoggedInUser *oauth.User 198 295 Knots []string 199 296 } 200 297 ··· 203 300 } 204 301 205 302 type ForkRepoParams struct { 206 - LoggedInUser *auth.User 303 + LoggedInUser *oauth.User 207 304 Knots []string 208 - RepoInfo RepoInfo 305 + RepoInfo repoinfo.RepoInfo 209 306 } 210 307 211 308 func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { ··· 213 310 } 214 311 215 312 type ProfilePageParams struct { 216 - LoggedInUser *auth.User 217 - UserDid string 218 - UserHandle string 313 + LoggedInUser *oauth.User 219 314 Repos []db.Repo 220 315 CollaboratingRepos []db.Repo 221 - ProfileStats ProfileStats 222 - FollowStatus db.FollowStatus 223 - AvatarUri string 224 316 ProfileTimeline *db.ProfileTimeline 317 + Card ProfileCard 225 318 226 319 DidHandleMap map[string]string 227 320 } 228 321 229 - type ProfileStats struct { 230 - Followers int 231 - Following int 322 + type ProfileCard struct { 323 + UserDid string 324 + UserHandle string 325 + FollowStatus db.FollowStatus 326 + AvatarUri string 327 + Followers int 328 + Following int 329 + 330 + Profile *db.Profile 232 331 } 233 332 234 333 func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 235 334 return p.execute("user/profile", w, params) 236 335 } 237 336 337 + type ReposPageParams struct { 338 + LoggedInUser *oauth.User 339 + Repos []db.Repo 340 + Card ProfileCard 341 + 342 + DidHandleMap map[string]string 343 + } 344 + 345 + func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 346 + return p.execute("user/repos", w, params) 347 + } 348 + 238 349 type FollowFragmentParams struct { 239 350 UserDid string 240 351 FollowStatus db.FollowStatus ··· 244 355 return p.executePlain("user/fragments/follow", w, params) 245 356 } 246 357 358 + type EditBioParams struct { 359 + LoggedInUser *oauth.User 360 + Profile *db.Profile 361 + } 362 + 363 + func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 364 + return p.executePlain("user/fragments/editBio", w, params) 365 + } 366 + 367 + type EditPinsParams struct { 368 + LoggedInUser *oauth.User 369 + Profile *db.Profile 370 + AllRepos []PinnedRepo 371 + DidHandleMap map[string]string 372 + } 373 + 374 + type PinnedRepo struct { 375 + IsPinned bool 376 + db.Repo 377 + } 378 + 379 + func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 380 + return p.executePlain("user/fragments/editPins", w, params) 381 + } 382 + 247 383 type RepoActionsFragmentParams struct { 248 384 IsStarred bool 249 385 RepoAt syntax.ATURI ··· 255 391 } 256 392 257 393 type RepoDescriptionParams struct { 258 - RepoInfo RepoInfo 394 + RepoInfo repoinfo.RepoInfo 259 395 } 260 396 261 397 func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { ··· 266 402 return p.executePlain("repo/fragments/repoDescription", w, params) 267 403 } 268 404 269 - type RepoInfo struct { 270 - Name string 271 - OwnerDid string 272 - OwnerHandle string 273 - Description string 274 - Knot string 275 - RepoAt syntax.ATURI 276 - IsStarred bool 277 - Stats db.RepoStats 278 - Roles RolesInRepo 279 - Source *db.Repo 280 - SourceHandle string 281 - DisableFork bool 282 - } 283 - 284 - type RolesInRepo struct { 285 - Roles []string 286 - } 287 - 288 - func (r RolesInRepo) SettingsAllowed() bool { 289 - return slices.Contains(r.Roles, "repo:settings") 290 - } 291 - 292 - func (r RolesInRepo) CollaboratorInviteAllowed() bool { 293 - return slices.Contains(r.Roles, "repo:invite") 294 - } 295 - 296 - func (r RolesInRepo) RepoDeleteAllowed() bool { 297 - return slices.Contains(r.Roles, "repo:delete") 298 - } 299 - 300 - func (r RolesInRepo) IsOwner() bool { 301 - return slices.Contains(r.Roles, "repo:owner") 302 - } 303 - 304 - func (r RolesInRepo) IsCollaborator() bool { 305 - return slices.Contains(r.Roles, "repo:collaborator") 306 - } 307 - 308 - func (r RolesInRepo) IsPushAllowed() bool { 309 - return slices.Contains(r.Roles, "repo:push") 310 - } 311 - 312 - func (r RepoInfo) OwnerWithAt() string { 313 - if r.OwnerHandle != "" { 314 - return fmt.Sprintf("@%s", r.OwnerHandle) 315 - } else { 316 - return r.OwnerDid 317 - } 318 - } 319 - 320 - func (r RepoInfo) FullName() string { 321 - return path.Join(r.OwnerWithAt(), r.Name) 322 - } 323 - 324 - func (r RepoInfo) OwnerWithoutAt() string { 325 - if strings.HasPrefix(r.OwnerWithAt(), "@") { 326 - return strings.TrimPrefix(r.OwnerWithAt(), "@") 327 - } else { 328 - return userutil.FlattenDid(r.OwnerDid) 329 - } 330 - } 331 - 332 - func (r RepoInfo) FullNameWithoutAt() string { 333 - return path.Join(r.OwnerWithoutAt(), r.Name) 334 - } 335 - 336 - func (r RepoInfo) GetTabs() [][]string { 337 - tabs := [][]string{ 338 - {"overview", "/", "square-chart-gantt"}, 339 - {"issues", "/issues", "circle-dot"}, 340 - {"pulls", "/pulls", "git-pull-request"}, 341 - } 342 - 343 - if r.Roles.SettingsAllowed() { 344 - tabs = append(tabs, []string{"settings", "/settings", "cog"}) 345 - } 346 - 347 - return tabs 348 - } 349 - 350 - // each tab on a repo could have some metadata: 351 - // 352 - // issues -> number of open issues etc. 353 - // settings -> a warning icon to setup branch protection? idk 354 - // 355 - // we gather these bits of info here, because go templates 356 - // are difficult to program in 357 - func (r RepoInfo) TabMetadata() map[string]any { 358 - meta := make(map[string]any) 359 - 360 - if r.Stats.PullCount.Open > 0 { 361 - meta["pulls"] = r.Stats.PullCount.Open 362 - } 363 - 364 - if r.Stats.IssueCount.Open > 0 { 365 - meta["issues"] = r.Stats.IssueCount.Open 366 - } 367 - 368 - // more stuff? 369 - 370 - return meta 371 - } 372 - 373 405 type RepoIndexParams struct { 374 - LoggedInUser *auth.User 375 - RepoInfo RepoInfo 376 - Active string 377 - TagMap map[string][]string 406 + LoggedInUser *oauth.User 407 + RepoInfo repoinfo.RepoInfo 408 + Active string 409 + TagMap map[string][]string 410 + CommitsTrunc []*object.Commit 411 + TagsTrunc []*types.TagReference 412 + BranchesTrunc []types.Branch 378 413 types.RepoIndexResponse 379 414 HTMLReadme template.HTML 380 415 Raw bool ··· 387 422 return p.executeRepo("repo/empty", w, params) 388 423 } 389 424 425 + p.rctx.RepoInfo = params.RepoInfo 426 + p.rctx.RendererType = markup.RendererTypeRepoMarkdown 427 + 390 428 if params.ReadmeFileName != "" { 391 429 var htmlString string 392 430 ext := filepath.Ext(params.ReadmeFileName) 393 431 switch ext { 394 432 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 395 - htmlString = markup.RenderMarkdown(params.Readme) 433 + htmlString = p.rctx.RenderMarkdown(params.Readme) 396 434 params.Raw = false 397 435 params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString)) 398 436 default: ··· 406 444 } 407 445 408 446 type RepoLogParams struct { 409 - LoggedInUser *auth.User 410 - RepoInfo RepoInfo 447 + LoggedInUser *oauth.User 448 + RepoInfo repoinfo.RepoInfo 449 + TagMap map[string][]string 411 450 types.RepoLogResponse 412 451 Active string 413 452 EmailToDidOrHandle map[string]string ··· 415 454 416 455 func (p *Pages) RepoLog(w io.Writer, params RepoLogParams) error { 417 456 params.Active = "overview" 418 - return p.execute("repo/log", w, params) 457 + return p.executeRepo("repo/log", w, params) 419 458 } 420 459 421 460 type RepoCommitParams struct { 422 - LoggedInUser *auth.User 423 - RepoInfo RepoInfo 424 - Active string 461 + LoggedInUser *oauth.User 462 + RepoInfo repoinfo.RepoInfo 463 + Active string 464 + EmailToDidOrHandle map[string]string 465 + 425 466 types.RepoCommitResponse 426 - EmailToDidOrHandle map[string]string 427 467 } 428 468 429 469 func (p *Pages) RepoCommit(w io.Writer, params RepoCommitParams) error { ··· 432 472 } 433 473 434 474 type RepoTreeParams struct { 435 - LoggedInUser *auth.User 436 - RepoInfo RepoInfo 475 + LoggedInUser *oauth.User 476 + RepoInfo repoinfo.RepoInfo 437 477 Active string 438 478 BreadCrumbs [][]string 439 479 BaseTreeLink string ··· 468 508 } 469 509 470 510 type RepoBranchesParams struct { 471 - LoggedInUser *auth.User 472 - RepoInfo RepoInfo 511 + LoggedInUser *oauth.User 512 + RepoInfo repoinfo.RepoInfo 513 + Active string 473 514 types.RepoBranchesResponse 474 515 } 475 516 476 517 func (p *Pages) RepoBranches(w io.Writer, params RepoBranchesParams) error { 518 + params.Active = "overview" 477 519 return p.executeRepo("repo/branches", w, params) 478 520 } 479 521 480 522 type RepoTagsParams struct { 481 - LoggedInUser *auth.User 482 - RepoInfo RepoInfo 523 + LoggedInUser *oauth.User 524 + RepoInfo repoinfo.RepoInfo 525 + Active string 483 526 types.RepoTagsResponse 527 + ArtifactMap map[plumbing.Hash][]db.Artifact 528 + DanglingArtifacts []db.Artifact 484 529 } 485 530 486 531 func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { 532 + params.Active = "overview" 487 533 return p.executeRepo("repo/tags", w, params) 488 534 } 489 535 536 + type RepoArtifactParams struct { 537 + LoggedInUser *oauth.User 538 + RepoInfo repoinfo.RepoInfo 539 + Artifact db.Artifact 540 + } 541 + 542 + func (p *Pages) RepoArtifactFragment(w io.Writer, params RepoArtifactParams) error { 543 + return p.executePlain("repo/fragments/artifact", w, params) 544 + } 545 + 490 546 type RepoBlobParams struct { 491 - LoggedInUser *auth.User 492 - RepoInfo RepoInfo 547 + LoggedInUser *oauth.User 548 + RepoInfo repoinfo.RepoInfo 493 549 Active string 494 550 BreadCrumbs [][]string 495 551 ShowRendered bool ··· 504 560 if params.ShowRendered { 505 561 switch markup.GetFormat(params.Path) { 506 562 case markup.FormatMarkdown: 507 - params.RenderedContents = template.HTML(markup.RenderMarkdown(params.Contents)) 563 + p.rctx.RepoInfo = params.RepoInfo 564 + p.rctx.RendererType = markup.RendererTypeRepoMarkdown 565 + params.RenderedContents = template.HTML(bluemonday.UGCPolicy().Sanitize(p.rctx.RenderMarkdown(params.Contents))) 508 566 } 509 567 } 510 568 ··· 548 606 } 549 607 550 608 type RepoSettingsParams struct { 551 - LoggedInUser *auth.User 552 - RepoInfo RepoInfo 609 + LoggedInUser *oauth.User 610 + RepoInfo repoinfo.RepoInfo 553 611 Collaborators []Collaborator 554 612 Active string 555 613 Branches []string ··· 564 622 } 565 623 566 624 type RepoIssuesParams struct { 567 - LoggedInUser *auth.User 568 - RepoInfo RepoInfo 569 - Active string 570 - Issues []db.Issue 571 - DidHandleMap map[string]string 572 - 625 + LoggedInUser *oauth.User 626 + RepoInfo repoinfo.RepoInfo 627 + Active string 628 + Issues []db.Issue 629 + DidHandleMap map[string]string 630 + Page pagination.Page 573 631 FilteringByOpen bool 574 632 } 575 633 ··· 579 637 } 580 638 581 639 type RepoSingleIssueParams struct { 582 - LoggedInUser *auth.User 583 - RepoInfo RepoInfo 640 + LoggedInUser *oauth.User 641 + RepoInfo repoinfo.RepoInfo 584 642 Active string 585 643 Issue db.Issue 586 644 Comments []db.Comment ··· 601 659 } 602 660 603 661 type RepoNewIssueParams struct { 604 - LoggedInUser *auth.User 605 - RepoInfo RepoInfo 662 + LoggedInUser *oauth.User 663 + RepoInfo repoinfo.RepoInfo 606 664 Active string 607 665 } 608 666 ··· 612 670 } 613 671 614 672 type EditIssueCommentParams struct { 615 - LoggedInUser *auth.User 616 - RepoInfo RepoInfo 673 + LoggedInUser *oauth.User 674 + RepoInfo repoinfo.RepoInfo 617 675 Issue *db.Issue 618 676 Comment *db.Comment 619 677 } ··· 623 681 } 624 682 625 683 type SingleIssueCommentParams struct { 626 - LoggedInUser *auth.User 684 + LoggedInUser *oauth.User 627 685 DidHandleMap map[string]string 628 - RepoInfo RepoInfo 686 + RepoInfo repoinfo.RepoInfo 629 687 Issue *db.Issue 630 688 Comment *db.Comment 631 689 } ··· 635 693 } 636 694 637 695 type RepoNewPullParams struct { 638 - LoggedInUser *auth.User 639 - RepoInfo RepoInfo 696 + LoggedInUser *oauth.User 697 + RepoInfo repoinfo.RepoInfo 640 698 Branches []types.Branch 641 699 Active string 642 700 } ··· 647 705 } 648 706 649 707 type RepoPullsParams struct { 650 - LoggedInUser *auth.User 651 - RepoInfo RepoInfo 708 + LoggedInUser *oauth.User 709 + RepoInfo repoinfo.RepoInfo 652 710 Pulls []*db.Pull 653 711 Active string 654 712 DidHandleMap map[string]string ··· 679 737 } 680 738 681 739 type RepoSinglePullParams struct { 682 - LoggedInUser *auth.User 683 - RepoInfo RepoInfo 684 - Active string 685 - DidHandleMap map[string]string 686 - Pull *db.Pull 687 - PullSourceRepo *db.Repo 688 - MergeCheck types.MergeCheckResponse 689 - ResubmitCheck ResubmitResult 740 + LoggedInUser *oauth.User 741 + RepoInfo repoinfo.RepoInfo 742 + Active string 743 + DidHandleMap map[string]string 744 + Pull *db.Pull 745 + MergeCheck types.MergeCheckResponse 746 + ResubmitCheck ResubmitResult 690 747 } 691 748 692 749 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { ··· 695 752 } 696 753 697 754 type RepoPullPatchParams struct { 698 - LoggedInUser *auth.User 755 + LoggedInUser *oauth.User 699 756 DidHandleMap map[string]string 700 - RepoInfo RepoInfo 757 + RepoInfo repoinfo.RepoInfo 701 758 Pull *db.Pull 702 - Diff types.NiceDiff 759 + Diff *types.NiceDiff 703 760 Round int 704 761 Submission *db.PullSubmission 705 762 } ··· 710 767 } 711 768 712 769 type RepoPullInterdiffParams struct { 713 - LoggedInUser *auth.User 770 + LoggedInUser *oauth.User 714 771 DidHandleMap map[string]string 715 - RepoInfo RepoInfo 772 + RepoInfo repoinfo.RepoInfo 716 773 Pull *db.Pull 717 774 Round int 718 775 Interdiff *patchutil.InterdiffResult ··· 724 781 } 725 782 726 783 type PullPatchUploadParams struct { 727 - RepoInfo RepoInfo 784 + RepoInfo repoinfo.RepoInfo 728 785 } 729 786 730 787 func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { ··· 732 789 } 733 790 734 791 type PullCompareBranchesParams struct { 735 - RepoInfo RepoInfo 792 + RepoInfo repoinfo.RepoInfo 736 793 Branches []types.Branch 737 794 } 738 795 ··· 741 798 } 742 799 743 800 type PullCompareForkParams struct { 744 - RepoInfo RepoInfo 801 + RepoInfo repoinfo.RepoInfo 745 802 Forks []db.Repo 746 803 } 747 804 ··· 750 807 } 751 808 752 809 type PullCompareForkBranchesParams struct { 753 - RepoInfo RepoInfo 810 + RepoInfo repoinfo.RepoInfo 754 811 SourceBranches []types.Branch 755 812 TargetBranches []types.Branch 756 813 } ··· 760 817 } 761 818 762 819 type PullResubmitParams struct { 763 - LoggedInUser *auth.User 764 - RepoInfo RepoInfo 820 + LoggedInUser *oauth.User 821 + RepoInfo repoinfo.RepoInfo 765 822 Pull *db.Pull 766 823 SubmissionId int 767 824 } ··· 771 828 } 772 829 773 830 type PullActionsParams struct { 774 - LoggedInUser *auth.User 775 - RepoInfo RepoInfo 831 + LoggedInUser *oauth.User 832 + RepoInfo repoinfo.RepoInfo 776 833 Pull *db.Pull 777 834 RoundNumber int 778 835 MergeCheck types.MergeCheckResponse ··· 784 841 } 785 842 786 843 type PullNewCommentParams struct { 787 - LoggedInUser *auth.User 788 - RepoInfo RepoInfo 844 + LoggedInUser *oauth.User 845 + RepoInfo repoinfo.RepoInfo 789 846 Pull *db.Pull 790 847 RoundNumber int 791 848 } ··· 795 852 } 796 853 797 854 func (p *Pages) Static() http.Handler { 855 + if p.dev { 856 + return http.StripPrefix("/static/", http.FileServer(http.Dir("appview/pages/static"))) 857 + } 858 + 798 859 sub, err := fs.Sub(Files, "static") 799 860 if err != nil { 800 861 log.Fatalf("no static dir found? that's crazy: %v", err)
+117
appview/pages/repoinfo/repoinfo.go
··· 1 + package repoinfo 2 + 3 + import ( 4 + "fmt" 5 + "path" 6 + "slices" 7 + "strings" 8 + 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + "tangled.sh/tangled.sh/core/appview/db" 11 + "tangled.sh/tangled.sh/core/appview/state/userutil" 12 + ) 13 + 14 + func (r RepoInfo) OwnerWithAt() string { 15 + if r.OwnerHandle != "" { 16 + return fmt.Sprintf("@%s", r.OwnerHandle) 17 + } else { 18 + return r.OwnerDid 19 + } 20 + } 21 + 22 + func (r RepoInfo) FullName() string { 23 + return path.Join(r.OwnerWithAt(), r.Name) 24 + } 25 + 26 + func (r RepoInfo) OwnerWithoutAt() string { 27 + if strings.HasPrefix(r.OwnerWithAt(), "@") { 28 + return strings.TrimPrefix(r.OwnerWithAt(), "@") 29 + } else { 30 + return userutil.FlattenDid(r.OwnerDid) 31 + } 32 + } 33 + 34 + func (r RepoInfo) FullNameWithoutAt() string { 35 + return path.Join(r.OwnerWithoutAt(), r.Name) 36 + } 37 + 38 + func (r RepoInfo) GetTabs() [][]string { 39 + tabs := [][]string{ 40 + {"overview", "/", "square-chart-gantt"}, 41 + {"issues", "/issues", "circle-dot"}, 42 + {"pulls", "/pulls", "git-pull-request"}, 43 + } 44 + 45 + if r.Roles.SettingsAllowed() { 46 + tabs = append(tabs, []string{"settings", "/settings", "cog"}) 47 + } 48 + 49 + return tabs 50 + } 51 + 52 + type RepoInfo struct { 53 + Name string 54 + OwnerDid string 55 + OwnerHandle string 56 + Description string 57 + Knot string 58 + RepoAt syntax.ATURI 59 + IsStarred bool 60 + Stats db.RepoStats 61 + Roles RolesInRepo 62 + Source *db.Repo 63 + SourceHandle string 64 + Ref string 65 + DisableFork bool 66 + } 67 + 68 + // each tab on a repo could have some metadata: 69 + // 70 + // issues -> number of open issues etc. 71 + // settings -> a warning icon to setup branch protection? idk 72 + // 73 + // we gather these bits of info here, because go templates 74 + // are difficult to program in 75 + func (r RepoInfo) TabMetadata() map[string]any { 76 + meta := make(map[string]any) 77 + 78 + if r.Stats.PullCount.Open > 0 { 79 + meta["pulls"] = r.Stats.PullCount.Open 80 + } 81 + 82 + if r.Stats.IssueCount.Open > 0 { 83 + meta["issues"] = r.Stats.IssueCount.Open 84 + } 85 + 86 + // more stuff? 87 + 88 + return meta 89 + } 90 + 91 + type RolesInRepo struct { 92 + Roles []string 93 + } 94 + 95 + func (r RolesInRepo) SettingsAllowed() bool { 96 + return slices.Contains(r.Roles, "repo:settings") 97 + } 98 + 99 + func (r RolesInRepo) CollaboratorInviteAllowed() bool { 100 + return slices.Contains(r.Roles, "repo:invite") 101 + } 102 + 103 + func (r RolesInRepo) RepoDeleteAllowed() bool { 104 + return slices.Contains(r.Roles, "repo:delete") 105 + } 106 + 107 + func (r RolesInRepo) IsOwner() bool { 108 + return slices.Contains(r.Roles, "repo:owner") 109 + } 110 + 111 + func (r RolesInRepo) IsCollaborator() bool { 112 + return slices.Contains(r.Roles, "repo:collaborator") 113 + } 114 + 115 + func (r RolesInRepo) IsPushAllowed() bool { 116 + return slices.Contains(r.Roles, "repo:push") 117 + }
+2 -2
appview/pages/templates/knot.html
··· 83 83 class="max-w-2xl space-y-4"> 84 84 <input 85 85 type="text" 86 - id="member" 87 - name="member" 86 + id="subject" 87 + name="subject" 88 88 placeholder="did or handle" 89 89 required 90 90 class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400"/>
+1
appview/pages/templates/layouts/base.html
··· 7 7 name="viewport" 8 8 content="width=device-width, initial-scale=1.0" 9 9 /> 10 + <meta name="htmx-config" content='{"includeIndicatorStyles": false}'> 10 11 <script src="/static/htmx.min.js"></script> 11 12 <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 12 13 <title>{{ block "title" . }}{{ end }} ยท tangled</title>
+2 -2
appview/pages/templates/layouts/footer.html
··· 1 1 {{ define "layouts/footer" }} 2 - <div class="w-full p-4 bg-white dark:bg-gray-800 rounded-t"> 2 + <div class="w-full p-4 bg-white dark:bg-gray-800 rounded-t drop-shadow-sm"> 3 3 <div class="container mx-auto text-center text-gray-600 dark:text-gray-400 text-sm"> 4 - <span class="font-semibold italic">tangled</span> &mdash; made by <a href="/@oppili.bsky.social">@oppili.bsky.social</a> and <a href="/@icyphox.sh">@icyphox.sh</a> 4 + <span class="font-semibold italic">tangled</span> &mdash; made by <a href="/@oppi.li">@oppi.li</a> and <a href="/@icyphox.sh">@icyphox.sh</a> 5 5 </div> 6 6 </div> 7 7 {{ end }}
+1 -1
appview/pages/templates/repo/blob.html
··· 42 42 <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 43 43 <span>{{ byteFmt .SizeHint }}</span> 44 44 <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 45 - <a href="/{{ .RepoInfo.FullName }}/blob/{{ .Ref }}/raw/{{ .Path }}">view raw</a> 45 + <a href="/{{ .RepoInfo.FullName }}/raw/{{ .Ref }}/{{ .Path }}">view raw</a> 46 46 {{ if .RenderToggle }} 47 47 <span class="select-none px-1 md:px-2 [&:before]:content-['ยท']"></span> 48 48 <a
+95 -10
appview/pages/templates/repo/branches.html
··· 1 1 {{ define "title" }} 2 - branches | {{ .RepoInfo.FullName }} 2 + branches ยท {{ .RepoInfo.FullName }} 3 3 {{ end }} 4 4 5 5 {{ define "repoContent" }} 6 - <h3>branches</h3> 7 - <div class="refs"> 8 - {{ range .Branches }} 9 - <div> 10 - <strong>{{ .Name }}</strong> 11 - <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Name }}/">browse</a> 12 - <a href="/{{ $.RepoInfo.FullName }}/log/{{ .Name }}">log</a> 13 - </div> 14 - {{ end }} 6 + <section id="branches-table" class="overflow-x-auto"> 7 + <h2 class="font-bold text-sm mb-4 uppercase dark:text-white"> 8 + Branches 9 + </h2> 10 + 11 + <!-- desktop view (hidden on small screens) --> 12 + <table class="w-full border-collapse hidden md:table"> 13 + <thead> 14 + <tr> 15 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Name</th> 16 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Commit</th> 17 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Message</th> 18 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Date</th> 19 + </tr> 20 + </thead> 21 + <tbody> 22 + {{ range $index, $branch := .Branches }} 23 + <tr class="{{ if ne $index (sub (len $.Branches) 1) }}border-b border-gray-200 dark:border-gray-700{{ end }}"> 24 + <td class="py-3 whitespace-nowrap"> 25 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Name | urlquery }}" class="no-underline hover:underline flex items-center gap-2"> 26 + <span class="dark:text-white"> 27 + {{ .Name }} 28 + </span> 29 + {{ if .IsDefault }} 30 + <span class=" 31 + text-sm rounded 32 + bg-gray-100 dark:bg-gray-700 text-black dark:text-white 33 + font-mono 34 + px-2 mx-1/2 35 + inline-flex items-center 36 + "> 37 + default 38 + </span> 39 + {{ end }} 40 + </a> 41 + </td> 42 + <td class="py-3 whitespace-nowrap"> 43 + {{ if .Commit }} 44 + <a href="/{{ $.RepoInfo.FullName }}/commits/{{ .Name | urlquery }}" class="font-mono text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ slice .Commit.Hash.String 0 8 }}</a> 45 + {{ end }} 46 + </td> 47 + <td class="py-3 whitespace-nowrap"> 48 + {{ if .Commit }} 49 + {{ $messageParts := splitN .Commit.Message "\n\n" 2 }} 50 + <span class="text-gray-700 dark:text-gray-300">{{ index $messageParts 0 }}</span> 51 + {{ end }} 52 + </td> 53 + <td class="py-3 whitespace-nowrap text-gray-500 dark:text-gray-400"> 54 + {{ if .Commit }} 55 + {{ .Commit.Author.When | timeFmt }} 56 + {{ end }} 57 + </td> 58 + </tr> 59 + {{ end }} 60 + </tbody> 61 + </table> 62 + 63 + <!-- mobile view (visible only on small screens) --> 64 + <div class="md:hidden"> 65 + {{ range $index, $branch := .Branches }} 66 + <div class="relative p-2 {{ if ne $index (sub (len $.Branches) 1) }}border-b border-gray-200 dark:border-gray-700{{ end }}"> 67 + <div class="flex items-center justify-between"> 68 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Name | urlquery }}" class="no-underline hover:underline flex items-center gap-2"> 69 + <span class="dark:text-white font-medium"> 70 + {{ .Name }} 71 + </span> 72 + {{ if .IsDefault }} 73 + <span class=" 74 + text-xs rounded 75 + bg-gray-100 dark:bg-gray-700 text-black dark:text-white 76 + font-mono 77 + px-2 78 + inline-flex items-center 79 + "> 80 + default 81 + </span> 82 + {{ end }} 83 + </a> 84 + </div> 85 + 86 + {{ if .Commit }} 87 + <div class="text-xs text-gray-500 dark:text-gray-400 mt-1 flex items-center"> 88 + <span class="font-mono"> 89 + <a href="/{{ $.RepoInfo.FullName }}/commits/{{ .Name | urlquery }}" class="text-gray-500 dark:text-gray-400 no-underline hover:underline"> 90 + {{ slice .Commit.Hash.String 0 8 }} 91 + </a> 92 + </span> 93 + <div class="inline-block px-1 select-none after:content-['ยท']"></div> 94 + <span>{{ .Commit.Author.When | timeFmt }}</span> 95 + </div> 96 + {{ end }} 15 97 </div> 98 + {{ end }} 99 + </div> 100 + </section> 16 101 {{ end }}
+34
appview/pages/templates/repo/fragments/artifact.html
··· 1 + {{ define "repo/fragments/artifact" }} 2 + {{ $unique := .Artifact.BlobCid.String }} 3 + <div id="artifact-{{ $unique }}" class="flex items-center justify-between p-2 border-b border-gray-200 dark:border-gray-700"> 4 + <div id="left-side" class="flex items-center gap-2 min-w-0 max-w-[60%]"> 5 + {{ i "box" "w-4 h-4" }} 6 + <a href="/{{ .RepoInfo.FullName }}/tags/{{ .Artifact.Tag.String }}/download/{{ .Artifact.Name | urlquery }}" class="no-underline hover:no-underline"> 7 + {{ .Artifact.Name }} 8 + </a> 9 + <span class="text-gray-500 dark:text-gray-400 pl-2 text-sm">{{ byteFmt .Artifact.Size }}</span> 10 + </div> 11 + 12 + <div id="right-side" class="text-gray-500 dark:text-gray-400 flex items-center flex-shrink-0 gap-2 text-sm"> 13 + <span title="{{ longTimeFmt .Artifact.CreatedAt }}" class="hidden md:inline">{{ timeFmt .Artifact.CreatedAt }}</span> 14 + <span title="{{ longTimeFmt .Artifact.CreatedAt }}" class=" md:hidden">{{ shortTimeFmt .Artifact.CreatedAt }}</span> 15 + 16 + <span class="select-none after:content-['ยท'] hidden md:inline"></span> 17 + <span class="truncate max-w-[100px] hidden md:inline">{{ .Artifact.MimeType }}</span> 18 + 19 + {{ if and .LoggedInUser (eq .LoggedInUser.Did .Artifact.Did) }} 20 + <button 21 + id="delete-{{ $unique }}" 22 + class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2" 23 + title="Delete artifact" 24 + hx-delete="/{{ .RepoInfo.FullName }}/tags/{{ .Artifact.Tag.String }}/{{ .Artifact.Name | urlquery }}" 25 + hx-swap="outerHTML" 26 + hx-target="#artifact-{{ $unique }}" 27 + hx-disabled-elt="#delete-{{ $unique }}" 28 + hx-confirm="Are you sure you want to delete the artifact '{{ .Artifact.Name }}'?"> 29 + {{ i "trash-2" "w-4 h-4" }} 30 + </button> 31 + {{ end }} 32 + </div> 33 + </div> 34 + {{ end }}
+7 -3
appview/pages/templates/repo/fragments/cloneInstructions.html
··· 1 1 {{ define "repo/fragments/cloneInstructions" }} 2 + {{ $knot := .RepoInfo.Knot }} 3 + {{ if eq $knot "knot1.tangled.sh" }} 4 + {{ $knot = "tangled.sh" }} 5 + {{ end }} 2 6 <section 3 - class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4" 7 + class="mt-4 p-6 rounded drop-shadow-sm bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto flex flex-col gap-4" 4 8 > 5 9 <div class="flex flex-col gap-2"> 6 10 <strong>push</strong> 7 11 <div class="md:pl-4 overflow-x-auto whitespace-nowrap"> 8 12 <code class="dark:text-gray-100" 9 13 >git remote add origin 10 - git@{{ .RepoInfo.Knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 14 + git@{{ $knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 11 15 > 12 16 </div> 13 17 </div> ··· 36 40 <div class="overflow-x-auto whitespace-nowrap flex-1"> 37 41 <code class="dark:text-gray-100" 38 42 >git clone 39 - git@{{ .RepoInfo.Knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 43 + git@{{ $knot }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code 40 44 > 41 45 </div> 42 46 </div>
+4 -17
appview/pages/templates/repo/fragments/diff.html
··· 3 3 {{ $diff := index . 1 }} 4 4 {{ $commit := $diff.Commit }} 5 5 {{ $stat := $diff.Stat }} 6 + {{ $fileTree := fileTree $diff.ChangedFiles }} 6 7 {{ $diff := $diff.Diff }} 7 8 8 9 {{ $this := $commit.This }} ··· 14 15 <strong class="text-sm uppercase dark:text-gray-200">Changed files</strong> 15 16 {{ block "statPill" $stat }} {{ end }} 16 17 </div> 17 - <div class="overflow-x-auto"> 18 - {{ range $diff }} 19 - <ul class="dark:text-gray-200"> 20 - {{ if .IsDelete }} 21 - <li><a href="#file-{{ .Name.Old }}" class="dark:hover:text-gray-300">{{ .Name.Old }}</a></li> 22 - {{ else }} 23 - <li><a href="#file-{{ .Name.New }}" class="dark:hover:text-gray-300">{{ .Name.New }}</a></li> 24 - {{ end }} 25 - </ul> 26 - {{ end }} 27 - </div> 18 + {{ block "fileTree" $fileTree }} {{ end }} 28 19 </div> 29 20 </section> 30 21 ··· 38 29 <summary class="list-none cursor-pointer sticky top-0"> 39 30 <div id="diff-file-header" class="rounded cursor-pointer bg-white dark:bg-gray-800 flex justify-between"> 40 31 <div id="left-side-items" class="p-2 flex gap-2 items-center overflow-x-auto"> 41 - <div class="flex gap-1 items-center" style="direction: ltr;"> 32 + <div class="flex gap-1 items-center"> 42 33 {{ $markerstyle := "diff-type p-1 mr-1 font-mono text-sm rounded select-none" }} 43 34 {{ if .IsNew }} 44 35 <span class="bg-green-100 text-green-700 dark:bg-green-800/50 dark:text-green-400 {{ $markerstyle }}">ADDED</span> ··· 55 46 {{ block "statPill" .Stats }} {{ end }} 56 47 </div> 57 48 58 - <div class="flex gap-2 items-center overflow-x-auto" style="direction: rtl;"> 49 + <div class="flex gap-2 items-center overflow-x-auto"> 59 50 {{ if .IsDelete }} 60 51 <a class="dark:text-white whitespace-nowrap overflow-x-auto" {{if $this }}href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.Old }}"{{end}}> 61 52 {{ .Name.Old }} ··· 101 92 {{ else if .IsCopy }} 102 93 <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 103 94 This file has been copied. 104 - </p> 105 - {{ else if .IsRename }} 106 - <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 107 - This file has been renamed. 108 95 </p> 109 96 {{ else if .IsBinary }} 110 97 <p class="text-center text-gray-400 dark:text-gray-500 p-4">
+27
appview/pages/templates/repo/fragments/filetree.html
··· 1 + {{ define "fileTree" }} 2 + {{ if and .Name .IsDirectory }} 3 + <details open> 4 + <summary class="cursor-pointer list-none pt-1"> 5 + <span class="tree-directory inline-flex items-center gap-2 "> 6 + {{ i "folder" "size-4 fill-current" }} 7 + <span class="filename text-black dark:text-white">{{ .Name }}</span> 8 + </span> 9 + </summary> 10 + <div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700"> 11 + {{ range $child := .Children }} 12 + {{ block "fileTree" $child }} {{ end }} 13 + {{ end }} 14 + </div> 15 + </details> 16 + {{ else if .Name }} 17 + <div class="tree-file flex items-center gap-2 pt-1"> 18 + {{ i "file" "size-4" }} 19 + <a href="#file-{{ .Path }}" class="filename text-black dark:text-white no-underline hover:underline">{{ .Name }}</a> 20 + </div> 21 + {{ else }} 22 + {{ range $child := .Children }} 23 + {{ block "fileTree" $child }} {{ end }} 24 + {{ end }} 25 + {{ end }} 26 + {{ end }} 27 +
+2 -7
appview/pages/templates/repo/fragments/interdiff.html
··· 1 1 {{ define "repo/fragments/interdiff" }} 2 2 {{ $repo := index . 0 }} 3 3 {{ $x := index . 1 }} 4 + {{ $fileTree := fileTree $x.AffectedFiles }} 4 5 {{ $diff := $x.Files }} 5 6 6 7 <section class="mt-6 p-6 border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm"> ··· 8 9 <div class="flex gap-2 items-center"> 9 10 <strong class="text-sm uppercase dark:text-gray-200">files</strong> 10 11 </div> 11 - <div class="overflow-x-auto"> 12 - <ul class="dark:text-gray-200"> 13 - {{ range $diff }} 14 - <li><a href="#file-{{ .Name }}" class="dark:hover:text-gray-300">{{ .Name }}</a></li> 15 - {{ end }} 16 - </ul> 17 - </div> 12 + {{ block "fileTree" $fileTree }} {{ end }} 18 13 </div> 19 14 </section> 20 15
+13 -12
appview/pages/templates/repo/fragments/repoActions.html
··· 2 2 <div class="flex items-center gap-2 z-auto"> 3 3 <button 4 4 id="starBtn" 5 - class="btn disabled:opacity-50 disabled:cursor-not-allowed" 5 + class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group" 6 6 {{ if .IsStarred }} 7 7 hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}" 8 8 {{ else }} ··· 14 14 hx-swap="outerHTML" 15 15 hx-disabled-elt="#starBtn" 16 16 > 17 - <div class="flex gap-2 items-center"> 18 - {{ if .IsStarred }} 19 - {{ i "star" "w-4 h-4 fill-current" }} 20 - {{ else }} 21 - {{ i "star" "w-4 h-4" }} 22 - {{ end }} 23 - <span class="text-sm"> 24 - {{ .Stats.StarCount }} 25 - </span> 26 - </div> 17 + {{ if .IsStarred }} 18 + {{ i "star" "w-4 h-4 fill-current" }} 19 + {{ else }} 20 + {{ i "star" "w-4 h-4" }} 21 + {{ end }} 22 + <span class="text-sm"> 23 + {{ .Stats.StarCount }} 24 + </span> 25 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 27 26 </button> 28 27 {{ if .DisableFork }} 29 28 <button ··· 36 35 </button> 37 36 {{ else }} 38 37 <a 39 - class="btn text-sm no-underline hover:no-underline flex items-center gap-2" 38 + class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group" 39 + hx-boost="true" 40 40 href="/{{ .FullName }}/fork" 41 41 > 42 42 {{ i "git-fork" "w-4 h-4" }} 43 43 fork 44 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 44 45 </a> 45 46 {{ end }} 46 47 </div>
+222 -124
appview/pages/templates/repo/index.html
··· 29 29 30 30 {{ define "repoContent" }} 31 31 <main> 32 - {{ block "branchSelector" . }}{{ end }} 32 + <div class="flex items-center justify-between pb-5"> 33 + {{ block "branchSelector" . }}{{ end }} 34 + <div class="flex md:hidden items-center gap-4"> 35 + <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1"> 36 + {{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }} 37 + </a> 38 + <a href="/{{ .RepoInfo.FullName }}/branches" class="inline-flex items-center text-sm gap-1"> 39 + {{ i "git-branch" "w-4" "h-4" }} {{ len .Branches }} 40 + </a> 41 + <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex items-center text-sm gap-1"> 42 + {{ i "tags" "w-4" "h-4" }} {{ len .Tags }} 43 + </a> 44 + </div> 45 + </div> 33 46 <div class="grid grid-cols-1 md:grid-cols-2 gap-2"> 34 47 {{ block "fileTree" . }}{{ end }} 35 - {{ block "commitLog" . }}{{ end }} 48 + {{ block "rightInfo" . }}{{ end }} 36 49 </div> 37 50 </main> 38 51 {{ end }} 39 52 40 53 {{ define "branchSelector" }} 41 - <div class="flex justify-between pb-5"> 42 - <select 43 - onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 44 - class="p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 45 - > 46 - <optgroup label="branches" class="bold text-sm"> 47 - {{ range .Branches }} 48 - <option 49 - value="{{ .Reference.Name }}" 50 - class="py-1" 51 - {{ if eq .Reference.Name $.Ref }} 52 - selected 53 - {{ end }} 54 - > 55 - {{ .Reference.Name }} 56 - </option> 57 - {{ end }} 58 - </optgroup> 59 - <optgroup label="tags" class="bold text-sm"> 60 - {{ range .Tags }} 61 - <option 62 - value="{{ .Reference.Name }}" 63 - class="py-1" 64 - {{ if eq .Reference.Name $.Ref }} 65 - selected 66 - {{ end }} 67 - > 68 - {{ .Reference.Name }} 69 - </option> 70 - {{ else }} 71 - <option class="py-1" disabled>no tags found</option> 72 - {{ end }} 73 - </optgroup> 74 - </select> 75 - <a 76 - href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" 77 - class="ml-2 no-underline flex items-center gap-2 text-sm uppercase font-bold dark:text-white" 78 - > 79 - {{ i "logs" "w-4 h-4" }} 80 - {{ .TotalCommits }} 81 - {{ if eq .TotalCommits 1 }}commit{{ else }}commits{{ end }} 82 - </a> 83 - </div> 54 + <select 55 + onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)" 56 + class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700" 57 + > 58 + <optgroup label="branches ({{len .Branches}})" class="bold text-sm"> 59 + {{ range .Branches }} 60 + <option 61 + value="{{ .Reference.Name }}" 62 + class="py-1" 63 + {{ if eq .Reference.Name $.Ref }} 64 + selected 65 + {{ end }} 66 + > 67 + {{ .Reference.Name }} 68 + </option> 69 + {{ end }} 70 + </optgroup> 71 + <optgroup label="tags ({{len .Tags}})" class="bold text-sm"> 72 + {{ range .Tags }} 73 + <option 74 + value="{{ .Reference.Name }}" 75 + class="py-1" 76 + {{ if eq .Reference.Name $.Ref }} 77 + selected 78 + {{ end }} 79 + > 80 + {{ .Reference.Name }} 81 + </option> 82 + {{ else }} 83 + <option class="py-1" disabled>no tags found</option> 84 + {{ end }} 85 + </optgroup> 86 + </select> 84 87 {{ end }} 85 88 86 89 {{ define "fileTree" }} ··· 100 103 class="{{ $linkstyle }}" 101 104 > 102 105 <div class="flex items-center gap-2"> 103 - {{ i "folder" "w-3 h-3 fill-current" }} 106 + {{ i "folder" "size-4 fill-current" }} 104 107 {{ .Name }} 105 108 </div> 106 109 </a> ··· 122 125 class="{{ $linkstyle }}" 123 126 > 124 127 <div class="flex items-center gap-2"> 125 - {{ i "file" "w-3 h-3" }}{{ .Name }} 128 + {{ i "file" "size-4" }}{{ .Name }} 126 129 </div> 127 130 </a> 128 131 ··· 136 139 </div> 137 140 {{ end }} 138 141 139 - {{ define "commitLog" }} 140 - <div id="commit-log" class="hidden md:block md:col-span-1"> 141 - {{ range .Commits }} 142 - <div class="relative px-2 pb-8"> 143 - <div id="commit-message"> 144 - {{ $messageParts := splitN .Message "\n\n" 2 }} 145 - <div class="text-base cursor-pointer"> 146 - <div> 147 - <div> 148 - <a 149 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 150 - class="inline no-underline hover:underline dark:text-white" 151 - >{{ index $messageParts 0 }}</a 152 - > 153 - {{ if gt (len $messageParts) 1 }} 142 + {{ define "rightInfo" }} 143 + <div id="right-info" class="hidden md:block col-span-1"> 144 + {{ block "commitLog" . }} {{ end }} 145 + {{ block "branchList" . }} {{ end }} 146 + {{ block "tagList" . }} {{ end }} 147 + </div> 148 + {{ end }} 154 149 155 - <button 156 - class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 157 - hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')" 158 - > 159 - {{ i "ellipsis" "w-3 h-3" }} 160 - </button> 161 - {{ end }} 162 - </div> 163 - {{ if gt (len $messageParts) 1 }} 164 - <p 165 - class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 166 - > 167 - {{ nl2br (index $messageParts 1) }} 168 - </p> 169 - {{ end }} 170 - </div> 171 - </div> 172 - </div> 150 + {{ define "commitLog" }} 151 + <div id="commit-log" class="md:col-span-1 px-2 pb-4"> 152 + <div class="flex justify-between items-center"> 153 + <a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="flex text-black dark:text-white items-center gap-4 pb-2 no-underline hover:no-underline group"> 154 + <div class="flex gap-2 items-center font-bold"> 155 + {{ i "logs" "w-4 h-4" }} commits 156 + </div> 157 + <span class="hidden group-hover:flex gap-2 items-center text-sm text-gray-500 dark:text-gray-400 "> 158 + view {{ .TotalCommits }} commits {{ i "chevron-right" "w-4 h-4" }} 159 + </span> 160 + </a> 161 + </div> 162 + <div class="flex flex-col gap-6"> 163 + {{ range .CommitsTrunc }} 164 + <div> 165 + <div id="commit-message"> 166 + {{ $messageParts := splitN .Message "\n\n" 2 }} 167 + <div class="text-base cursor-pointer"> 168 + <div> 169 + <div> 170 + <a 171 + href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 172 + class="inline no-underline hover:underline dark:text-white" 173 + >{{ index $messageParts 0 }}</a 174 + > 175 + {{ if gt (len $messageParts) 1 }} 173 176 174 - <div class="text-xs text-gray-500 dark:text-gray-400"> 175 - <span class="font-mono"> 176 - <a 177 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 178 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 179 - >{{ slice .Hash.String 0 8 }}</a></span> 180 - <span 181 - class="mx-2 before:content-['ยท'] before:select-none" 182 - ></span> 183 - <span> 184 - {{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }} 185 - <a 186 - href="{{ if $didOrHandle }} 187 - /{{ $didOrHandle }} 188 - {{ else }} 189 - mailto:{{ .Author.Email }} 190 - {{ end }}" 191 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 192 - >{{ if $didOrHandle }} 193 - {{ $didOrHandle }} 194 - {{ else }} 195 - {{ .Author.Name }} 196 - {{ end }}</a 197 - > 198 - </span> 199 - <div 200 - class="inline-block px-1 select-none after:content-['ยท']" 201 - ></div> 202 - <span>{{ timeFmt .Author.When }}</span> 203 - {{ $tagsForCommit := index $.TagMap .Hash.String }} 204 - {{ if gt (len $tagsForCommit) 0 }} 205 - <div 206 - class="inline-block px-1 select-none after:content-['ยท']" 207 - ></div> 208 - {{ end }} 209 - {{ range $tagsForCommit }} 210 - <span 211 - class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center" 212 - > 213 - {{ . }} 214 - </span> 215 - {{ end }} 216 - </div> 177 + <button 178 + class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 179 + hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')" 180 + > 181 + {{ i "ellipsis" "w-3 h-3" }} 182 + </button> 183 + {{ end }} 217 184 </div> 185 + {{ if gt (len $messageParts) 1 }} 186 + <p 187 + class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 188 + > 189 + {{ nl2br (index $messageParts 1) }} 190 + </p> 191 + {{ end }} 192 + </div> 193 + </div> 194 + </div> 195 + 196 + <div class="text-xs text-gray-500 dark:text-gray-400"> 197 + <span class="font-mono"> 198 + <a 199 + href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 200 + class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 201 + >{{ slice .Hash.String 0 8 }}</a 202 + ></span 203 + > 204 + <span 205 + class="mx-2 before:content-['ยท'] before:select-none" 206 + ></span> 207 + <span> 208 + {{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }} 209 + <a 210 + href="{{ if $didOrHandle }} 211 + /{{ $didOrHandle }} 212 + {{ else }} 213 + mailto:{{ .Author.Email }} 214 + {{ end }}" 215 + class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 216 + >{{ if $didOrHandle }} 217 + {{ $didOrHandle }} 218 + {{ else }} 219 + {{ .Author.Name }} 220 + {{ end }}</a 221 + > 222 + </span> 223 + <div 224 + class="inline-block px-1 select-none after:content-['ยท']" 225 + ></div> 226 + <span>{{ timeFmt .Author.When }}</span> 227 + {{ $tagsForCommit := index $.TagMap .Hash.String }} 228 + {{ if gt (len $tagsForCommit) 0 }} 229 + <div 230 + class="inline-block px-1 select-none after:content-['ยท']" 231 + ></div> 232 + {{ end }} 233 + {{ range $tagsForCommit }} 234 + <span 235 + class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center" 236 + > 237 + {{ . }} 238 + </span> 239 + {{ end }} 240 + </div> 241 + </div> 242 + {{ end }} 243 + </div> 244 + </div> 245 + {{ end }} 246 + 247 + {{ define "branchList" }} 248 + {{ if gt (len .BranchesTrunc) 0 }} 249 + <div id="branches" class="md:col-span-1 px-2 py-4 border-t border-gray-200 dark:border-gray-700"> 250 + <a href="/{{ .RepoInfo.FullName }}/branches" class="flex text-black dark:text-white items-center gap-4 pb-2 no-underline hover:no-underline group"> 251 + <div class="flex gap-2 items-center font-bold"> 252 + {{ i "git-branch" "w-4 h-4" }} branches 253 + </div> 254 + <span class="hidden group-hover:flex gap-2 items-center text-sm text-gray-500 dark:text-gray-400 "> 255 + view {{ len .Branches }} branches {{ i "chevron-right" "w-4 h-4" }} 256 + </span> 257 + </a> 258 + <div class="flex flex-col gap-1"> 259 + {{ range .BranchesTrunc }} 260 + <div class="text-base flex items-center gap-2"> 261 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Reference.Name | urlquery }}" 262 + class="inline no-underline hover:underline dark:text-white"> 263 + {{ .Reference.Name }} 264 + </a> 265 + {{ if .Commit }} 266 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 267 + <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .Commit.Author.When }}</time> 268 + {{ end }} 269 + {{ if .IsDefault }} 270 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 271 + <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-xs font-mono">default</span> 272 + {{ end }} 273 + </div> 218 274 {{ end }} 275 + </div> 219 276 </div> 277 + {{ end }} 278 + {{ end }} 279 + 280 + {{ define "tagList" }} 281 + {{ if gt (len .TagsTrunc) 0 }} 282 + <div id="tags" class="md:col-span-1 px-2 py-4 border-t border-gray-200 dark:border-gray-700"> 283 + <div class="flex justify-between items-center"> 284 + <a href="/{{ .RepoInfo.FullName }}/tags" class="flex text-black dark:text-white items-center gap-4 pb-2 no-underline hover:no-underline group"> 285 + <div class="flex gap-2 items-center font-bold"> 286 + {{ i "tags" "w-4 h-4" }} tags 287 + </div> 288 + <span class="hidden group-hover:flex gap-2 items-center text-sm text-gray-500 dark:text-gray-400 "> 289 + view {{ len .Tags }} tags {{ i "chevron-right" "w-4 h-4" }} 290 + </span> 291 + </a> 292 + </div> 293 + <div class="flex flex-col gap-1"> 294 + {{ range $idx, $tag := .TagsTrunc }} 295 + {{ with $tag }} 296 + <div> 297 + <div class="text-base flex items-center gap-2"> 298 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Reference.Name | urlquery }}" 299 + class="inline no-underline hover:underline dark:text-white"> 300 + {{ .Reference.Name }} 301 + </a> 302 + </div> 303 + <div> 304 + {{ with .Tag }} 305 + <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .Tagger.When }}</time> 306 + {{ end }} 307 + {{ if eq $idx 0 }} 308 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 309 + <span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 text-xs font-mono">latest</span> 310 + {{ end }} 311 + </div> 312 + </div> 313 + {{ end }} 314 + {{ end }} 315 + </div> 316 + </div> 317 + {{ end }} 220 318 {{ end }} 221 319 222 320 {{ define "repoAfter" }} 223 321 {{- if .HTMLReadme }} 224 322 <section 225 - class="mt-4 p-6 rounded bg-white dark:bg-gray-800 dark:text-white w-full mx-auto overflow-auto {{ if not .Raw }} 323 + class="p-6 mt-4 rounded-br rounded-bl bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm w-full mx-auto overflow-auto {{ if not .Raw }} 226 324 prose dark:prose-invert dark:[&_pre]:bg-gray-900 227 325 dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 228 326 dark:[&_pre]:border dark:[&_pre]:border-gray-700
+64 -17
appview/pages/templates/repo/issues/issues.html
··· 1 1 {{ define "title" }}issues &middot; {{ .RepoInfo.FullName }}{{ end }} 2 2 3 3 {{ define "repoContent" }} 4 - <div class="flex justify-between items-center"> 5 - <p> 6 - filtering 7 - <select class="border p-1 bg-white border-gray-200 dark:bg-gray-800 dark:border-gray-700" onchange="window.location.href = '/{{ .RepoInfo.FullName }}/issues?state=' + this.value"> 8 - <option value="open" {{ if .FilteringByOpen }}selected{{ end }}>open ({{ .RepoInfo.Stats.IssueCount.Open }})</option> 9 - <option value="closed" {{ if not .FilteringByOpen }}selected{{ end }}>closed ({{ .RepoInfo.Stats.IssueCount.Closed }})</option> 10 - </select> 11 - issues 12 - </p> 13 - <a 14 - href="/{{ .RepoInfo.FullName }}/issues/new" 15 - class="btn text-sm flex items-center gap-2 no-underline hover:no-underline"> 16 - {{ i "circle-plus" "w-4 h-4" }} 17 - <span>new</span> 18 - </a> 19 - </div> 20 - <div class="error" id="issues"></div> 4 + <div class="flex justify-between items-center gap-4"> 5 + <div class="flex gap-4"> 6 + <a 7 + href="?state=open" 8 + class="flex items-center gap-2 {{ if .FilteringByOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}" 9 + > 10 + {{ i "circle-dot" "w-4 h-4" }} 11 + <span>{{ .RepoInfo.Stats.IssueCount.Open }} open</span> 12 + </a> 13 + <a 14 + href="?state=closed" 15 + class="flex items-center gap-2 {{ if not .FilteringByOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}" 16 + > 17 + {{ i "ban" "w-4 h-4" }} 18 + <span>{{ .RepoInfo.Stats.IssueCount.Closed }} closed</span> 19 + </a> 20 + </div> 21 + <a 22 + href="/{{ .RepoInfo.FullName }}/issues/new" 23 + class="btn text-sm flex items-center justify-center gap-2 no-underline hover:no-underline" 24 + > 25 + {{ i "circle-plus" "w-4 h-4" }} 26 + <span>new</span> 27 + </a> 28 + </div> 29 + <div class="error" id="issues"></div> 21 30 {{ end }} 22 31 23 32 {{ define "repoAfter" }} ··· 69 78 </p> 70 79 </div> 71 80 {{ end }} 81 + </div> 82 + 83 + {{ block "pagination" . }} {{ end }} 84 + 85 + {{ end }} 86 + 87 + {{ define "pagination" }} 88 + <div class="flex justify-end mt-4 gap-2"> 89 + {{ $currentState := "closed" }} 90 + {{ if .FilteringByOpen }} 91 + {{ $currentState = "open" }} 92 + {{ end }} 93 + 94 + {{ if gt .Page.Offset 0 }} 95 + {{ $prev := .Page.Previous }} 96 + <a 97 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 98 + hx-boost="true" 99 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}" 100 + > 101 + {{ i "chevron-left" "w-4 h-4" }} 102 + previous 103 + </a> 104 + {{ else }} 105 + <div></div> 106 + {{ end }} 107 + 108 + {{ if eq (len .Issues) .Page.Limit }} 109 + {{ $next := .Page.Next }} 110 + <a 111 + class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 112 + hx-boost="true" 113 + href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}" 114 + > 115 + next 116 + {{ i "chevron-right" "w-4 h-4" }} 117 + </a> 118 + {{ end }} 72 119 </div> 73 120 {{ end }}
+130 -135
appview/pages/templates/repo/log.html
··· 1 1 {{ define "title" }}commits &middot; {{ .RepoInfo.FullName }}{{ end }} 2 2 3 3 {{ define "repoContent" }} 4 - <section id="commit-message"> 5 - {{ $commit := index .Commits 0 }} 6 - {{ $messageParts := splitN $commit.Message "\n\n" 2 }} 7 - <div> 8 - <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white"> 9 - <p class="pb-5">{{ index $messageParts 0 }}</p> 10 - {{ if gt (len $messageParts) 1 }} 11 - <p class="mt-1 text-sm cursor-text pb-5"> 12 - {{ nl2br (unwrapText (index $messageParts 1)) }} 13 - </p> 14 - {{ end }} 15 - </a> 16 - </div> 4 + <section id="commit-table" class="overflow-x-auto"> 5 + <h2 class="font-bold text-sm mb-4 uppercase dark:text-white"> 6 + commits 7 + </h2> 8 + 9 + <!-- desktop view (hidden on small screens) --> 10 + <table class="w-full border-collapse hidden md:table"> 11 + <thead> 12 + <tr> 13 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Author</th> 14 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Commit</th> 15 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Message</th> 16 + <th class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">Date</th> 17 + </tr> 18 + </thead> 19 + <tbody> 20 + {{ range $index, $commit := .Commits }} 21 + {{ $messageParts := splitN $commit.Message "\n\n" 2 }} 22 + <tr class="{{ if ne $index (sub (len $.Commits) 1) }}border-b border-gray-200 dark:border-gray-700{{ end }}"> 23 + <td class=" py-3 align-top"> 24 + {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 25 + {{ if $didOrHandle }} 26 + <a href="/{{ $didOrHandle }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $didOrHandle }}</a> 27 + {{ else }} 28 + <a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a> 29 + {{ end }} 30 + </td> 31 + <td class=" py-3 align-top font-mono flex items-end"> 32 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ slice $commit.Hash.String 0 8 }}</a> 33 + <div class="inline-flex"> 34 + <button class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" 35 + title="Copy SHA" 36 + onclick="navigator.clipboard.writeText('{{ $commit.Hash.String }}'); this.innerHTML=`{{ i "copy-check" "w-4 h-4" }}`; setTimeout(() => this.innerHTML=`{{ i "copy" "w-4 h-4" }}`, 1500)"> 37 + {{ i "copy" "w-4 h-4" }} 38 + </button> 39 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $commit.Hash.String }}" class="p-1 mx-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" title="Browse repository at this commit"> 40 + {{ i "folder-code" "w-4 h-4" }} 41 + </a> 42 + </div> 43 + </td> 44 + <td class=" py-3 align-top"> 45 + <div> 46 + <div class="flex items-center justify-start"> 47 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a> 48 + {{ if gt (len $messageParts) 1 }} 49 + <button class="ml-2 py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')">{{ i "ellipsis" "w-3 h-3" }}</button> 50 + {{ end }} 17 51 18 - <div class="text-sm text-gray-500 dark:text-gray-400"> 19 - <span class="font-mono"> 20 - <a 21 - href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" 22 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 23 - >{{ slice $commit.Hash.String }}</a 24 - > 25 - </span> 26 - <span class="mx-2 before:content-['ยท'] before:select-none"></span> 27 - <span> 28 - {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 29 - {{ if $didOrHandle }} 30 - <a 31 - href="/{{ $didOrHandle }}" 32 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 33 - >{{ $didOrHandle }}</a 34 - > 35 - {{ else }} 36 - <a 37 - href="mailto:{{ $commit.Author.Email }}" 38 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 39 - >{{ $commit.Author.Name }}</a 40 - > 52 + 53 + {{ if index $.TagMap $commit.Hash.String }} 54 + {{ range $tag := index $.TagMap $commit.Hash.String }} 55 + <span class="ml-2 text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 inline-flex items-center"> 56 + {{ $tag }} 57 + </span> 58 + {{ end }} 59 + {{ end }} 60 + 61 + </div> 62 + 63 + {{ if gt (len $messageParts) 1 }} 64 + <p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p> 65 + {{ end }} 66 + </td> 67 + <td class=" py-3 align-top text-gray-500 dark:text-gray-400">{{ timeFmt $commit.Author.When }}</td> 68 + </tr> 41 69 {{ end }} 42 - </span> 43 - <div 44 - class="inline-block px-1 select-none after:content-['ยท']" 45 - ></div> 46 - <span>{{ timeFmt $commit.Author.When }}</span> 47 - </div> 48 - </section> 49 - {{ end }} 70 + </tbody> 71 + </table> 50 72 51 - {{ define "repoAfter" }} 52 - <main> 53 - <div id="commit-log" class="flex-1 relative"> 54 - <div class="absolute left-8 top-0 bottom-0 w-px bg-gray-300 dark:bg-gray-600"></div> 55 - {{ $end := length .Commits }} 56 - {{ $commits := subslice .Commits 1 $end }} 57 - {{ range $commits }} 58 - <div class="flex flex-row justify-between items-center"> 59 - <div 60 - class="relative w-full px-4 py-4 mt-4 rounded-sm bg-white dark:bg-gray-800" 61 - > 62 - <div id="commit-message"> 63 - {{ $messageParts := splitN .Message "\n\n" 2 }} 64 - <div class="text-base cursor-pointer"> 65 - <div> 66 - <div> 67 - <a 68 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 69 - class="inline no-underline hover:underline dark:text-white" 70 - >{{ index $messageParts 0 }}</a 71 - > 73 + <!-- mobile view (visible only on small screens) --> 74 + <div class="md:hidden"> 75 + {{ range $index, $commit := .Commits }} 76 + <div class="relative p-2 {{ if ne $index (sub (len $.Commits) 1) }}border-b border-gray-200 dark:border-gray-700{{ end }}"> 77 + <div id="commit-message"> 78 + {{ $messageParts := splitN $commit.Message "\n\n" 2 }} 79 + <div class="text-base cursor-pointer"> 80 + <div> 81 + <div class="flex items-center justify-between"> 82 + <div class="flex-1"> 83 + <div class="inline"> 84 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" 85 + class="inline no-underline hover:underline dark:text-white"> 86 + {{ index $messageParts 0 }} 87 + </a> 72 88 {{ if gt (len $messageParts) 1 }} 73 - 74 - <button 75 - class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600 rounded" 76 - hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')" 77 - > 78 - {{ i "ellipsis" "w-3 h-3" }} 89 + <button 90 + class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600 ml-2" 91 + hx-on:click="this.parentElement.nextElementSibling.classList.toggle('hidden')"> 92 + {{ i "ellipsis" "w-3 h-3" }} 79 93 </button> 80 94 {{ end }} 95 + 96 + {{ if index $.TagMap $commit.Hash.String }} 97 + {{ range $tag := index $.TagMap $commit.Hash.String }} 98 + <span class="ml-2 text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 inline-flex items-center"> 99 + {{ $tag }} 100 + </span> 101 + {{ end }} 102 + {{ end }} 81 103 </div> 104 + 82 105 {{ if gt (len $messageParts) 1 }} 83 - <p 84 - class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300" 85 - > 106 + <p class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300"> 86 107 {{ nl2br (index $messageParts 1) }} 87 108 </p> 88 109 {{ end }} 89 110 </div> 111 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $commit.Hash.String }}" 112 + class="p-1 mr-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded" 113 + title="Browse repository at this commit"> 114 + {{ i "folder-code" "w-4 h-4" }} 115 + </a> 90 116 </div> 91 117 </div> 92 - 93 - <div class="text-sm text-gray-500 dark:text-gray-400 mt-3"> 94 - <span class="font-mono"> 95 - <a 96 - href="/{{ $.RepoInfo.FullName }}/commit/{{ .Hash.String }}" 97 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 98 - >{{ slice .Hash.String 0 8 }}</a 99 - > 100 - </span> 101 - <span 102 - class="mx-2 before:content-['ยท'] before:select-none" 103 - ></span> 104 - <span> 105 - {{ $didOrHandle := index $.EmailToDidOrHandle .Author.Email }} 106 - {{ if $didOrHandle }} 107 - <a 108 - href="/{{ $didOrHandle }}" 109 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 110 - >{{ $didOrHandle }}</a 111 - > 112 - {{ else }} 113 - <a 114 - href="mailto:{{ .Author.Email }}" 115 - class="text-gray-500 dark:text-gray-400 no-underline hover:underline" 116 - >{{ .Author.Name }}</a 117 - > 118 - {{ end }} 119 - </span> 120 - <div 121 - class="inline-block px-1 select-none after:content-['ยท']" 122 - ></div> 123 - <span>{{ timeFmt .Author.When }}</span> 124 - </div> 125 118 </div> 126 119 </div> 127 - {{ end }} 128 - </div> 129 120 130 - {{ $commits_len := len .Commits }} 131 - <div class="flex justify-end mt-4 gap-2"> 132 - {{ if gt .Page 1 }} 133 - <a 134 - class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 135 - hx-boost="true" 136 - onclick="window.location.href = window.location.pathname + '?page={{ sub .Page 1 }}'" 137 - > 138 - {{ i "chevron-left" "w-4 h-4" }} 139 - previous 140 - </a> 141 - {{ else }} 142 - <div></div> 143 - {{ end }} 121 + <div class="text-xs text-gray-500 dark:text-gray-400"> 122 + <span class="font-mono"> 123 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" 124 + class="text-gray-500 dark:text-gray-400 no-underline hover:underline"> 125 + {{ slice $commit.Hash.String 0 8 }} 126 + </a> 127 + </span> 128 + <span class="mx-2 before:content-['ยท'] before:select-none"></span> 129 + <span> 130 + {{ $didOrHandle := index $.EmailToDidOrHandle $commit.Author.Email }} 131 + <a href="{{ if $didOrHandle }}/{{ $didOrHandle }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}" 132 + class="text-gray-500 dark:text-gray-400 no-underline hover:underline"> 133 + {{ if $didOrHandle }}{{ $didOrHandle }}{{ else }}{{ $commit.Author.Name }}{{ end }} 134 + </a> 135 + </span> 136 + <div class="inline-block px-1 select-none after:content-['ยท']"></div> 137 + <span>{{ shortTimeFmt $commit.Author.When }}</span> 138 + </div> 139 + </div> 140 + {{ end }} 141 + </div> 142 + </section> 144 143 145 - {{ if eq $commits_len 30 }} 146 - <a 147 - class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" 148 - hx-boost="true" 149 - onclick="window.location.href = window.location.pathname + '?page={{ add .Page 1 }}'" 150 - > 151 - next 152 - {{ i "chevron-right" "w-4 h-4" }} 153 - </a> 154 - {{ end }} 155 - </div> 156 - </main> 144 + {{ end }} 145 + 146 + {{ define "repoAfter" }} 147 + {{ $commits_len := len .Commits }} 148 + <div class="flex justify-end mt-4 gap-2"> 149 + {{ if gt .Page 1 }}<a class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" hx-boost="true" onclick="window.location.href = window.location.pathname + '?page={{ sub .Page 1 }}'">{{ i "chevron-left" "w-4 h-4" }} previous</a>{{ else }}<div></div>{{ end }} 150 + {{ if eq $commits_len 60 }}<a class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700" hx-boost="true" onclick="window.location.href = window.location.pathname + '?page={{ add .Page 1 }}'">next {{ i "chevron-right" "w-4 h-4" }}</a>{{ end }} 151 + </div> 157 152 {{ end }}
+7 -2
appview/pages/templates/repo/new.html
··· 5 5 <p class="text-xl font-bold dark:text-white">Create a new repository</p> 6 6 </div> 7 7 <div class="p-6 bg-white dark:bg-gray-800 drop-shadow-sm rounded"> 8 - <form hx-post="/repo/new" class="space-y-12" hx-swap="none"> 8 + <form hx-post="/repo/new" class="space-y-12" hx-swap="none" hx-indicator="#spinner"> 9 9 <div class="space-y-2"> 10 10 <label for="name" class="-mb-1 dark:text-white">Repository name</label> 11 11 <input ··· 60 60 </fieldset> 61 61 62 62 <div class="space-y-2"> 63 - <button type="submit" class="btn">create repo</button> 63 + <button type="submit" class="btn flex gap-2 items-center"> 64 + create repo 65 + <span id="spinner" class="group"> 66 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 67 + </span> 68 + </button> 64 69 <div id="repo" class="error"></div> 65 70 </div> 66 71 </form>
+14 -9
appview/pages/templates/repo/pulls/fragments/pullActions.html
··· 17 17 hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/round/{{ $roundNumber }}/comment" 18 18 hx-target="#actions-{{$roundNumber}}" 19 19 hx-swap="outerHtml" 20 - class="btn p-2 flex items-center gap-2 no-underline hover:no-underline"> 20 + class="btn p-2 flex items-center gap-2 no-underline hover:no-underline group"> 21 21 {{ i "message-square-plus" "w-4 h-4" }} 22 22 <span>comment</span> 23 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 23 24 </button> 24 25 {{ if and $isPushAllowed $isOpen $isLastRound }} 25 26 {{ $disabled := "" }} ··· 30 31 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge" 31 32 hx-swap="none" 32 33 hx-confirm="Are you sure you want to merge pull #{{ .Pull.PullId }} into the `{{ .Pull.TargetBranch }}` branch?" 33 - class="btn p-2 flex items-center gap-2" {{ $disabled }}> 34 + class="btn p-2 flex items-center gap-2 group" {{ $disabled }}> 34 35 {{ i "git-merge" "w-4 h-4" }} 35 - <span>merge</span> 36 + <span>merge</span> 37 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 36 38 </button> 37 39 {{ end }} 38 40 ··· 51 53 {{ end }} 52 54 53 55 hx-disabled-elt="#resubmitBtn" 54 - class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }} 56 + class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed group" {{ $disabled }} 55 57 56 58 {{ if $disabled }} 57 59 title="Update this branch to resubmit this pull request" ··· 59 61 title="Resubmit this pull request" 60 62 {{ end }} 61 63 > 62 - {{ i "rotate-ccw" "w-4 h-4" }} 64 + {{ i "rotate-ccw" "w-4 h-4" }} 63 65 <span>resubmit</span> 66 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 64 67 </button> 65 68 {{ end }} 66 69 ··· 68 71 <button 69 72 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/close" 70 73 hx-swap="none" 71 - class="btn p-2 flex items-center gap-2"> 72 - {{ i "ban" "w-4 h-4" }} 74 + class="btn p-2 flex items-center gap-2 group"> 75 + {{ i "ban" "w-4 h-4" }} 73 76 <span>close</span> 77 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 74 78 </button> 75 79 {{ end }} 76 80 ··· 78 82 <button 79 83 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/reopen" 80 84 hx-swap="none" 81 - class="btn p-2 flex items-center gap-2"> 82 - {{ i "refresh-ccw-dot" "w-4 h-4" }} 85 + class="btn p-2 flex items-center gap-2 group"> 86 + {{ i "refresh-ccw-dot" "w-4 h-4" }} 83 87 <span>reopen</span> 88 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 84 89 </button> 85 90 {{ end }} 86 91 </div>
+10 -12
appview/pages/templates/repo/pulls/fragments/pullHeader.html
··· 42 42 </span> 43 43 </span> 44 44 {{ if not .Pull.IsPatchBased }} 45 - <span>from 46 - {{ if not .Pull.IsBranchBased }} 47 - <a href="/{{ $owner }}/{{ .PullSourceRepo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSourceRepo.Name }}</a> 48 - {{ end }} 49 - 50 - {{ $fullRepo := .RepoInfo.FullName }} 51 - {{ if not .Pull.IsBranchBased }} 52 - {{ $fullRepo = printf "%s/%s" $owner .PullSourceRepo.Name }} 53 - {{ end }} 45 + from 54 46 <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 55 - <a href="/{{ $fullRepo }}/tree/{{ .Pull.PullSource.Branch }}" class="no-underline hover:underline">{{ .Pull.PullSource.Branch }}</a> 47 + {{ if .Pull.IsForkBased }} 48 + {{ if .Pull.PullSource.Repo }} 49 + <a href="/{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}</a>: 50 + {{- else -}} 51 + <span class="italic">[deleted fork]</span> 52 + {{- end -}} 53 + {{- end -}} 54 + {{- .Pull.PullSource.Branch -}} 56 55 </span> 57 - </span> 58 56 {{ end }} 59 57 </span> 60 58 </div> ··· 67 65 </section> 68 66 69 67 70 - {{ end }} 68 + {{ end }}
+1 -1
appview/pages/templates/repo/pulls/new.html
··· 18 18 > 19 19 <option disabled selected>target branch</option> 20 20 {{ range .Branches }} 21 - <option value="{{ .Reference.Name }}" class="py-1"> 21 + <option value="{{ .Reference.Name }}" class="py-1" {{if .IsDefault}}selected{{end}}> 22 22 {{ .Reference.Name }} 23 23 </option> 24 24 {{ end }}
+18 -9
appview/pages/templates/repo/pulls/pull.html
··· 51 51 </span> 52 52 </div> 53 53 54 - <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2" 54 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 55 55 hx-boost="true" 56 56 href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}"> 57 - {{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">view patch</span> 57 + {{ i "file-diff" "w-4 h-4" }} 58 + <span class="hidden md:inline">diff</span> 59 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 58 60 </a> 59 61 {{ if not (eq .RoundNumber 0) }} 60 - <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2" 62 + <a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group" 61 63 hx-boost="true" 62 64 href="/{{ $.RepoInfo.FullName }}/pulls/{{ $.Pull.PullId }}/round/{{.RoundNumber}}/interdiff"> 63 - {{ i "file-diff" "w-4 h-4" }} <span class="hidden md:inline">interdiff</span> 65 + {{ i "chevrons-left-right-ellipsis" "w-4 h-4 rotate-90" }} 66 + <span class="hidden md:inline">interdiff</span> 67 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 64 68 </a> 65 69 <span id="interdiff-error-{{.RoundNumber}}"></span> 66 70 {{ end }} ··· 88 92 <div class="flex items-center gap-2"> 89 93 {{ i "git-commit-horizontal" "w-4 h-4" }} 90 94 <div class="text-sm text-gray-500 dark:text-gray-400"> 91 - {{ if not $.Pull.IsPatchBased }} 92 - {{ $fullRepo := $.RepoInfo.FullName }} 93 - {{ if not $.Pull.IsBranchBased }} 94 - {{ $fullRepo = printf "%s/%s" $owner $.PullSourceRepo.Name }} 95 - {{ end }} 95 + <!-- attempt to resolve $fullRepo: this is possible only on non-deleted forks and branches --> 96 + {{ $fullRepo := "" }} 97 + {{ if and $.Pull.IsForkBased $.Pull.PullSource.Repo }} 98 + {{ $fullRepo = printf "%s/%s" $owner $.Pull.PullSource.Repo.Name }} 99 + {{ else if $.Pull.IsBranchBased }} 100 + {{ $fullRepo = $.RepoInfo.FullName }} 101 + {{ end }} 102 + 103 + <!-- if $fullRepo was resolved, link to it, otherwise just span without a link --> 104 + {{ if $fullRepo }} 96 105 <a href="/{{ $fullRepo }}/commit/{{ .SHA }}" class="font-mono text-gray-500 dark:text-gray-400">{{ slice .SHA 0 8 }}</a> 97 106 {{ else }} 98 107 <span class="font-mono">{{ slice .SHA 0 8 }}</span>
+32 -29
appview/pages/templates/repo/pulls/pulls.html
··· 2 2 3 3 {{ define "repoContent" }} 4 4 <div class="flex justify-between items-center"> 5 - <p class="dark:text-white"> 6 - filtering 7 - <select 8 - class="border p-1 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 dark:text-white" 9 - onchange="window.location.href = '/{{ .RepoInfo.FullName }}/pulls?state=' + this.value" 5 + <div class="flex gap-4"> 6 + <a 7 + href="?state=open" 8 + class="flex items-center gap-2 {{ if .FilteringBy.IsOpen }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}" 9 + > 10 + {{ i "git-pull-request" "w-4 h-4" }} 11 + <span>{{ .RepoInfo.Stats.PullCount.Open }} open</span> 12 + </a> 13 + <a 14 + href="?state=merged" 15 + class="flex items-center gap-2 {{ if .FilteringBy.IsMerged }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}" 16 + > 17 + {{ i "git-merge" "w-4 h-4" }} 18 + <span>{{ .RepoInfo.Stats.PullCount.Merged }} merged</span> 19 + </a> 20 + <a 21 + href="?state=closed" 22 + class="flex items-center gap-2 {{ if .FilteringBy.IsClosed }}font-bold {{ else }}text-gray-500 dark:text-gray-400{{ end }}" 10 23 > 11 - <option value="open" {{ if .FilteringBy.IsOpen }}selected{{ end }}> 12 - open ({{ .RepoInfo.Stats.PullCount.Open }}) 13 - </option> 14 - <option value="merged" {{ if .FilteringBy.IsMerged }}selected{{ end }}> 15 - merged ({{ .RepoInfo.Stats.PullCount.Merged }}) 16 - </option> 17 - <option value="closed" {{ if .FilteringBy.IsClosed }}selected{{ end }}> 18 - closed ({{ .RepoInfo.Stats.PullCount.Closed }}) 19 - </option> 20 - </select> 21 - pull requests 22 - </p> 24 + {{ i "ban" "w-4 h-4" }} 25 + <span>{{ .RepoInfo.Stats.PullCount.Closed }} closed</span> 26 + </a> 27 + </div> 23 28 <a 24 29 href="/{{ .RepoInfo.FullName }}/pulls/new" 25 30 class="btn text-sm flex items-center gap-2 no-underline hover:no-underline" ··· 79 84 </span> 80 85 </span> 81 86 {{ if not .IsPatchBased }} 82 - <span>from 83 - {{ if .IsForkBased }} 84 - {{ if .PullSource.Repo }} 85 - <a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a> 86 - {{ else }} 87 - <span class="italic">[deleted fork]</span> 88 - {{ end }} 89 - {{ end }} 90 - 91 - <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 92 - {{ .PullSource.Branch }} 93 - </span> 87 + from 88 + <span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center"> 89 + {{ if .IsForkBased }} 90 + {{ if .PullSource.Repo }} 91 + <a href="/{{ $owner }}/{{ .PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .PullSource.Repo.Name }}</a>: 92 + {{- else -}} 93 + <span class="italic">[deleted fork]</span> 94 + {{- end -}} 95 + {{- end -}} 96 + {{- .PullSource.Branch -}} 94 97 </span> 95 98 {{ end }} 96 99 <span class="before:content-['ยท']">
+161 -13
appview/pages/templates/repo/tags.html
··· 1 + {{ define "title" }} 2 + tags ยท {{ .RepoInfo.FullName }} 3 + {{ end }} 4 + 1 5 {{ define "repoContent" }} 2 - {{ $name := .RepoInfo.Name }} 3 - <h3>tags</h3> 4 - <div class="refs"> 5 - {{ range .Tags }} 6 - <div> 7 - <strong>{{ .Ref.Name }}</strong> 8 - <a href="/{{ $name }}/tree/{{ .Ref.Name }}/">browse</a> 9 - <a href="/{{ $name }}/log/{{ .Ref.Name }}">log</a> 10 - <a href="/{{ $name }}/archive/{{ .Ref.Name }}.tar.gz">tar.gz</a> 11 - {{ if .Message }} 12 - <pre>{{ .Message }}</pre> 13 - {{ end }} 14 - </div> 6 + <section> 7 + <h2 class="mb-4 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">tags</h2> 8 + <div class="flex flex-col py-2 gap-12 md:gap-0"> 9 + {{ range .Tags }} 10 + <div class="md:grid md:grid-cols-12 md:items-start flex flex-col"> 11 + <!-- Header column (top on mobile, left on md+) --> 12 + <div class="md:col-span-2 md:border-r border-b md:border-b-0 border-gray-200 dark:border-gray-700 w-full md:h-full"> 13 + <!-- Mobile layout: horizontal --> 14 + <div class="flex md:hidden flex-col py-2 px-2 text-xl"> 15 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Name | urlquery }}" class="no-underline hover:underline flex items-center gap-2 font-bold"> 16 + {{ i "tag" "w-4 h-4" }} 17 + {{ .Name }} 18 + </a> 19 + 20 + <div class="flex items-center gap-3 text-gray-500 dark:text-gray-400 text-sm"> 21 + {{ if .Tag }} 22 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Tag.Target.String }}" 23 + class="no-underline hover:underline text-gray-500 dark:text-gray-400"> 24 + {{ slice .Tag.Target.String 0 8 }} 25 + </a> 26 + 27 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 28 + <span>{{ .Tag.Tagger.Name }}</span> 29 + 30 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 31 + <time>{{ shortTimeFmt .Tag.Tagger.When }}</time> 32 + {{ end }} 33 + </div> 34 + </div> 35 + 36 + <!-- Desktop layout: vertical and left-aligned --> 37 + <div class="hidden md:block text-left px-2 pb-6"> 38 + <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Name | urlquery }}" class="no-underline hover:underline flex items-center gap-2 font-bold"> 39 + {{ i "tag" "w-4 h-4" }} 40 + {{ .Name }} 41 + </a> 42 + <div class="flex flex-grow flex-col text-gray-500 dark:text-gray-400 text-sm"> 43 + {{ if .Tag }} 44 + <a href="/{{ $.RepoInfo.FullName }}/commit/{{ .Tag.Target.String }}" 45 + class="no-underline hover:underline text-gray-500 dark:text-gray-400 flex items-center gap-2"> 46 + {{ i "git-commit-horizontal" "w-4 h-4" }} 47 + {{ slice .Tag.Target.String 0 8 }} 48 + </a> 49 + <span>{{ .Tag.Tagger.Name }}</span> 50 + <time>{{ timeFmt .Tag.Tagger.When }}</time> 51 + {{ end }} 52 + </div> 53 + </div> 54 + </div> 55 + 56 + <!-- Content column (bottom on mobile, right on md+) --> 57 + <div class="md:col-span-10 px-2 py-3 md:py-0 md:pb-6"> 58 + {{ if .Tag }} 59 + {{ $messageParts := splitN .Tag.Message "\n\n" 2 }} 60 + <p class="font-bold text-lg">{{ index $messageParts 0 }}</p> 61 + {{ if gt (len $messageParts) 1 }} 62 + <p class="cursor-text py-2">{{ nl2br (index $messageParts 1) }}</p> 63 + {{ end }} 64 + {{ block "artifacts" (list $ .) }} {{ end }} 65 + {{ else }} 66 + <p class="italic text-gray-500 dark:text-gray-400">no message</p> 15 67 {{ end }} 68 + </div> 16 69 </div> 70 + {{ else }} 71 + <p class="text-center text-gray-400 dark:text-gray-500 p-4"> 72 + This repository does not contain any tags. 73 + </p> 74 + {{ end }} 75 + </div> 76 + </section> 77 + {{ end }} 78 + 79 + {{ define "repoAfter" }} 80 + {{ if gt (len .DanglingArtifacts) 0 }} 81 + <section class="bg-white dark:bg-gray-800 p-6 mt-4"> 82 + {{ block "dangling" . }} {{ end }} 83 + </section> 84 + {{ end }} 85 + {{ end }} 86 + 87 + {{ define "artifacts" }} 88 + {{ $root := index . 0 }} 89 + {{ $tag := index . 1 }} 90 + {{ $isPushAllowed := $root.RepoInfo.Roles.IsPushAllowed }} 91 + {{ $artifacts := index $root.ArtifactMap $tag.Tag.Hash }} 92 + 93 + {{ if or (gt (len $artifacts) 0) $isPushAllowed }} 94 + <h2 class="my-4 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold">artifacts</h2> 95 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700"> 96 + {{ range $artifact := $artifacts }} 97 + {{ $args := dict "LoggedInUser" $root.LoggedInUser "RepoInfo" $root.RepoInfo "Artifact" $artifact }} 98 + {{ template "repo/fragments/artifact" $args }} 99 + {{ end }} 100 + {{ if $isPushAllowed }} 101 + {{ block "uploadArtifact" (list $root $tag) }} {{ end }} 102 + {{ end }} 103 + </div> 104 + {{ end }} 105 + {{ end }} 106 + 107 + {{ define "uploadArtifact" }} 108 + {{ $root := index . 0 }} 109 + {{ $tag := index . 1 }} 110 + {{ $unique := $tag.Tag.Target.String }} 111 + <form 112 + id="upload-{{$unique}}" 113 + method="post" 114 + enctype="multipart/form-data" 115 + hx-post="/{{ $root.RepoInfo.FullName }}/tags/{{ $tag.Name | urlquery }}/upload" 116 + hx-on::after-request="if(event.detail.successful) this.reset()" 117 + hx-disabled-elt="#upload-btn-{{$unique}}" 118 + hx-swap="beforebegin" 119 + hx-target="this" 120 + class="flex items-center gap-2 px-2"> 121 + <div class="flex-grow"> 122 + <input type="file" 123 + name="artifact" 124 + required 125 + class="block py-2 px-0 w-full border-none 126 + text-black dark:text-white 127 + bg-white dark:bg-gray-800 128 + file:mr-4 file:px-2 file:py-2 129 + file:rounded file:border-0 130 + file:text-sm file:font-medium 131 + file:text-gray-700 file:dark:text-gray-300 132 + file:bg-gray-200 file:dark:bg-gray-700 133 + file:hover:bg-gray-100 file:hover:dark:bg-gray-600 134 + "> 135 + </input> 136 + </div> 137 + <div class="flex justify-end"> 138 + <button 139 + type="submit" 140 + class="btn gap-2" 141 + id="upload-btn-{{$unique}}" 142 + title="Upload artifact"> 143 + {{ i "upload" "w-4 h-4" }} 144 + <span class="hidden md:inline">upload</span> 145 + </button> 146 + </div> 147 + </form> 148 + {{ end }} 149 + 150 + {{ define "dangling" }} 151 + {{ $root := . }} 152 + {{ $isPushAllowed := $root.RepoInfo.Roles.IsPushAllowed }} 153 + {{ $artifacts := $root.DanglingArtifacts }} 154 + 155 + {{ if and (gt (len $artifacts) 0) $isPushAllowed }} 156 + <h2 class="mb-2 text-sm text-left text-red-700 dark:text-red-400 uppercase font-bold">dangling artifacts</h2> 157 + <p class="mb-4">The tags that these artifacts were attached to have been deleted. These artifacts are only visible to collaborators.</p> 158 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700"> 159 + {{ range $artifact := $artifacts }} 160 + {{ $args := dict "LoggedInUser" $root.LoggedInUser "RepoInfo" $root.RepoInfo "Artifact" $artifact }} 161 + {{ template "repo/fragments/artifact" $args }} 162 + {{ end }} 163 + </div> 164 + {{ end }} 17 165 {{ end }}
+2 -2
appview/pages/templates/repo/tree.html
··· 54 54 <div class="flex justify-between items-center"> 55 55 <a href="/{{ $.BaseTreeLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 56 56 <div class="flex items-center gap-2"> 57 - {{ i "folder" "w-3 h-3 fill-current" }}{{ .Name }} 57 + {{ i "folder" "size-4 fill-current" }}{{ .Name }} 58 58 </div> 59 59 </a> 60 60 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time> ··· 69 69 <div class="flex justify-between items-center"> 70 70 <a href="/{{ $.BaseBlobLink }}/{{ .Name }}" class="{{ $linkstyle }}"> 71 71 <div class="flex items-center gap-2"> 72 - {{ i "file" "w-3 h-3" }}{{ .Name }} 72 + {{ i "file" "size-4" }}{{ .Name }} 73 73 </div> 74 74 </a> 75 75 <time class="text-xs text-gray-500 dark:text-gray-400">{{ timeFmt .LastCommit.When }}</time>
+1 -1
appview/pages/templates/timeline.html
··· 23 23 </div> 24 24 <div class="italic text-lg"> 25 25 tightly-knit social coding, <a href="/login" class="underline inline-flex gap-1 items-center">join now {{ i "arrow-right" "w-4 h-4" }}</a> 26 - <p class="pt-5 px-10 text-sm text-gray-500 dark:text-gray-400">Join our <a href="https://chat.tangled.sh">Discord</a>or IRC channel: <a href="https://web.libera.chat/#tangled"><code>#tangled</code> on Libera Chat</a>. 26 + <p class="pt-5 px-10 text-sm text-gray-500 dark:text-gray-400">Join our <a href="https://chat.tangled.sh">Discord</a> or IRC channel: <a href="https://web.libera.chat/#tangled"><code>#tangled</code> on Libera Chat</a>. 27 27 Read an introduction to Tangled <a href="https://blog.tangled.sh/intro">here</a>.</p> 28 28 </div> 29 29 </div>
+6
appview/pages/templates/user/fragments/bluesky.html
··· 1 + {{ define "user/fragments/bluesky" }} 2 + <svg class="{{.}}" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="-3 -3 30 30"> 3 + <title>Bluesky</title> 4 + <path fill="none" stroke="currentColor" d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z" stroke-width="2.25"/> 5 + </svg> 6 + {{ end }}
+111
appview/pages/templates/user/fragments/editBio.html
··· 1 + {{ define "user/fragments/editBio" }} 2 + <form 3 + hx-post="/profile/bio" 4 + class="flex flex-col gap-4 my-2 max-w-full" 5 + hx-disabled-elt="#save-btn,#cancel-btn" 6 + hx-swap="none" 7 + hx-indicator="#spinner"> 8 + <div class="flex flex-col gap-1"> 9 + {{ $description := "" }} 10 + {{ if and .Profile .Profile.Description }} 11 + {{ $description = .Profile.Description }} 12 + {{ end }} 13 + <label class="m-0 p-0" for="description">bio</label> 14 + <textarea 15 + type="text" 16 + class="py-1 px-1 w-full" 17 + name="description" 18 + rows="3" 19 + placeholder="write a bio">{{ $description }}</textarea> 20 + </div> 21 + 22 + <div class="flex flex-col gap-1"> 23 + <label class="m-0 p-0" for="location">location</label> 24 + <div class="flex items-center gap-2 w-full"> 25 + {{ $location := "" }} 26 + {{ if and .Profile .Profile.Location }} 27 + {{ $location = .Profile.Location }} 28 + {{ end }} 29 + <span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span> 30 + <input type="text" class="py-1 px-1 w-full" name="location" value="{{ $location }}"> 31 + </div> 32 + </div> 33 + 34 + <div class="flex flex-col gap-1"> 35 + <label class="m-0 p-0">social links</label> 36 + <div class="flex items-center gap-2 py-1"> 37 + {{ $includeBsky := false }} 38 + {{ if and .Profile .Profile.IncludeBluesky }} 39 + {{ $includeBsky = true }} 40 + {{ end }} 41 + <input type="checkbox" id="includeBluesky" name="includeBluesky" value="on" {{if $includeBsky}}checked{{end}}> 42 + <label for="includeBluesky" class="my-0 py-0 normal-case font-normal">Link to Bluesky account</label> 43 + </div> 44 + 45 + {{ $profile := .Profile }} 46 + {{ range $idx, $s := (sequence 5) }} 47 + {{ $link := "" }} 48 + {{ if and $profile $profile.Links }} 49 + {{ if lt $idx (len $profile.Links) }} 50 + {{ $link = index $profile.Links $idx }} 51 + {{ end }} 52 + {{ end }} 53 + 54 + <div class="flex items-center gap-2 w-full"> 55 + <span class="flex-shrink-0">{{ i "link" "size-4" }}</span> 56 + <input type="text" class="py-1 px-1 w-full" name="link{{$idx}}" value="{{ $link }}" placeholder="social link {{add $idx 1}}"> 57 + </div> 58 + {{ end }} 59 + </div> 60 + 61 + <div class="flex flex-col gap-1"> 62 + <label class="m-0 p-0">vanity stats</label> 63 + {{ range $idx, $s := (sequence 2) }} 64 + {{ $stat := "" }} 65 + {{ if and $profile $profile.Stats }} 66 + {{ if lt $idx (len $profile.Stats) }} 67 + {{ $s := index $profile.Stats $idx }} 68 + {{ $stat = $s.Kind }} 69 + {{ end }} 70 + {{ end }} 71 + 72 + {{ block "stat" (list $idx $stat) }} {{ end }} 73 + {{ end }} 74 + </div> 75 + 76 + <div class="flex items-center gap-2 justify-between"> 77 + <button id="save-btn" type="submit" class="btn p-1 w-full flex items-center gap-2 no-underline text-sm"> 78 + {{ i "check" "size-4" }} save 79 + <span id="spinner" class="group"> 80 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 81 + </span> 82 + </button> 83 + <a href="/{{.LoggedInUser.Did}}" class="w-full no-underline hover:no-underline"> 84 + <button id="cancel-btn" type="button" class="btn p-1 w-full flex items-center gap-2 no-underline text-sm"> 85 + {{ i "x" "size-4" }} cancel 86 + </button> 87 + </a> 88 + </div> 89 + </form> 90 + {{ end }} 91 + 92 + {{ define "stat" }} 93 + {{ $id := index . 0 }} 94 + {{ $stat := index . 1 }} 95 + <select class="stat-group w-full p-1 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700 text-sm" id="stat{{$id}}" name="stat{{$id}}"> 96 + <option value="">choose stat</option> 97 + {{ $stats := assoc 98 + "merged-pull-request-count" "Merged PR Count" 99 + "closed-pull-request-count" "Closed PR Count" 100 + "open-pull-request-count" "Open PR Count" 101 + "open-issue-count" "Open Issue Count" 102 + "closed-issue-count" "Closed Issue Count" 103 + "repository-count" "Repository Count" 104 + }} 105 + {{ range $s := $stats }} 106 + {{ $value := index $s 0 }} 107 + {{ $label := index $s 1 }} 108 + <option value="{{ $value }}"{{ if eq $stat $value }} selected{{ end }}>{{ $label }}</option> 109 + {{ end }} 110 + </select> 111 + {{ end }}
+42
appview/pages/templates/user/fragments/editPins.html
··· 1 + {{ define "user/fragments/editPins" }} 2 + {{ $profile := .Profile }} 3 + <form 4 + hx-post="/profile/pins" 5 + hx-disabled-elt="#save-btn,#cancel-btn" 6 + hx-swap="none" 7 + hx-indicator="#spinner"> 8 + <div class="flex items-center justify-between mb-2"> 9 + <p class="text-sm font-bold p-2 dark:text-white">SELECT PINNED REPOS</p> 10 + <div class="flex items-center gap-2"> 11 + <button id="save-btn" type="submit" class="btn px-2 flex items-center gap-2 no-underline text-sm"> 12 + {{ i "check" "w-3 h-3" }} save 13 + <span id="spinner" class="group"> 14 + {{ i "loader-circle" "w-3 h-3 animate-spin hidden group-[.htmx-request]:inline" }} 15 + </span> 16 + </button> 17 + <a href="/{{.LoggedInUser.Did}}" class="w-full no-underline hover:no-underline"> 18 + <button id="cancel-btn" type="button" class="btn px-2 w-full flex items-center gap-2 no-underline text-sm"> 19 + {{ i "x" "w-3 h-3" }} cancel 20 + </button> 21 + </a> 22 + </div> 23 + </div> 24 + <div id="repos" class="grid grid-cols-1 gap-1 mb-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700"> 25 + {{ range $idx, $r := .AllRepos }} 26 + <div class="flex items-center gap-2 text-base p-2 border-b border-gray-200 dark:border-gray-700"> 27 + <input type="checkbox" id="repo-{{$idx}}" name="pinnedRepo{{$idx}}" value="{{.RepoAt}}" {{if .IsPinned}}checked{{end}}> 28 + <label for="repo-{{$idx}}" class="my-0 py-0 normal-case font-normal w-full"> 29 + <div class="flex justify-between items-center w-full"> 30 + <span class="flex-shrink-0 overflow-hidden text-ellipsis ">{{ index $.DidHandleMap .Did }}/{{.Name}}</span> 31 + <div class="flex gap-1 items-center"> 32 + {{ i "star" "size-4 fill-current" }} 33 + <span>{{ .RepoStats.StarCount }}</span> 34 + </div> 35 + </div> 36 + </label> 37 + </div> 38 + {{ end }} 39 + </div> 40 + 41 + </form> 42 + {{ end }}
+97
appview/pages/templates/user/fragments/profileCard.html
··· 1 + {{ define "user/fragments/profileCard" }} 2 + <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit"> 3 + <div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center"> 4 + <div id="avatar" class="col-span-1 flex justify-center items-center"> 5 + {{ if .AvatarUri }} 6 + <img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" /> 7 + {{ end }} 8 + </div> 9 + <div class="col-span-2"> 10 + <p title="{{ didOrHandle .UserDid .UserHandle }}" 11 + class="text-lg font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 12 + {{ didOrHandle .UserDid .UserHandle }} 13 + </p> 14 + 15 + <div class="md:hidden"> 16 + {{ block "followerFollowing" (list .Followers .Following) }} {{ end }} 17 + </div> 18 + </div> 19 + <div class="col-span-3 md:col-span-full"> 20 + <div id="profile-bio" class="text-sm"> 21 + {{ $profile := .Profile }} 22 + {{ with .Profile }} 23 + 24 + {{ if .Description }} 25 + <p class="text-base pb-4 md:pb-2">{{ .Description }}</p> 26 + {{ end }} 27 + 28 + <div class="hidden md:block"> 29 + {{ block "followerFollowing" (list $.Followers $.Following) }} {{ end }} 30 + </div> 31 + 32 + <div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full"> 33 + {{ if .Location }} 34 + <div class="flex items-center gap-2"> 35 + <span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span> 36 + <span>{{ .Location }}</span> 37 + </div> 38 + {{ end }} 39 + {{ if .IncludeBluesky }} 40 + <div class="flex items-center gap-2"> 41 + <span class="flex-shrink-0">{{ template "user/fragments/bluesky" "w-4 h-4 text-black dark:text-white" }}</span> 42 + <a id="bluesky-link" href="https://bsky.app/profile/{{ $.UserDid }}">{{ didOrHandle $.UserDid $.UserHandle }}</a> 43 + </div> 44 + {{ end }} 45 + {{ range $link := .Links }} 46 + {{ if $link }} 47 + <div class="flex items-center gap-2"> 48 + <span class="flex-shrink-0">{{ i "link" "size-4" }}</span> 49 + <a href="{{ $link }}">{{ $link }}</a> 50 + </div> 51 + {{ end }} 52 + {{ end }} 53 + {{ if not $profile.IsStatsEmpty }} 54 + <div class="flex items-center justify-evenly gap-2 py-2"> 55 + {{ range $stat := .Stats }} 56 + {{ if $stat.Kind }} 57 + <div class="flex flex-col items-center gap-2"> 58 + <span class="text-xl font-bold">{{ $stat.Value }}</span> 59 + <span>{{ $stat.Kind.String }}</span> 60 + </div> 61 + {{ end }} 62 + {{ end }} 63 + </div> 64 + {{ end }} 65 + </div> 66 + {{ end }} 67 + {{ if ne .FollowStatus.String "IsSelf" }} 68 + {{ template "user/fragments/follow" . }} 69 + {{ else }} 70 + <button id="editBtn" 71 + class="btn mt-2 w-full flex items-center gap-2 group" 72 + hx-target="#profile-bio" 73 + hx-get="/profile/edit-bio" 74 + hx-swap="innerHTML"> 75 + {{ i "pencil" "w-4 h-4" }} 76 + edit 77 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 78 + </button> 79 + {{ end }} 80 + </div> 81 + <div id="update-profile" class="text-red-400 dark:text-red-500"></div> 82 + </div> 83 + </div> 84 + </div> 85 + {{ end }} 86 + 87 + {{ define "followerFollowing" }} 88 + {{ $followers := index . 0 }} 89 + {{ $following := index . 1 }} 90 + <div class="flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full text-sm"> 91 + <span class="flex-shrink-0">{{ i "users" "size-4" }}</span> 92 + <span id="followers">{{ $followers }} followers</span> 93 + <span class="select-none after:content-['ยท']"></span> 94 + <span id="following">{{ $following }} following</span> 95 + </div> 96 + {{ end }} 97 +
+22 -33
appview/pages/templates/user/login.html
··· 8 8 content="width=device-width, initial-scale=1.0" 9 9 /> 10 10 <script src="/static/htmx.min.js"></script> 11 - <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 11 + <link 12 + rel="stylesheet" 13 + href="/static/tw.css?{{ cssContentHash }}" 14 + type="text/css" 15 + /> 12 16 <title>login</title> 13 17 </head> 14 18 <body class="flex items-center justify-center min-h-screen"> 15 - <main class="max-w-7xl px-6 -mt-4"> 16 - <h1 class="text-center text-2xl font-semibold italic dark:text-white"> 19 + <main class="max-w-md px-6 -mt-4"> 20 + <h1 21 + class="text-center text-2xl font-semibold italic dark:text-white" 22 + > 17 23 tangled 18 24 </h1> 19 25 <h2 class="text-center text-xl italic dark:text-white"> 20 26 tightly-knit social coding. 21 27 </h2> 22 28 <form 23 - class="w-full mt-4" 29 + class="mt-4 max-w-sm mx-auto" 24 30 hx-post="/login" 25 31 hx-swap="none" 26 - hx-disabled-elt="this" 32 + hx-disabled-elt="#login-button" 27 33 > 28 34 <div class="flex flex-col"> 29 35 <label for="handle">handle</label> 30 - <input 31 - type="text" 32 - id="handle" 33 - name="handle" 34 - tabindex="1" 35 - required 36 - /> 37 - <span class="text-xs text-gray-500 mt-1"> 38 - You need to use your 39 - <a href="https://bsky.app">Bluesky</a> handle to log 40 - in. 41 - </span> 42 - </div> 43 - 44 - <div class="flex flex-col mt-2"> 45 - <label for="app_password">app password</label> 46 36 <input 47 - type="password" 48 - id="app_password" 49 - name="app_password" 50 - tabindex="2" 37 + type="text" 38 + id="handle" 39 + name="handle" 40 + tabindex="1" 51 41 required 52 42 /> 53 - <span class="text-xs text-gray-500 mt-1"> 54 - Generate an app password 55 - <a 56 - href="https://bsky.app/settings/app-passwords" 57 - target="_blank" 58 - >here</a 59 - >. 43 + <span class="text-sm text-gray-500 mt-1"> 44 + Use your 45 + <a href="https://bsky.app">Bluesky</a> handle to log 46 + in. You will then be redirected to your PDS to 47 + complete authentication. 60 48 </span> 61 49 </div> 62 50 ··· 70 58 </button> 71 59 </form> 72 60 <p class="text-sm text-gray-500"> 73 - Join our <a href="https://chat.tangled.sh">Discord</a> or IRC channel: 61 + Join our <a href="https://chat.tangled.sh">Discord</a> or 62 + IRC channel: 74 63 <a href="https://web.libera.chat/#tangled" 75 64 ><code>#tangled</code> on Libera Chat</a 76 65 >.
+82 -92
appview/pages/templates/user/profile.html
··· 1 - {{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }} 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }} 2 2 3 3 {{ define "content" }} 4 - <div class="grid grid-cols-1 md:grid-cols-5 gap-6"> 5 - <div class="md:col-span-1 order-1 md:order-1"> 6 - {{ block "profileCard" . }}{{ end }} 4 + <div class="grid grid-cols-1 md:grid-cols-8 gap-6"> 5 + <div class="md:col-span-2 order-1 md:order-1"> 6 + {{ template "user/fragments/profileCard" .Card }} 7 7 </div> 8 - <div class="md:col-span-2 order-2 md:order-2"> 8 + <div id="all-repos" class="md:col-span-3 order-2 md:order-2"> 9 9 {{ block "ownRepos" . }}{{ end }} 10 10 {{ block "collaboratingRepos" . }}{{ end }} 11 11 </div> 12 - <div class="md:col-span-2 order-3 md:order-3"> 12 + <div class="md:col-span-3 order-3 md:order-3"> 13 13 {{ block "profileTimeline" . }}{{ end }} 14 14 </div> 15 15 </div> 16 16 {{ end }} 17 17 18 18 {{ define "profileTimeline" }} 19 - <p class="text-sm font-bold py-2 dark:text-white px-6">ACTIVITY</p> 19 + <p class="text-sm font-bold p-2 dark:text-white">ACTIVITY</p> 20 20 <div class="flex flex-col gap-6 relative"> 21 21 {{ with .ProfileTimeline }} 22 22 {{ range $idx, $byMonth := .ByMonth }} ··· 179 179 180 180 181 181 {{ if gt $stats.Closed 0 }} 182 - <span class="px-2 py-1/2 text-sm rounded text-black dark:text-white bg-gray-50 dark:bg-gray-700 "> 182 + <span class="px-2 py-1/2 text-sm rounded text-white bg-gray-800 dark:bg-gray-700"> 183 183 {{$stats.Closed}} closed 184 184 </span> 185 185 {{ end }} ··· 225 225 {{ end }} 226 226 {{ end }} 227 227 228 - {{ define "profileCard" }} 229 - <div class="bg-white dark:bg-gray-800 px-6 py-4 rounded drop-shadow-sm max-h-fit"> 230 - <div class="flex justify-center items-center"> 231 - {{ if .AvatarUri }} 232 - <img class="w-3/4 rounded-full p-2" src="{{ .AvatarUri }}" /> 233 - {{ end }} 234 - </div> 235 - <p 236 - title="{{ didOrHandle .UserDid .UserHandle }}" 237 - class="text-lg font-bold text-center dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full" 238 - > 239 - {{ didOrHandle .UserDid .UserHandle }} 240 - </p> 241 - <div class="text-sm text-center dark:text-gray-300"> 242 - <span>{{ .ProfileStats.Followers }} followers</span> 243 - <div 244 - class="inline-block px-1 select-none after:content-['ยท']" 245 - ></div> 246 - <span>{{ .ProfileStats.Following }} following</span> 247 - </div> 248 - 249 - {{ if ne .FollowStatus.String "IsSelf" }} 250 - {{ template "user/fragments/follow" . }} 251 - {{ end }} 252 - </div> 253 - {{ end }} 254 - 255 228 {{ define "ownRepos" }} 256 - <p class="text-sm font-bold py-2 px-6 dark:text-white">REPOS</p> 257 - <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 258 - {{ range .Repos }} 259 - <div 260 - id="repo-card" 261 - class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800" 262 - > 263 - <div id="repo-card-name" class="font-medium dark:text-white"> 264 - <a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}" 265 - >{{ .Name }}</a 266 - > 267 - </div> 268 - {{ if .Description }} 269 - <div class="text-gray-600 dark:text-gray-300 text-sm"> 270 - {{ .Description }} 271 - </div> 272 - {{ end }} 273 - <div 274 - class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto" 275 - > 276 - 277 - {{ if .RepoStats.StarCount }} 278 - <div class="flex gap-1 items-center text-sm"> 279 - {{ i "star" "w-3 h-3 fill-current" }} 280 - <span>{{ .RepoStats.StarCount }}</span> 281 - </div> 282 - {{ end }} 229 + <div class="text-sm font-bold p-2 pr-0 dark:text-white flex items-center justify-between gap-2"> 230 + <a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos" 231 + class="flex text-black dark:text-white items-center gap-4 no-underline hover:no-underline group"> 232 + <span>PINNED REPOS</span> 233 + <span class="flex md:hidden group-hover:flex gap-2 items-center font-normal text-sm text-gray-500 dark:text-gray-400 "> 234 + view all {{ i "chevron-right" "w-4 h-4" }} 235 + </span> 236 + </a> 237 + {{ if and .LoggedInUser (eq .LoggedInUser.Did .Card.UserDid) }} 238 + <button 239 + hx-get="profile/edit-pins" 240 + hx-target="#all-repos" 241 + class="btn font-normal text-sm flex gap-2 items-center group"> 242 + {{ i "pencil" "w-3 h-3" }} 243 + edit 244 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 245 + </button> 246 + {{ end }} 247 + </div> 248 + <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 249 + {{ range .Repos }} 250 + <div 251 + id="repo-card" 252 + class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800"> 253 + <div id="repo-card-name" class="font-medium"> 254 + <a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}" 255 + >{{ .Name }}</a 256 + > 257 + </div> 258 + {{ if .Description }} 259 + <div class="text-gray-600 dark:text-gray-300 text-sm"> 260 + {{ .Description }} 261 + </div> 262 + {{ end }} 263 + <div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto"> 264 + {{ if .RepoStats.StarCount }} 265 + <div class="flex gap-1 items-center text-sm"> 266 + {{ i "star" "w-3 h-3 fill-current" }} 267 + <span>{{ .RepoStats.StarCount }}</span> 283 268 </div> 284 - </div> 285 - {{ else }} 286 - <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 287 - {{ end }} 288 - </div> 269 + {{ end }} 270 + </div> 271 + </div> 272 + {{ else }} 273 + <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 274 + {{ end }} 275 + </div> 276 + {{ end }} 289 277 290 - <p class="text-sm font-bold py-2 px-6 dark:text-white">COLLABORATING ON</p> 278 + {{ define "collaboratingRepos" }} 279 + {{ if gt (len .CollaboratingRepos) 0 }} 280 + <p class="text-sm font-bold p-2 dark:text-white">COLLABORATING ON</p> 291 281 <div id="collaborating" class="grid grid-cols-1 gap-4 mb-6"> 292 - {{ range .CollaboratingRepos }} 293 - <div 294 - id="repo-card" 295 - class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col" 296 - > 297 - <div id="repo-card-name" class="font-medium dark:text-white"> 298 - <a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}"> 299 - {{ index $.DidHandleMap .Did }}/{{ .Name }} 300 - </a> 282 + {{ range .CollaboratingRepos }} 283 + <div 284 + id="repo-card" 285 + class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800 flex flex-col"> 286 + <div id="repo-card-name" class="font-medium dark:text-white"> 287 + <a href="/{{ index $.DidHandleMap .Did }}/{{ .Name }}"> 288 + {{ index $.DidHandleMap .Did }}/{{ .Name }} 289 + </a> 290 + </div> 291 + {{ if .Description }} 292 + <div class="text-gray-600 dark:text-gray-300 text-sm"> 293 + {{ .Description }} 301 294 </div> 302 - {{ if .Description }} 303 - <div class="text-gray-600 dark:text-gray-300 text-sm"> 304 - {{ .Description }} 295 + {{ end }} 296 + <div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto"> 297 + 298 + {{ if .RepoStats.StarCount }} 299 + <div class="flex gap-1 items-center text-sm"> 300 + {{ i "star" "w-3 h-3 fill-current" }} 301 + <span>{{ .RepoStats.StarCount }}</span> 305 302 </div> 306 303 {{ end }} 307 - <div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto"> 308 - 309 - {{ if .RepoStats.StarCount }} 310 - <div class="flex gap-1 items-center text-sm"> 311 - {{ i "star" "w-3 h-3 fill-current" }} 312 - <span>{{ .RepoStats.StarCount }}</span> 313 - </div> 314 - {{ end }} 315 - </div> 316 304 </div> 317 - {{ else }} 318 - <p class="px-6 dark:text-white">This user is not collaborating.</p> 319 - {{ end }} 305 + </div> 306 + {{ else }} 307 + <p class="px-6 dark:text-white">This user is not collaborating.</p> 308 + {{ end }} 320 309 </div> 310 + {{ end }} 321 311 {{ end }}
+44
appview/pages/templates/user/repos.html
··· 1 + {{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }} 2 + 3 + {{ define "content" }} 4 + <div class="grid grid-cols-1 md:grid-cols-8 gap-6"> 5 + <div class="md:col-span-2 order-1 md:order-1"> 6 + {{ template "user/fragments/profileCard" .Card }} 7 + </div> 8 + <div id="all-repos" class="md:col-span-6 order-2 md:order-2"> 9 + {{ block "ownRepos" . }}{{ end }} 10 + </div> 11 + </div> 12 + {{ end }} 13 + 14 + {{ define "ownRepos" }} 15 + <p class="text-sm font-bold p-2 dark:text-white">ALL REPOSITORIES</p> 16 + <div id="repos" class="grid grid-cols-1 gap-4 mb-6"> 17 + {{ range .Repos }} 18 + <div 19 + id="repo-card" 20 + class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800"> 21 + <div id="repo-card-name" class="font-medium"> 22 + <a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}/{{ .Name }}" 23 + >{{ .Name }}</a 24 + > 25 + </div> 26 + {{ if .Description }} 27 + <div class="text-gray-600 dark:text-gray-300 text-sm"> 28 + {{ .Description }} 29 + </div> 30 + {{ end }} 31 + <div class="text-gray-400 pt-1 text-sm font-mono inline-flex gap-4 mt-auto"> 32 + {{ if .RepoStats.StarCount }} 33 + <div class="flex gap-1 items-center text-sm"> 34 + {{ i "star" "w-3 h-3 fill-current" }} 35 + <span>{{ .RepoStats.StarCount }}</span> 36 + </div> 37 + {{ end }} 38 + </div> 39 + </div> 40 + {{ else }} 41 + <p class="px-6 dark:text-white">This user does not have any repos yet.</p> 42 + {{ end }} 43 + </div> 44 + {{ end }}
+31
appview/pagination/page.go
··· 1 + package pagination 2 + 3 + type Page struct { 4 + Offset int // where to start from 5 + Limit int // number of items in a page 6 + } 7 + 8 + func FirstPage() Page { 9 + return Page{ 10 + Offset: 0, 11 + Limit: 10, 12 + } 13 + } 14 + 15 + func (p Page) Previous() Page { 16 + if p.Offset-p.Limit < 0 { 17 + return FirstPage() 18 + } else { 19 + return Page{ 20 + Offset: p.Offset - p.Limit, 21 + Limit: p.Limit, 22 + } 23 + } 24 + } 25 + 26 + func (p Page) Next() Page { 27 + return Page{ 28 + Offset: p.Offset + p.Limit, 29 + Limit: p.Limit, 30 + } 31 + }
+460
appview/settings/settings.go
··· 1 + package settings 2 + 3 + import ( 4 + "database/sql" 5 + "errors" 6 + "fmt" 7 + "log" 8 + "net/http" 9 + "net/url" 10 + "strings" 11 + "time" 12 + 13 + "github.com/go-chi/chi/v5" 14 + "tangled.sh/tangled.sh/core/api/tangled" 15 + "tangled.sh/tangled.sh/core/appview" 16 + "tangled.sh/tangled.sh/core/appview/db" 17 + "tangled.sh/tangled.sh/core/appview/email" 18 + "tangled.sh/tangled.sh/core/appview/middleware" 19 + "tangled.sh/tangled.sh/core/appview/oauth" 20 + "tangled.sh/tangled.sh/core/appview/pages" 21 + 22 + comatproto "github.com/bluesky-social/indigo/api/atproto" 23 + lexutil "github.com/bluesky-social/indigo/lex/util" 24 + "github.com/gliderlabs/ssh" 25 + "github.com/google/uuid" 26 + ) 27 + 28 + type Settings struct { 29 + Db *db.DB 30 + OAuth *oauth.OAuth 31 + Pages *pages.Pages 32 + Config *appview.Config 33 + } 34 + 35 + func (s *Settings) Router() http.Handler { 36 + r := chi.NewRouter() 37 + 38 + r.Use(middleware.AuthMiddleware(s.OAuth)) 39 + 40 + r.Get("/", s.settings) 41 + 42 + r.Route("/keys", func(r chi.Router) { 43 + r.Put("/", s.keys) 44 + r.Delete("/", s.keys) 45 + }) 46 + 47 + r.Route("/emails", func(r chi.Router) { 48 + r.Put("/", s.emails) 49 + r.Delete("/", s.emails) 50 + r.Get("/verify", s.emailsVerify) 51 + r.Post("/verify/resend", s.emailsVerifyResend) 52 + r.Post("/primary", s.emailsPrimary) 53 + }) 54 + 55 + return r 56 + } 57 + 58 + func (s *Settings) settings(w http.ResponseWriter, r *http.Request) { 59 + user := s.OAuth.GetUser(r) 60 + pubKeys, err := db.GetPublicKeys(s.Db, user.Did) 61 + if err != nil { 62 + log.Println(err) 63 + } 64 + 65 + emails, err := db.GetAllEmails(s.Db, user.Did) 66 + if err != nil { 67 + log.Println(err) 68 + } 69 + 70 + s.Pages.Settings(w, pages.SettingsParams{ 71 + LoggedInUser: user, 72 + PubKeys: pubKeys, 73 + Emails: emails, 74 + }) 75 + } 76 + 77 + // buildVerificationEmail creates an email.Email struct for verification emails 78 + func (s *Settings) buildVerificationEmail(emailAddr, did, code string) email.Email { 79 + verifyURL := s.verifyUrl(did, emailAddr, code) 80 + 81 + return email.Email{ 82 + APIKey: s.Config.Resend.ApiKey, 83 + From: "noreply@notifs.tangled.sh", 84 + To: emailAddr, 85 + Subject: "Verify your Tangled email", 86 + Text: `Click the link below (or copy and paste it into your browser) to verify your email address. 87 + ` + verifyURL, 88 + Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p> 89 + <p><a href="` + verifyURL + `">` + verifyURL + `</a></p>`, 90 + } 91 + } 92 + 93 + // sendVerificationEmail handles the common logic for sending verification emails 94 + func (s *Settings) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error { 95 + emailToSend := s.buildVerificationEmail(emailAddr, did, code) 96 + 97 + err := email.SendEmail(emailToSend) 98 + if err != nil { 99 + log.Printf("sending email: %s", err) 100 + s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext)) 101 + return err 102 + } 103 + 104 + return nil 105 + } 106 + 107 + func (s *Settings) emails(w http.ResponseWriter, r *http.Request) { 108 + switch r.Method { 109 + case http.MethodGet: 110 + s.Pages.Notice(w, "settings-emails", "Unimplemented.") 111 + log.Println("unimplemented") 112 + return 113 + case http.MethodPut: 114 + did := s.OAuth.GetDid(r) 115 + emAddr := r.FormValue("email") 116 + emAddr = strings.TrimSpace(emAddr) 117 + 118 + if !email.IsValidEmail(emAddr) { 119 + s.Pages.Notice(w, "settings-emails-error", "Invalid email address.") 120 + return 121 + } 122 + 123 + // check if email already exists in database 124 + existingEmail, err := db.GetEmail(s.Db, did, emAddr) 125 + if err != nil && !errors.Is(err, sql.ErrNoRows) { 126 + log.Printf("checking for existing email: %s", err) 127 + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 128 + return 129 + } 130 + 131 + if err == nil { 132 + if existingEmail.Verified { 133 + s.Pages.Notice(w, "settings-emails-error", "This email is already verified.") 134 + return 135 + } 136 + 137 + s.Pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.") 138 + return 139 + } 140 + 141 + code := uuid.New().String() 142 + 143 + // Begin transaction 144 + tx, err := s.Db.Begin() 145 + if err != nil { 146 + log.Printf("failed to start transaction: %s", err) 147 + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 148 + return 149 + } 150 + defer tx.Rollback() 151 + 152 + if err := db.AddEmail(tx, db.Email{ 153 + Did: did, 154 + Address: emAddr, 155 + Verified: false, 156 + VerificationCode: code, 157 + }); err != nil { 158 + log.Printf("adding email: %s", err) 159 + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 160 + return 161 + } 162 + 163 + if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 164 + return 165 + } 166 + 167 + // Commit transaction 168 + if err := tx.Commit(); err != nil { 169 + log.Printf("failed to commit transaction: %s", err) 170 + s.Pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 171 + return 172 + } 173 + 174 + s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") 175 + return 176 + case http.MethodDelete: 177 + did := s.OAuth.GetDid(r) 178 + emailAddr := r.FormValue("email") 179 + emailAddr = strings.TrimSpace(emailAddr) 180 + 181 + // Begin transaction 182 + tx, err := s.Db.Begin() 183 + if err != nil { 184 + log.Printf("failed to start transaction: %s", err) 185 + s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 186 + return 187 + } 188 + defer tx.Rollback() 189 + 190 + if err := db.DeleteEmail(tx, did, emailAddr); err != nil { 191 + log.Printf("deleting email: %s", err) 192 + s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 193 + return 194 + } 195 + 196 + // Commit transaction 197 + if err := tx.Commit(); err != nil { 198 + log.Printf("failed to commit transaction: %s", err) 199 + s.Pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 200 + return 201 + } 202 + 203 + s.Pages.HxLocation(w, "/settings") 204 + return 205 + } 206 + } 207 + 208 + func (s *Settings) verifyUrl(did string, email string, code string) string { 209 + var appUrl string 210 + if s.Config.Core.Dev { 211 + appUrl = "http://" + s.Config.Core.ListenAddr 212 + } else { 213 + appUrl = "https://tangled.sh" 214 + } 215 + 216 + return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code)) 217 + } 218 + 219 + func (s *Settings) emailsVerify(w http.ResponseWriter, r *http.Request) { 220 + q := r.URL.Query() 221 + 222 + // Get the parameters directly from the query 223 + emailAddr := q.Get("email") 224 + did := q.Get("did") 225 + code := q.Get("code") 226 + 227 + valid, err := db.CheckValidVerificationCode(s.Db, did, emailAddr, code) 228 + if err != nil { 229 + log.Printf("checking email verification: %s", err) 230 + s.Pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.") 231 + return 232 + } 233 + 234 + if !valid { 235 + s.Pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.") 236 + return 237 + } 238 + 239 + // Mark email as verified in the database 240 + if err := db.MarkEmailVerified(s.Db, did, emailAddr); err != nil { 241 + log.Printf("marking email as verified: %s", err) 242 + s.Pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.") 243 + return 244 + } 245 + 246 + http.Redirect(w, r, "/settings", http.StatusSeeOther) 247 + } 248 + 249 + func (s *Settings) emailsVerifyResend(w http.ResponseWriter, r *http.Request) { 250 + if r.Method != http.MethodPost { 251 + s.Pages.Notice(w, "settings-emails-error", "Invalid request method.") 252 + return 253 + } 254 + 255 + did := s.OAuth.GetDid(r) 256 + emAddr := r.FormValue("email") 257 + emAddr = strings.TrimSpace(emAddr) 258 + 259 + if !email.IsValidEmail(emAddr) { 260 + s.Pages.Notice(w, "settings-emails-error", "Invalid email address.") 261 + return 262 + } 263 + 264 + // Check if email exists and is unverified 265 + existingEmail, err := db.GetEmail(s.Db, did, emAddr) 266 + if err != nil { 267 + if errors.Is(err, sql.ErrNoRows) { 268 + s.Pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.") 269 + } else { 270 + log.Printf("checking for existing email: %s", err) 271 + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 272 + } 273 + return 274 + } 275 + 276 + if existingEmail.Verified { 277 + s.Pages.Notice(w, "settings-emails-error", "This email is already verified.") 278 + return 279 + } 280 + 281 + // Check if last verification email was sent less than 10 minutes ago 282 + if existingEmail.LastSent != nil { 283 + timeSinceLastSent := time.Since(*existingEmail.LastSent) 284 + if timeSinceLastSent < 10*time.Minute { 285 + waitTime := 10*time.Minute - timeSinceLastSent 286 + s.Pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1))) 287 + return 288 + } 289 + } 290 + 291 + // Generate new verification code 292 + code := uuid.New().String() 293 + 294 + // Begin transaction 295 + tx, err := s.Db.Begin() 296 + if err != nil { 297 + log.Printf("failed to start transaction: %s", err) 298 + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 299 + return 300 + } 301 + defer tx.Rollback() 302 + 303 + // Update the verification code and last sent time 304 + if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil { 305 + log.Printf("updating email verification: %s", err) 306 + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 307 + return 308 + } 309 + 310 + // Send verification email 311 + if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 312 + return 313 + } 314 + 315 + // Commit transaction 316 + if err := tx.Commit(); err != nil { 317 + log.Printf("failed to commit transaction: %s", err) 318 + s.Pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 319 + return 320 + } 321 + 322 + s.Pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.") 323 + } 324 + 325 + func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) { 326 + did := s.OAuth.GetDid(r) 327 + emailAddr := r.FormValue("email") 328 + emailAddr = strings.TrimSpace(emailAddr) 329 + 330 + if emailAddr == "" { 331 + s.Pages.Notice(w, "settings-emails-error", "Email address cannot be empty.") 332 + return 333 + } 334 + 335 + if err := db.MakeEmailPrimary(s.Db, did, emailAddr); err != nil { 336 + log.Printf("setting primary email: %s", err) 337 + s.Pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.") 338 + return 339 + } 340 + 341 + s.Pages.HxLocation(w, "/settings") 342 + } 343 + 344 + func (s *Settings) keys(w http.ResponseWriter, r *http.Request) { 345 + switch r.Method { 346 + case http.MethodGet: 347 + s.Pages.Notice(w, "settings-keys", "Unimplemented.") 348 + log.Println("unimplemented") 349 + return 350 + case http.MethodPut: 351 + did := s.OAuth.GetDid(r) 352 + key := r.FormValue("key") 353 + key = strings.TrimSpace(key) 354 + name := r.FormValue("name") 355 + client, err := s.OAuth.AuthorizedClient(r) 356 + if err != nil { 357 + s.Pages.Notice(w, "settings-keys", "Failed to authorize. Try again later.") 358 + return 359 + } 360 + 361 + _, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key)) 362 + if err != nil { 363 + log.Printf("parsing public key: %s", err) 364 + s.Pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.") 365 + return 366 + } 367 + 368 + rkey := appview.TID() 369 + 370 + tx, err := s.Db.Begin() 371 + if err != nil { 372 + log.Printf("failed to start tx; adding public key: %s", err) 373 + s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") 374 + return 375 + } 376 + defer tx.Rollback() 377 + 378 + if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil { 379 + log.Printf("adding public key: %s", err) 380 + s.Pages.Notice(w, "settings-keys", "Failed to add public key.") 381 + return 382 + } 383 + 384 + // store in pds too 385 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 386 + Collection: tangled.PublicKeyNSID, 387 + Repo: did, 388 + Rkey: rkey, 389 + Record: &lexutil.LexiconTypeDecoder{ 390 + Val: &tangled.PublicKey{ 391 + CreatedAt: time.Now().Format(time.RFC3339), 392 + Key: key, 393 + Name: name, 394 + }}, 395 + }) 396 + // invalid record 397 + if err != nil { 398 + log.Printf("failed to create record: %s", err) 399 + s.Pages.Notice(w, "settings-keys", "Failed to create record.") 400 + return 401 + } 402 + 403 + log.Println("created atproto record: ", resp.Uri) 404 + 405 + err = tx.Commit() 406 + if err != nil { 407 + log.Printf("failed to commit tx; adding public key: %s", err) 408 + s.Pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") 409 + return 410 + } 411 + 412 + s.Pages.HxLocation(w, "/settings") 413 + return 414 + 415 + case http.MethodDelete: 416 + did := s.OAuth.GetDid(r) 417 + q := r.URL.Query() 418 + 419 + name := q.Get("name") 420 + rkey := q.Get("rkey") 421 + key := q.Get("key") 422 + 423 + log.Println(name) 424 + log.Println(rkey) 425 + log.Println(key) 426 + 427 + client, err := s.OAuth.AuthorizedClient(r) 428 + if err != nil { 429 + log.Printf("failed to authorize client: %s", err) 430 + s.Pages.Notice(w, "settings-keys", "Failed to authorize client.") 431 + return 432 + } 433 + 434 + if err := db.DeletePublicKey(s.Db, did, name, key); err != nil { 435 + log.Printf("removing public key: %s", err) 436 + s.Pages.Notice(w, "settings-keys", "Failed to remove public key.") 437 + return 438 + } 439 + 440 + if rkey != "" { 441 + // remove from pds too 442 + _, err := client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 443 + Collection: tangled.PublicKeyNSID, 444 + Repo: did, 445 + Rkey: rkey, 446 + }) 447 + 448 + // invalid record 449 + if err != nil { 450 + log.Printf("failed to delete record from PDS: %s", err) 451 + s.Pages.Notice(w, "settings-keys", "Failed to remove key from PDS.") 452 + return 453 + } 454 + } 455 + log.Println("deleted successfully") 456 + 457 + s.Pages.HxLocation(w, "/settings") 458 + return 459 + } 460 + }
+296
appview/state/artifact.go
··· 1 + package state 2 + 3 + import ( 4 + "fmt" 5 + "log" 6 + "net/http" 7 + "net/url" 8 + "time" 9 + 10 + comatproto "github.com/bluesky-social/indigo/api/atproto" 11 + lexutil "github.com/bluesky-social/indigo/lex/util" 12 + "github.com/dustin/go-humanize" 13 + "github.com/go-chi/chi/v5" 14 + "github.com/go-git/go-git/v5/plumbing" 15 + "github.com/ipfs/go-cid" 16 + "tangled.sh/tangled.sh/core/api/tangled" 17 + "tangled.sh/tangled.sh/core/appview" 18 + "tangled.sh/tangled.sh/core/appview/db" 19 + "tangled.sh/tangled.sh/core/appview/knotclient" 20 + "tangled.sh/tangled.sh/core/appview/pages" 21 + "tangled.sh/tangled.sh/core/types" 22 + ) 23 + 24 + // TODO: proper statuses here on early exit 25 + func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { 26 + user := s.oauth.GetUser(r) 27 + tagParam := chi.URLParam(r, "tag") 28 + f, err := s.fullyResolvedRepo(r) 29 + if err != nil { 30 + log.Println("failed to get repo and knot", err) 31 + s.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution") 32 + return 33 + } 34 + 35 + tag, err := s.resolveTag(f, tagParam) 36 + if err != nil { 37 + log.Println("failed to resolve tag", err) 38 + s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 39 + return 40 + } 41 + 42 + file, handler, err := r.FormFile("artifact") 43 + if err != nil { 44 + log.Println("failed to upload artifact", err) 45 + s.pages.Notice(w, "upload", "failed to upload artifact") 46 + return 47 + } 48 + defer file.Close() 49 + 50 + client, err := s.oauth.AuthorizedClient(r) 51 + if err != nil { 52 + log.Println("failed to get authorized client", err) 53 + s.pages.Notice(w, "upload", "failed to get authorized client") 54 + return 55 + } 56 + 57 + uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file) 58 + if err != nil { 59 + log.Println("failed to upload blob", err) 60 + s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") 61 + return 62 + } 63 + 64 + log.Println("uploaded blob", humanize.Bytes(uint64(uploadBlobResp.Blob.Size)), uploadBlobResp.Blob.Ref.String()) 65 + 66 + rkey := appview.TID() 67 + createdAt := time.Now() 68 + 69 + putRecordResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 70 + Collection: tangled.RepoArtifactNSID, 71 + Repo: user.Did, 72 + Rkey: rkey, 73 + Record: &lexutil.LexiconTypeDecoder{ 74 + Val: &tangled.RepoArtifact{ 75 + Artifact: uploadBlobResp.Blob, 76 + CreatedAt: createdAt.Format(time.RFC3339), 77 + Name: handler.Filename, 78 + Repo: f.RepoAt.String(), 79 + Tag: tag.Tag.Hash[:], 80 + }, 81 + }, 82 + }) 83 + if err != nil { 84 + log.Println("failed to create record", err) 85 + s.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.") 86 + return 87 + } 88 + 89 + log.Println(putRecordResp.Uri) 90 + 91 + tx, err := s.db.BeginTx(r.Context(), nil) 92 + if err != nil { 93 + log.Println("failed to start tx") 94 + s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 95 + return 96 + } 97 + defer tx.Rollback() 98 + 99 + artifact := db.Artifact{ 100 + Did: user.Did, 101 + Rkey: rkey, 102 + RepoAt: f.RepoAt, 103 + Tag: tag.Tag.Hash, 104 + CreatedAt: createdAt, 105 + BlobCid: cid.Cid(uploadBlobResp.Blob.Ref), 106 + Name: handler.Filename, 107 + Size: uint64(uploadBlobResp.Blob.Size), 108 + MimeType: uploadBlobResp.Blob.MimeType, 109 + } 110 + 111 + err = db.AddArtifact(tx, artifact) 112 + if err != nil { 113 + log.Println("failed to add artifact record to db", err) 114 + s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 115 + return 116 + } 117 + 118 + err = tx.Commit() 119 + if err != nil { 120 + log.Println("failed to add artifact record to db") 121 + s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 122 + return 123 + } 124 + 125 + s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{ 126 + LoggedInUser: user, 127 + RepoInfo: f.RepoInfo(s, user), 128 + Artifact: artifact, 129 + }) 130 + } 131 + 132 + // TODO: proper statuses here on early exit 133 + func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) { 134 + tagParam := chi.URLParam(r, "tag") 135 + filename := chi.URLParam(r, "file") 136 + f, err := s.fullyResolvedRepo(r) 137 + if err != nil { 138 + log.Println("failed to get repo and knot", err) 139 + return 140 + } 141 + 142 + tag, err := s.resolveTag(f, tagParam) 143 + if err != nil { 144 + log.Println("failed to resolve tag", err) 145 + s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 146 + return 147 + } 148 + 149 + client, err := s.oauth.AuthorizedClient(r) 150 + if err != nil { 151 + log.Println("failed to get authorized client", err) 152 + return 153 + } 154 + 155 + artifacts, err := db.GetArtifact( 156 + s.db, 157 + db.Filter("repo_at", f.RepoAt), 158 + db.Filter("tag", tag.Tag.Hash[:]), 159 + db.Filter("name", filename), 160 + ) 161 + if err != nil { 162 + log.Println("failed to get artifacts", err) 163 + return 164 + } 165 + if len(artifacts) != 1 { 166 + log.Printf("too many or too little artifacts found") 167 + return 168 + } 169 + 170 + artifact := artifacts[0] 171 + 172 + getBlobResp, err := client.SyncGetBlob(r.Context(), artifact.BlobCid.String(), artifact.Did) 173 + if err != nil { 174 + log.Println("failed to get blob from pds", err) 175 + return 176 + } 177 + 178 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) 179 + w.Write(getBlobResp) 180 + } 181 + 182 + // TODO: proper statuses here on early exit 183 + func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 184 + user := s.oauth.GetUser(r) 185 + tagParam := chi.URLParam(r, "tag") 186 + filename := chi.URLParam(r, "file") 187 + f, err := s.fullyResolvedRepo(r) 188 + if err != nil { 189 + log.Println("failed to get repo and knot", err) 190 + return 191 + } 192 + 193 + client, _ := s.oauth.AuthorizedClient(r) 194 + 195 + tag := plumbing.NewHash(tagParam) 196 + 197 + artifacts, err := db.GetArtifact( 198 + s.db, 199 + db.Filter("repo_at", f.RepoAt), 200 + db.Filter("tag", tag[:]), 201 + db.Filter("name", filename), 202 + ) 203 + if err != nil { 204 + log.Println("failed to get artifacts", err) 205 + s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 206 + return 207 + } 208 + if len(artifacts) != 1 { 209 + s.pages.Notice(w, "remove", "Unable to find artifact.") 210 + return 211 + } 212 + 213 + artifact := artifacts[0] 214 + 215 + if user.Did != artifact.Did { 216 + log.Println("user not authorized to delete artifact", err) 217 + s.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 218 + return 219 + } 220 + 221 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 222 + Collection: tangled.RepoArtifactNSID, 223 + Repo: user.Did, 224 + Rkey: artifact.Rkey, 225 + }) 226 + if err != nil { 227 + log.Println("failed to get blob from pds", err) 228 + s.pages.Notice(w, "remove", "Failed to remove blob from PDS.") 229 + return 230 + } 231 + 232 + tx, err := s.db.BeginTx(r.Context(), nil) 233 + if err != nil { 234 + log.Println("failed to start tx") 235 + s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 236 + return 237 + } 238 + defer tx.Rollback() 239 + 240 + err = db.DeleteArtifact(tx, 241 + db.Filter("repo_at", f.RepoAt), 242 + db.Filter("tag", artifact.Tag[:]), 243 + db.Filter("name", filename), 244 + ) 245 + if err != nil { 246 + log.Println("failed to remove artifact record from db", err) 247 + s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 248 + return 249 + } 250 + 251 + err = tx.Commit() 252 + if err != nil { 253 + log.Println("failed to remove artifact record from db") 254 + s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 255 + return 256 + } 257 + 258 + w.Write([]byte{}) 259 + } 260 + 261 + func (s *State) resolveTag(f *FullyResolvedRepo, tagParam string) (*types.TagReference, error) { 262 + tagParam, err := url.QueryUnescape(tagParam) 263 + if err != nil { 264 + return nil, err 265 + } 266 + 267 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 268 + if err != nil { 269 + return nil, err 270 + } 271 + 272 + result, err := us.Tags(f.OwnerDid(), f.RepoName) 273 + if err != nil { 274 + log.Println("failed to reach knotserver", err) 275 + return nil, err 276 + } 277 + 278 + var tag *types.TagReference 279 + for _, t := range result.Tags { 280 + if t.Tag != nil { 281 + if t.Reference.Name == tagParam || t.Reference.Hash == tagParam { 282 + tag = t 283 + } 284 + } 285 + } 286 + 287 + if tag == nil { 288 + return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts") 289 + } 290 + 291 + if tag.Tag.Target.IsZero() { 292 + return nil, fmt.Errorf("invalid tag, only annotated tags are supported for artifacts") 293 + } 294 + 295 + return tag, nil 296 + }
+12 -7
appview/state/follow.go
··· 7 7 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 lexutil "github.com/bluesky-social/indigo/lex/util" 10 - tangled "tangled.sh/tangled.sh/core/api/tangled" 10 + "tangled.sh/tangled.sh/core/api/tangled" 11 + "tangled.sh/tangled.sh/core/appview" 11 12 "tangled.sh/tangled.sh/core/appview/db" 12 13 "tangled.sh/tangled.sh/core/appview/pages" 13 14 ) 14 15 15 16 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 16 - currentUser := s.auth.GetUser(r) 17 + currentUser := s.oauth.GetUser(r) 17 18 18 19 subject := r.URL.Query().Get("subject") 19 20 if subject == "" { ··· 31 32 return 32 33 } 33 34 34 - client, _ := s.auth.AuthorizedClient(r) 35 + client, err := s.oauth.AuthorizedClient(r) 36 + if err != nil { 37 + log.Println("failed to authorize client") 38 + return 39 + } 35 40 36 41 switch r.Method { 37 42 case http.MethodPost: 38 43 createdAt := time.Now().Format(time.RFC3339) 39 - rkey := s.TID() 40 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 44 + rkey := appview.TID() 45 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 41 46 Collection: tangled.GraphFollowNSID, 42 47 Repo: currentUser.Did, 43 48 Rkey: rkey, ··· 74 79 return 75 80 } 76 81 77 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 82 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 78 83 Collection: tangled.GraphFollowNSID, 79 84 Repo: currentUser.Did, 80 85 Rkey: follow.Rkey, ··· 85 90 return 86 91 } 87 92 88 - err = db.DeleteFollow(s.db, currentUser.Did, subjectIdent.DID.String()) 93 + err = db.DeleteFollowByRkey(s.db, currentUser.Did, follow.Rkey) 89 94 if err != nil { 90 95 log.Println("failed to delete follow from DB") 91 96 // this is not an issue, the firehose event might have already done this
+2 -2
appview/state/git_http.go
··· 15 15 repo := chi.URLParam(r, "repo") 16 16 17 17 scheme := "https" 18 - if s.config.Dev { 18 + if s.config.Core.Dev { 19 19 scheme = "http" 20 20 } 21 21 targetURL := fmt.Sprintf("%s://%s/%s/%s/info/refs?%s", scheme, knot, user.DID, repo, r.URL.RawQuery) ··· 52 52 repo := chi.URLParam(r, "repo") 53 53 54 54 scheme := "https" 55 - if s.config.Dev { 55 + if s.config.Core.Dev { 56 56 scheme = "http" 57 57 } 58 58 targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-pack?%s", scheme, knot, user.DID, repo, r.URL.RawQuery)
-70
appview/state/jetstream.go
··· 1 - package state 2 - 3 - import ( 4 - "context" 5 - "encoding/json" 6 - "fmt" 7 - "log" 8 - 9 - "github.com/bluesky-social/indigo/atproto/syntax" 10 - "github.com/bluesky-social/jetstream/pkg/models" 11 - tangled "tangled.sh/tangled.sh/core/api/tangled" 12 - "tangled.sh/tangled.sh/core/appview/db" 13 - ) 14 - 15 - type Ingester func(ctx context.Context, e *models.Event) error 16 - 17 - func jetstreamIngester(d db.DbWrapper) Ingester { 18 - return func(ctx context.Context, e *models.Event) error { 19 - var err error 20 - defer func() { 21 - eventTime := e.TimeUS 22 - lastTimeUs := eventTime + 1 23 - if err := d.SaveLastTimeUs(lastTimeUs); err != nil { 24 - err = fmt.Errorf("(deferred) failed to save last time us: %w", err) 25 - } 26 - }() 27 - 28 - if e.Kind != models.EventKindCommit { 29 - return nil 30 - } 31 - 32 - did := e.Did 33 - raw := json.RawMessage(e.Commit.Record) 34 - 35 - switch e.Commit.Collection { 36 - case tangled.GraphFollowNSID: 37 - record := tangled.GraphFollow{} 38 - err := json.Unmarshal(raw, &record) 39 - if err != nil { 40 - log.Println("invalid record") 41 - return err 42 - } 43 - err = db.AddFollow(d, did, record.Subject, e.Commit.RKey) 44 - if err != nil { 45 - return fmt.Errorf("failed to add follow to db: %w", err) 46 - } 47 - case tangled.FeedStarNSID: 48 - record := tangled.FeedStar{} 49 - err := json.Unmarshal(raw, &record) 50 - if err != nil { 51 - log.Println("invalid record") 52 - return err 53 - } 54 - 55 - subjectUri, err := syntax.ParseATURI(record.Subject) 56 - 57 - if err != nil { 58 - log.Println("invalid record") 59 - return err 60 - } 61 - 62 - err = db.AddStar(d, did, subjectUri, e.Commit.RKey) 63 - if err != nil { 64 - return fmt.Errorf("failed to add follow to db: %w", err) 65 - } 66 - } 67 - 68 - return err 69 - } 70 - }
+45 -97
appview/state/middleware.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "fmt" 5 6 "log" 6 7 "net/http" 7 8 "strconv" ··· 10 11 11 12 "slices" 12 13 13 - comatproto "github.com/bluesky-social/indigo/api/atproto" 14 14 "github.com/bluesky-social/indigo/atproto/identity" 15 - "github.com/bluesky-social/indigo/xrpc" 16 15 "github.com/go-chi/chi/v5" 17 - "tangled.sh/tangled.sh/core/appview" 18 - "tangled.sh/tangled.sh/core/appview/auth" 19 16 "tangled.sh/tangled.sh/core/appview/db" 17 + "tangled.sh/tangled.sh/core/appview/middleware" 20 18 ) 21 19 22 - type Middleware func(http.Handler) http.Handler 23 - 24 - func AuthMiddleware(s *State) Middleware { 25 - return func(next http.Handler) http.Handler { 26 - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 27 - redirectFunc := func(w http.ResponseWriter, r *http.Request) { 28 - http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 29 - } 30 - if r.Header.Get("HX-Request") == "true" { 31 - redirectFunc = func(w http.ResponseWriter, _ *http.Request) { 32 - w.Header().Set("HX-Redirect", "/login") 33 - w.WriteHeader(http.StatusOK) 34 - } 35 - } 36 - 37 - session, err := s.auth.GetSession(r) 38 - if session.IsNew || err != nil { 39 - log.Printf("not logged in, redirecting") 40 - redirectFunc(w, r) 41 - return 42 - } 43 - 44 - authorized, ok := session.Values[appview.SessionAuthenticated].(bool) 45 - if !ok || !authorized { 46 - log.Printf("not logged in, redirecting") 47 - redirectFunc(w, r) 48 - return 49 - } 50 - 51 - // refresh if nearing expiry 52 - // TODO: dedup with /login 53 - expiryStr := session.Values[appview.SessionExpiry].(string) 54 - expiry, err := time.Parse(time.RFC3339, expiryStr) 55 - if err != nil { 56 - log.Println("invalid expiry time", err) 57 - redirectFunc(w, r) 58 - return 59 - } 60 - pdsUrl, ok1 := session.Values[appview.SessionPds].(string) 61 - did, ok2 := session.Values[appview.SessionDid].(string) 62 - refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string) 63 - 64 - if !ok1 || !ok2 || !ok3 { 65 - log.Println("invalid expiry time", err) 66 - redirectFunc(w, r) 67 - return 68 - } 69 - 70 - if time.Now().After(expiry) { 71 - log.Println("token expired, refreshing ...") 72 - 73 - client := xrpc.Client{ 74 - Host: pdsUrl, 75 - Auth: &xrpc.AuthInfo{ 76 - Did: did, 77 - AccessJwt: refreshJwt, 78 - RefreshJwt: refreshJwt, 79 - }, 80 - } 81 - atSession, err := comatproto.ServerRefreshSession(r.Context(), &client) 82 - if err != nil { 83 - log.Println("failed to refresh session", err) 84 - redirectFunc(w, r) 85 - return 86 - } 87 - 88 - sessionish := auth.RefreshSessionWrapper{atSession} 89 - 90 - err = s.auth.StoreSession(r, w, &sessionish, pdsUrl) 91 - if err != nil { 92 - log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err) 93 - return 94 - } 95 - 96 - log.Println("successfully refreshed token") 97 - } 98 - 99 - next.ServeHTTP(w, r) 100 - }) 101 - } 102 - } 103 - 104 - func knotRoleMiddleware(s *State, group string) Middleware { 20 + func knotRoleMiddleware(s *State, group string) middleware.Middleware { 105 21 return func(next http.Handler) http.Handler { 106 22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 107 23 // requires auth also 108 - actor := s.auth.GetUser(r) 24 + actor := s.oauth.GetUser(r) 109 25 if actor == nil { 110 26 // we need a logged in user 111 27 log.Printf("not logged in, redirecting") ··· 131 47 } 132 48 } 133 49 134 - func KnotOwner(s *State) Middleware { 50 + func KnotOwner(s *State) middleware.Middleware { 135 51 return knotRoleMiddleware(s, "server:owner") 136 52 } 137 53 138 - func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware { 54 + func RepoPermissionMiddleware(s *State, requiredPerm string) middleware.Middleware { 139 55 return func(next http.Handler) http.Handler { 140 56 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 141 57 // requires auth also 142 - actor := s.auth.GetUser(r) 58 + actor := s.oauth.GetUser(r) 143 59 if actor == nil { 144 60 // we need a logged in user 145 61 log.Printf("not logged in, redirecting") 146 62 http.Error(w, "Forbiden", http.StatusUnauthorized) 147 63 return 148 64 } 149 - f, err := fullyResolvedRepo(r) 65 + f, err := s.fullyResolvedRepo(r) 150 66 if err != nil { 151 67 http.Error(w, "malformed url", http.StatusBadRequest) 152 68 return ··· 175 91 }) 176 92 } 177 93 178 - func ResolveIdent(s *State) Middleware { 94 + func ResolveIdent(s *State) middleware.Middleware { 179 95 excluded := []string{"favicon.ico"} 180 96 181 97 return func(next http.Handler) http.Handler { ··· 201 117 } 202 118 } 203 119 204 - func ResolveRepo(s *State) Middleware { 120 + func ResolveRepo(s *State) middleware.Middleware { 205 121 return func(next http.Handler) http.Handler { 206 122 return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 207 123 repoName := chi.URLParam(req, "repo") ··· 216 132 if err != nil { 217 133 // invalid did or handle 218 134 log.Println("failed to resolve repo") 219 - w.WriteHeader(http.StatusNotFound) 135 + s.pages.Error404(w) 220 136 return 221 137 } 222 138 ··· 230 146 } 231 147 232 148 // middleware that is tacked on top of /{user}/{repo}/pulls/{pull} 233 - func ResolvePull(s *State) Middleware { 149 + func ResolvePull(s *State) middleware.Middleware { 234 150 return func(next http.Handler) http.Handler { 235 151 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 236 - f, err := fullyResolvedRepo(r) 152 + f, err := s.fullyResolvedRepo(r) 237 153 if err != nil { 238 154 log.Println("failed to fully resolve repo", err) 239 155 http.Error(w, "invalid repo url", http.StatusNotFound) ··· 260 176 }) 261 177 } 262 178 } 179 + 180 + // this should serve the go-import meta tag even if the path is technically 181 + // a 404 like tangled.sh/oppi.li/go-git/v5 182 + func GoImport(s *State) middleware.Middleware { 183 + return func(next http.Handler) http.Handler { 184 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 185 + f, err := s.fullyResolvedRepo(r) 186 + if err != nil { 187 + log.Println("failed to fully resolve repo", err) 188 + http.Error(w, "invalid repo url", http.StatusNotFound) 189 + return 190 + } 191 + 192 + fullName := f.OwnerHandle() + "/" + f.RepoName 193 + 194 + if r.Header.Get("User-Agent") == "Go-http-client/1.1" { 195 + if r.URL.Query().Get("go-get") == "1" { 196 + html := fmt.Sprintf( 197 + `<meta name="go-import" content="tangled.sh/%s git https://tangled.sh/%s"/>`, 198 + fullName, 199 + fullName, 200 + ) 201 + w.Header().Set("Content-Type", "text/html") 202 + w.Write([]byte(html)) 203 + return 204 + } 205 + } 206 + 207 + next.ServeHTTP(w, r) 208 + }) 209 + } 210 + }
+338 -17
appview/state/profile.go
··· 1 1 package state 2 2 3 3 import ( 4 + "crypto/hmac" 5 + "crypto/sha256" 6 + "encoding/hex" 4 7 "fmt" 5 8 "log" 6 9 "net/http" 10 + "slices" 11 + "strings" 7 12 13 + comatproto "github.com/bluesky-social/indigo/api/atproto" 8 14 "github.com/bluesky-social/indigo/atproto/identity" 15 + "github.com/bluesky-social/indigo/atproto/syntax" 16 + lexutil "github.com/bluesky-social/indigo/lex/util" 9 17 "github.com/go-chi/chi/v5" 18 + "tangled.sh/tangled.sh/core/api/tangled" 10 19 "tangled.sh/tangled.sh/core/appview/db" 11 20 "tangled.sh/tangled.sh/core/appview/pages" 12 21 ) 13 22 14 - func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { 23 + func (s *State) Profile(w http.ResponseWriter, r *http.Request) { 24 + tabVal := r.URL.Query().Get("tab") 25 + switch tabVal { 26 + case "": 27 + s.profilePage(w, r) 28 + case "repos": 29 + s.reposPage(w, r) 30 + } 31 + } 32 + 33 + func (s *State) profilePage(w http.ResponseWriter, r *http.Request) { 15 34 didOrHandle := chi.URLParam(r, "user") 16 35 if didOrHandle == "" { 17 36 http.Error(w, "Bad request", http.StatusBadRequest) ··· 24 43 return 25 44 } 26 45 46 + profile, err := db.GetProfile(s.db, ident.DID.String()) 47 + if err != nil { 48 + log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 49 + } 50 + 27 51 repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 28 52 if err != nil { 29 53 log.Printf("getting repos for %s: %s", ident.DID.String(), err) 30 54 } 31 55 56 + // filter out ones that are pinned 57 + pinnedRepos := []db.Repo{} 58 + for i, r := range repos { 59 + // if this is a pinned repo, add it 60 + if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 61 + pinnedRepos = append(pinnedRepos, r) 62 + } 63 + 64 + // if there are no saved pins, add the first 4 repos 65 + if profile.IsPinnedReposEmpty() && i < 4 { 66 + pinnedRepos = append(pinnedRepos, r) 67 + } 68 + } 69 + 32 70 collaboratingRepos, err := db.CollaboratingIn(s.db, ident.DID.String()) 33 71 if err != nil { 34 72 log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err) 35 73 } 36 74 75 + pinnedCollaboratingRepos := []db.Repo{} 76 + for _, r := range collaboratingRepos { 77 + // if this is a pinned repo, add it 78 + if slices.Contains(profile.PinnedRepos[:], r.RepoAt()) { 79 + pinnedCollaboratingRepos = append(pinnedCollaboratingRepos, r) 80 + } 81 + } 82 + 37 83 timeline, err := db.MakeProfileTimeline(s.db, ident.DID.String()) 38 84 if err != nil { 39 85 log.Printf("failed to create profile timeline for %s: %s", ident.DID.String(), err) ··· 73 119 log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 74 120 } 75 121 76 - loggedInUser := s.auth.GetUser(r) 122 + loggedInUser := s.oauth.GetUser(r) 77 123 followStatus := db.IsNotFollowing 78 124 if loggedInUser != nil { 79 125 followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 80 126 } 81 127 82 - profileAvatarUri, err := GetAvatarUri(ident.Handle.String()) 128 + profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 129 + s.pages.ProfilePage(w, pages.ProfilePageParams{ 130 + LoggedInUser: loggedInUser, 131 + Repos: pinnedRepos, 132 + CollaboratingRepos: pinnedCollaboratingRepos, 133 + DidHandleMap: didHandleMap, 134 + Card: pages.ProfileCard{ 135 + UserDid: ident.DID.String(), 136 + UserHandle: ident.Handle.String(), 137 + AvatarUri: profileAvatarUri, 138 + Profile: profile, 139 + FollowStatus: followStatus, 140 + Followers: followers, 141 + Following: following, 142 + }, 143 + ProfileTimeline: timeline, 144 + }) 145 + } 146 + 147 + func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 148 + ident, ok := r.Context().Value("resolvedId").(identity.Identity) 149 + if !ok { 150 + s.pages.Error404(w) 151 + return 152 + } 153 + 154 + profile, err := db.GetProfile(s.db, ident.DID.String()) 83 155 if err != nil { 84 - log.Println("failed to fetch bsky avatar", err) 156 + log.Printf("getting profile data for %s: %s", ident.DID.String(), err) 85 157 } 86 158 87 - s.pages.ProfilePage(w, pages.ProfilePageParams{ 88 - LoggedInUser: loggedInUser, 89 - UserDid: ident.DID.String(), 90 - UserHandle: ident.Handle.String(), 91 - Repos: repos, 92 - CollaboratingRepos: collaboratingRepos, 93 - ProfileStats: pages.ProfileStats{ 94 - Followers: followers, 95 - Following: following, 159 + repos, err := db.GetAllReposByDid(s.db, ident.DID.String()) 160 + if err != nil { 161 + log.Printf("getting repos for %s: %s", ident.DID.String(), err) 162 + } 163 + 164 + loggedInUser := s.oauth.GetUser(r) 165 + followStatus := db.IsNotFollowing 166 + if loggedInUser != nil { 167 + followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, ident.DID.String()) 168 + } 169 + 170 + followers, following, err := db.GetFollowerFollowing(s.db, ident.DID.String()) 171 + if err != nil { 172 + log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err) 173 + } 174 + 175 + profileAvatarUri := s.GetAvatarUri(ident.Handle.String()) 176 + 177 + s.pages.ReposPage(w, pages.ReposPageParams{ 178 + LoggedInUser: loggedInUser, 179 + Repos: repos, 180 + Card: pages.ProfileCard{ 181 + UserDid: ident.DID.String(), 182 + UserHandle: ident.Handle.String(), 183 + AvatarUri: profileAvatarUri, 184 + Profile: profile, 185 + FollowStatus: followStatus, 186 + Followers: followers, 187 + Following: following, 96 188 }, 97 - FollowStatus: db.FollowStatus(followStatus), 98 - DidHandleMap: didHandleMap, 99 - AvatarUri: profileAvatarUri, 100 - ProfileTimeline: timeline, 189 + }) 190 + } 191 + 192 + func (s *State) GetAvatarUri(handle string) string { 193 + secret := s.config.Avatar.SharedSecret 194 + h := hmac.New(sha256.New, []byte(secret)) 195 + h.Write([]byte(handle)) 196 + signature := hex.EncodeToString(h.Sum(nil)) 197 + return fmt.Sprintf("%s/%s/%s", s.config.Avatar.Host, signature, handle) 198 + } 199 + 200 + func (s *State) UpdateProfileBio(w http.ResponseWriter, r *http.Request) { 201 + user := s.oauth.GetUser(r) 202 + 203 + err := r.ParseForm() 204 + if err != nil { 205 + log.Println("invalid profile update form", err) 206 + s.pages.Notice(w, "update-profile", "Invalid form.") 207 + return 208 + } 209 + 210 + profile, err := db.GetProfile(s.db, user.Did) 211 + if err != nil { 212 + log.Printf("getting profile data for %s: %s", user.Did, err) 213 + } 214 + 215 + profile.Description = r.FormValue("description") 216 + profile.IncludeBluesky = r.FormValue("includeBluesky") == "on" 217 + profile.Location = r.FormValue("location") 218 + 219 + var links [5]string 220 + for i := range 5 { 221 + iLink := r.FormValue(fmt.Sprintf("link%d", i)) 222 + links[i] = iLink 223 + } 224 + profile.Links = links 225 + 226 + // Parse stats (exactly 2) 227 + stat0 := r.FormValue("stat0") 228 + stat1 := r.FormValue("stat1") 229 + 230 + if stat0 != "" { 231 + profile.Stats[0].Kind = db.VanityStatKind(stat0) 232 + } 233 + 234 + if stat1 != "" { 235 + profile.Stats[1].Kind = db.VanityStatKind(stat1) 236 + } 237 + 238 + if err := db.ValidateProfile(s.db, profile); err != nil { 239 + log.Println("invalid profile", err) 240 + s.pages.Notice(w, "update-profile", err.Error()) 241 + return 242 + } 243 + 244 + s.updateProfile(profile, w, r) 245 + return 246 + } 247 + 248 + func (s *State) UpdateProfilePins(w http.ResponseWriter, r *http.Request) { 249 + user := s.oauth.GetUser(r) 250 + 251 + err := r.ParseForm() 252 + if err != nil { 253 + log.Println("invalid profile update form", err) 254 + s.pages.Notice(w, "update-profile", "Invalid form.") 255 + return 256 + } 257 + 258 + profile, err := db.GetProfile(s.db, user.Did) 259 + if err != nil { 260 + log.Printf("getting profile data for %s: %s", user.Did, err) 261 + } 262 + 263 + i := 0 264 + var pinnedRepos [6]syntax.ATURI 265 + for key, values := range r.Form { 266 + if i >= 6 { 267 + log.Println("invalid pin update form", err) 268 + s.pages.Notice(w, "update-profile", "Only 6 repositories can be pinned at a time.") 269 + return 270 + } 271 + if strings.HasPrefix(key, "pinnedRepo") && len(values) > 0 && values[0] != "" && i < 6 { 272 + aturi, err := syntax.ParseATURI(values[0]) 273 + if err != nil { 274 + log.Println("invalid profile update form", err) 275 + s.pages.Notice(w, "update-profile", "Invalid form.") 276 + return 277 + } 278 + pinnedRepos[i] = aturi 279 + i++ 280 + } 281 + } 282 + profile.PinnedRepos = pinnedRepos 283 + 284 + s.updateProfile(profile, w, r) 285 + return 286 + } 287 + 288 + func (s *State) updateProfile(profile *db.Profile, w http.ResponseWriter, r *http.Request) { 289 + user := s.oauth.GetUser(r) 290 + tx, err := s.db.BeginTx(r.Context(), nil) 291 + if err != nil { 292 + log.Println("failed to start transaction", err) 293 + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 294 + return 295 + } 296 + 297 + client, err := s.oauth.AuthorizedClient(r) 298 + if err != nil { 299 + log.Println("failed to get authorized client", err) 300 + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 301 + return 302 + } 303 + 304 + // yeah... lexgen dose not support syntax.ATURI in the record for some reason, 305 + // nor does it support exact size arrays 306 + var pinnedRepoStrings []string 307 + for _, r := range profile.PinnedRepos { 308 + pinnedRepoStrings = append(pinnedRepoStrings, r.String()) 309 + } 310 + 311 + var vanityStats []string 312 + for _, v := range profile.Stats { 313 + vanityStats = append(vanityStats, string(v.Kind)) 314 + } 315 + 316 + ex, _ := client.RepoGetRecord(r.Context(), "", tangled.ActorProfileNSID, user.Did, "self") 317 + var cid *string 318 + if ex != nil { 319 + cid = ex.Cid 320 + } 321 + 322 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 323 + Collection: tangled.ActorProfileNSID, 324 + Repo: user.Did, 325 + Rkey: "self", 326 + Record: &lexutil.LexiconTypeDecoder{ 327 + Val: &tangled.ActorProfile{ 328 + Bluesky: profile.IncludeBluesky, 329 + Description: &profile.Description, 330 + Links: profile.Links[:], 331 + Location: &profile.Location, 332 + PinnedRepositories: pinnedRepoStrings, 333 + Stats: vanityStats[:], 334 + }}, 335 + SwapRecord: cid, 336 + }) 337 + if err != nil { 338 + log.Println("failed to update profile", err) 339 + s.pages.Notice(w, "update-profile", "Failed to update PDS, try again later.") 340 + return 341 + } 342 + 343 + err = db.UpsertProfile(tx, profile) 344 + if err != nil { 345 + log.Println("failed to update profile", err) 346 + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 347 + return 348 + } 349 + 350 + s.pages.HxRedirect(w, "/"+user.Did) 351 + return 352 + } 353 + 354 + func (s *State) EditBioFragment(w http.ResponseWriter, r *http.Request) { 355 + user := s.oauth.GetUser(r) 356 + 357 + profile, err := db.GetProfile(s.db, user.Did) 358 + if err != nil { 359 + log.Printf("getting profile data for %s: %s", user.Did, err) 360 + } 361 + 362 + s.pages.EditBioFragment(w, pages.EditBioParams{ 363 + LoggedInUser: user, 364 + Profile: profile, 365 + }) 366 + } 367 + 368 + func (s *State) EditPinsFragment(w http.ResponseWriter, r *http.Request) { 369 + user := s.oauth.GetUser(r) 370 + 371 + profile, err := db.GetProfile(s.db, user.Did) 372 + if err != nil { 373 + log.Printf("getting profile data for %s: %s", user.Did, err) 374 + } 375 + 376 + repos, err := db.GetAllReposByDid(s.db, user.Did) 377 + if err != nil { 378 + log.Printf("getting repos for %s: %s", user.Did, err) 379 + } 380 + 381 + collaboratingRepos, err := db.CollaboratingIn(s.db, user.Did) 382 + if err != nil { 383 + log.Printf("getting collaborating repos for %s: %s", user.Did, err) 384 + } 385 + 386 + allRepos := []pages.PinnedRepo{} 387 + 388 + for _, r := range repos { 389 + isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 390 + allRepos = append(allRepos, pages.PinnedRepo{ 391 + IsPinned: isPinned, 392 + Repo: r, 393 + }) 394 + } 395 + for _, r := range collaboratingRepos { 396 + isPinned := slices.Contains(profile.PinnedRepos[:], r.RepoAt()) 397 + allRepos = append(allRepos, pages.PinnedRepo{ 398 + IsPinned: isPinned, 399 + Repo: r, 400 + }) 401 + } 402 + 403 + var didsToResolve []string 404 + for _, r := range allRepos { 405 + didsToResolve = append(didsToResolve, r.Did) 406 + } 407 + resolvedIds := s.resolver.ResolveIdents(r.Context(), didsToResolve) 408 + didHandleMap := make(map[string]string) 409 + for _, identity := range resolvedIds { 410 + if !identity.Handle.IsInvalidHandle() { 411 + didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String()) 412 + } else { 413 + didHandleMap[identity.DID.String()] = identity.DID.String() 414 + } 415 + } 416 + 417 + s.pages.EditPinsFragment(w, pages.EditPinsParams{ 418 + LoggedInUser: user, 419 + Profile: profile, 420 + AllRepos: allRepos, 421 + DidHandleMap: didHandleMap, 101 422 }) 102 423 }
+122 -102
appview/state/pull.go
··· 8 8 "io" 9 9 "log" 10 10 "net/http" 11 - "net/url" 12 11 "strconv" 13 12 "time" 14 13 15 14 "tangled.sh/tangled.sh/core/api/tangled" 16 - "tangled.sh/tangled.sh/core/appview/auth" 15 + "tangled.sh/tangled.sh/core/appview" 17 16 "tangled.sh/tangled.sh/core/appview/db" 17 + "tangled.sh/tangled.sh/core/appview/knotclient" 18 + "tangled.sh/tangled.sh/core/appview/oauth" 18 19 "tangled.sh/tangled.sh/core/appview/pages" 19 20 "tangled.sh/tangled.sh/core/patchutil" 20 21 "tangled.sh/tangled.sh/core/types" ··· 29 30 func (s *State) PullActions(w http.ResponseWriter, r *http.Request) { 30 31 switch r.Method { 31 32 case http.MethodGet: 32 - user := s.auth.GetUser(r) 33 - f, err := fullyResolvedRepo(r) 33 + user := s.oauth.GetUser(r) 34 + f, err := s.fullyResolvedRepo(r) 34 35 if err != nil { 35 36 log.Println("failed to get repo and knot", err) 36 37 return ··· 73 74 } 74 75 75 76 func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 76 - user := s.auth.GetUser(r) 77 - f, err := fullyResolvedRepo(r) 77 + user := s.oauth.GetUser(r) 78 + f, err := s.fullyResolvedRepo(r) 78 79 if err != nil { 79 80 log.Println("failed to get repo and knot", err) 80 81 return ··· 120 121 resubmitResult = s.resubmitCheck(f, pull) 121 122 } 122 123 123 - var pullSourceRepo *db.Repo 124 - if pull.PullSource != nil { 125 - if pull.PullSource.RepoAt != nil { 126 - pullSourceRepo, err = db.GetRepoByAtUri(s.db, pull.PullSource.RepoAt.String()) 127 - if err != nil { 128 - log.Printf("failed to get repo by at uri: %v", err) 129 - return 130 - } 131 - } 132 - } 133 - 134 124 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 135 - LoggedInUser: user, 136 - RepoInfo: f.RepoInfo(s, user), 137 - DidHandleMap: didHandleMap, 138 - Pull: pull, 139 - PullSourceRepo: pullSourceRepo, 140 - MergeCheck: mergeCheckResponse, 141 - ResubmitCheck: resubmitResult, 125 + LoggedInUser: user, 126 + RepoInfo: f.RepoInfo(s, user), 127 + DidHandleMap: didHandleMap, 128 + Pull: pull, 129 + MergeCheck: mergeCheckResponse, 130 + ResubmitCheck: resubmitResult, 142 131 }) 143 132 } 144 133 ··· 155 144 } 156 145 } 157 146 158 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 147 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 159 148 if err != nil { 160 149 log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 161 150 return types.MergeCheckResponse{ ··· 227 216 repoName = f.RepoName 228 217 } 229 218 230 - us, err := NewUnsignedClient(knot, s.config.Dev) 219 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 231 220 if err != nil { 232 221 log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 233 222 return pages.Unknown ··· 262 251 } 263 252 264 253 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 265 - user := s.auth.GetUser(r) 266 - f, err := fullyResolvedRepo(r) 254 + user := s.oauth.GetUser(r) 255 + f, err := s.fullyResolvedRepo(r) 267 256 if err != nil { 268 257 log.Println("failed to get repo and knot", err) 269 258 return ··· 295 284 } 296 285 } 297 286 287 + diff := pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch) 288 + 298 289 s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{ 299 290 LoggedInUser: user, 300 291 DidHandleMap: didHandleMap, ··· 302 293 Pull: pull, 303 294 Round: roundIdInt, 304 295 Submission: pull.Submissions[roundIdInt], 305 - Diff: pull.Submissions[roundIdInt].AsNiceDiff(pull.TargetBranch), 296 + Diff: &diff, 306 297 }) 307 298 308 299 } 309 300 310 301 func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 311 - user := s.auth.GetUser(r) 302 + user := s.oauth.GetUser(r) 312 303 313 - f, err := fullyResolvedRepo(r) 304 + f, err := s.fullyResolvedRepo(r) 314 305 if err != nil { 315 306 log.Println("failed to get repo and knot", err) 316 307 return ··· 365 356 interdiff := patchutil.Interdiff(previousPatch, currentPatch) 366 357 367 358 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 368 - LoggedInUser: s.auth.GetUser(r), 359 + LoggedInUser: s.oauth.GetUser(r), 369 360 RepoInfo: f.RepoInfo(s, user), 370 361 Pull: pull, 371 362 Round: roundIdInt, ··· 407 398 } 408 399 409 400 func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 410 - user := s.auth.GetUser(r) 401 + user := s.oauth.GetUser(r) 411 402 params := r.URL.Query() 412 403 413 404 state := db.PullOpen ··· 418 409 state = db.PullMerged 419 410 } 420 411 421 - f, err := fullyResolvedRepo(r) 412 + f, err := s.fullyResolvedRepo(r) 422 413 if err != nil { 423 414 log.Println("failed to get repo and knot", err) 424 415 return ··· 461 452 } 462 453 463 454 s.pages.RepoPulls(w, pages.RepoPullsParams{ 464 - LoggedInUser: s.auth.GetUser(r), 455 + LoggedInUser: s.oauth.GetUser(r), 465 456 RepoInfo: f.RepoInfo(s, user), 466 457 Pulls: pulls, 467 458 DidHandleMap: didHandleMap, ··· 471 462 } 472 463 473 464 func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 474 - user := s.auth.GetUser(r) 475 - f, err := fullyResolvedRepo(r) 465 + user := s.oauth.GetUser(r) 466 + f, err := s.fullyResolvedRepo(r) 476 467 if err != nil { 477 468 log.Println("failed to get repo and knot", err) 478 469 return ··· 529 520 } 530 521 531 522 atUri := f.RepoAt.String() 532 - client, _ := s.auth.AuthorizedClient(r) 533 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 523 + client, err := s.oauth.AuthorizedClient(r) 524 + if err != nil { 525 + log.Println("failed to get authorized client", err) 526 + s.pages.Notice(w, "pull-comment", "Failed to create comment.") 527 + return 528 + } 529 + atResp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 534 530 Collection: tangled.RepoPullCommentNSID, 535 531 Repo: user.Did, 536 - Rkey: s.TID(), 532 + Rkey: appview.TID(), 537 533 Record: &lexutil.LexiconTypeDecoder{ 538 534 Val: &tangled.RepoPullComment{ 539 535 Repo: &atUri, 540 - Pull: pullAt, 536 + Pull: string(pullAt), 541 537 Owner: &ownerDid, 542 - Body: &body, 543 - CreatedAt: &createdAt, 538 + Body: body, 539 + CreatedAt: createdAt, 544 540 }, 545 541 }, 546 542 }) ··· 578 574 } 579 575 580 576 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 581 - user := s.auth.GetUser(r) 582 - f, err := fullyResolvedRepo(r) 577 + user := s.oauth.GetUser(r) 578 + f, err := s.fullyResolvedRepo(r) 583 579 if err != nil { 584 580 log.Println("failed to get repo and knot", err) 585 581 return ··· 587 583 588 584 switch r.Method { 589 585 case http.MethodGet: 590 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 586 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 591 587 if err != nil { 592 588 log.Printf("failed to create unsigned client for %s", f.Knot) 593 589 s.pages.Error503(w) ··· 656 652 return 657 653 } 658 654 659 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 655 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 660 656 if err != nil { 661 657 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 662 658 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") ··· 699 695 } 700 696 } 701 697 702 - func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, sourceBranch string) { 698 + func (s *State) handleBranchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, sourceBranch string) { 703 699 pullSource := &db.PullSource{ 704 700 Branch: sourceBranch, 705 701 } ··· 708 704 } 709 705 710 706 // Generate a patch using /compare 711 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 707 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 712 708 if err != nil { 713 709 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 714 710 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 733 729 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 734 730 } 735 731 736 - func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, title, body, targetBranch, patch string) { 732 + func (s *State) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, title, body, targetBranch, patch string) { 737 733 if !patchutil.IsPatchValid(patch) { 738 734 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 739 735 return ··· 742 738 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 743 739 } 744 740 745 - func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *auth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 741 + func (s *State) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *FullyResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string) { 746 742 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 747 743 if errors.Is(err, sql.ErrNoRows) { 748 744 s.pages.Notice(w, "pull", "No such fork.") ··· 760 756 return 761 757 } 762 758 763 - sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 759 + sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev) 764 760 if err != nil { 765 761 log.Println("failed to create signed client:", err) 766 762 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 767 763 return 768 764 } 769 765 770 - us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 766 + us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev) 771 767 if err != nil { 772 768 log.Println("failed to create unsigned client:", err) 773 769 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 788 784 return 789 785 } 790 786 791 - hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch)) 787 + hiddenRef := fmt.Sprintf("hidden/%s/%s", sourceBranch, targetBranch) 792 788 // We're now comparing the sourceBranch (on the fork) against the hiddenRef which is tracking 793 789 // the targetBranch on the target repository. This code is a bit confusing, but here's an example: 794 790 // hiddenRef: hidden/feature-1/main (on repo-fork) ··· 804 800 sourceRev := comparison.Rev2 805 801 patch := comparison.Patch 806 802 807 - if patchutil.IsPatchValid(patch) { 803 + if !patchutil.IsPatchValid(patch) { 808 804 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 809 805 return 810 806 } ··· 826 822 w http.ResponseWriter, 827 823 r *http.Request, 828 824 f *FullyResolvedRepo, 829 - user *auth.User, 825 + user *oauth.User, 830 826 title, body, targetBranch string, 831 827 patch string, 832 828 sourceRev string, ··· 858 854 body = formatPatches[0].Body 859 855 } 860 856 861 - rkey := s.TID() 857 + rkey := appview.TID() 862 858 initialSubmission := db.PullSubmission{ 863 859 Patch: patch, 864 860 SourceRev: sourceRev, ··· 880 876 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 881 877 return 882 878 } 883 - client, _ := s.auth.AuthorizedClient(r) 884 - pullId, err := db.NextPullId(s.db, f.RepoAt) 879 + client, err := s.oauth.AuthorizedClient(r) 880 + if err != nil { 881 + log.Println("failed to get authorized client", err) 882 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 883 + return 884 + } 885 + pullId, err := db.NextPullId(tx, f.RepoAt) 885 886 if err != nil { 886 887 log.Println("failed to get pull id", err) 887 888 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 888 889 return 889 890 } 890 891 891 - atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 892 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 892 893 Collection: tangled.RepoPullNSID, 893 894 Repo: user.Did, 894 895 Rkey: rkey, ··· 903 904 }, 904 905 }, 905 906 }) 906 - 907 - err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri) 908 907 if err != nil { 909 - log.Println("failed to get pull id", err) 908 + log.Println("failed to create pull request", err) 909 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 910 + return 911 + } 912 + 913 + if err = tx.Commit(); err != nil { 914 + log.Println("failed to create pull request", err) 910 915 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 911 916 return 912 917 } ··· 915 920 } 916 921 917 922 func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { 918 - _, err := fullyResolvedRepo(r) 923 + _, err := s.fullyResolvedRepo(r) 919 924 if err != nil { 920 925 log.Println("failed to get repo and knot", err) 921 926 return ··· 940 945 } 941 946 942 947 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 943 - user := s.auth.GetUser(r) 944 - f, err := fullyResolvedRepo(r) 948 + user := s.oauth.GetUser(r) 949 + f, err := s.fullyResolvedRepo(r) 945 950 if err != nil { 946 951 log.Println("failed to get repo and knot", err) 947 952 return ··· 953 958 } 954 959 955 960 func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 956 - user := s.auth.GetUser(r) 957 - f, err := fullyResolvedRepo(r) 961 + user := s.oauth.GetUser(r) 962 + f, err := s.fullyResolvedRepo(r) 958 963 if err != nil { 959 964 log.Println("failed to get repo and knot", err) 960 965 return 961 966 } 962 967 963 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 968 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 964 969 if err != nil { 965 970 log.Printf("failed to create unsigned client for %s", f.Knot) 966 971 s.pages.Error503(w) ··· 993 998 } 994 999 995 1000 func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 996 - user := s.auth.GetUser(r) 997 - f, err := fullyResolvedRepo(r) 1001 + user := s.oauth.GetUser(r) 1002 + f, err := s.fullyResolvedRepo(r) 998 1003 if err != nil { 999 1004 log.Println("failed to get repo and knot", err) 1000 1005 return ··· 1013 1018 } 1014 1019 1015 1020 func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1016 - user := s.auth.GetUser(r) 1021 + user := s.oauth.GetUser(r) 1017 1022 1018 - f, err := fullyResolvedRepo(r) 1023 + f, err := s.fullyResolvedRepo(r) 1019 1024 if err != nil { 1020 1025 log.Println("failed to get repo and knot", err) 1021 1026 return ··· 1030 1035 return 1031 1036 } 1032 1037 1033 - sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 1038 + sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev) 1034 1039 if err != nil { 1035 1040 log.Printf("failed to create unsigned client for %s", repo.Knot) 1036 1041 s.pages.Error503(w) ··· 1057 1062 return 1058 1063 } 1059 1064 1060 - targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1065 + targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1061 1066 if err != nil { 1062 1067 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1063 1068 s.pages.Error503(w) ··· 1092 1097 } 1093 1098 1094 1099 func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1095 - user := s.auth.GetUser(r) 1096 - f, err := fullyResolvedRepo(r) 1100 + user := s.oauth.GetUser(r) 1101 + f, err := s.fullyResolvedRepo(r) 1097 1102 if err != nil { 1098 1103 log.Println("failed to get repo and knot", err) 1099 1104 return ··· 1128 1133 } 1129 1134 1130 1135 func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1131 - user := s.auth.GetUser(r) 1136 + user := s.oauth.GetUser(r) 1132 1137 1133 1138 pull, ok := r.Context().Value("pull").(*db.Pull) 1134 1139 if !ok { ··· 1137 1142 return 1138 1143 } 1139 1144 1140 - f, err := fullyResolvedRepo(r) 1145 + f, err := s.fullyResolvedRepo(r) 1141 1146 if err != nil { 1142 1147 log.Println("failed to get repo and knot", err) 1143 1148 return ··· 1170 1175 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1171 1176 return 1172 1177 } 1173 - client, _ := s.auth.AuthorizedClient(r) 1178 + client, err := s.oauth.AuthorizedClient(r) 1179 + if err != nil { 1180 + log.Println("failed to get authorized client", err) 1181 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1182 + return 1183 + } 1174 1184 1175 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1185 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1176 1186 if err != nil { 1177 1187 // failed to get record 1178 1188 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1179 1189 return 1180 1190 } 1181 1191 1182 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1192 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1183 1193 Collection: tangled.RepoPullNSID, 1184 1194 Repo: user.Did, 1185 1195 Rkey: pull.Rkey, ··· 1211 1221 } 1212 1222 1213 1223 func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1214 - user := s.auth.GetUser(r) 1224 + user := s.oauth.GetUser(r) 1215 1225 1216 1226 pull, ok := r.Context().Value("pull").(*db.Pull) 1217 1227 if !ok { ··· 1220 1230 return 1221 1231 } 1222 1232 1223 - f, err := fullyResolvedRepo(r) 1233 + f, err := s.fullyResolvedRepo(r) 1224 1234 if err != nil { 1225 1235 log.Println("failed to get repo and knot", err) 1226 1236 return ··· 1238 1248 return 1239 1249 } 1240 1250 1241 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1251 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1242 1252 if err != nil { 1243 1253 log.Printf("failed to create client for %s: %s", f.Knot, err) 1244 1254 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1279 1289 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1280 1290 return 1281 1291 } 1282 - client, _ := s.auth.AuthorizedClient(r) 1292 + client, err := s.oauth.AuthorizedClient(r) 1293 + if err != nil { 1294 + log.Println("failed to authorize client") 1295 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1296 + return 1297 + } 1283 1298 1284 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1299 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1285 1300 if err != nil { 1286 1301 // failed to get record 1287 1302 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1291 1306 recordPullSource := &tangled.RepoPull_Source{ 1292 1307 Branch: pull.PullSource.Branch, 1293 1308 } 1294 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1309 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1295 1310 Collection: tangled.RepoPullNSID, 1296 1311 Repo: user.Did, 1297 1312 Rkey: pull.Rkey, ··· 1324 1339 } 1325 1340 1326 1341 func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1327 - user := s.auth.GetUser(r) 1342 + user := s.oauth.GetUser(r) 1328 1343 1329 1344 pull, ok := r.Context().Value("pull").(*db.Pull) 1330 1345 if !ok { ··· 1333 1348 return 1334 1349 } 1335 1350 1336 - f, err := fullyResolvedRepo(r) 1351 + f, err := s.fullyResolvedRepo(r) 1337 1352 if err != nil { 1338 1353 log.Println("failed to get repo and knot", err) 1339 1354 return ··· 1353 1368 } 1354 1369 1355 1370 // extract patch by performing compare 1356 - ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1371 + ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev) 1357 1372 if err != nil { 1358 1373 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1359 1374 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1368 1383 } 1369 1384 1370 1385 // update the hidden tracking branch to latest 1371 - signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1386 + signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev) 1372 1387 if err != nil { 1373 1388 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1374 1389 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1382 1397 return 1383 1398 } 1384 1399 1385 - hiddenRef := url.QueryEscape(fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch)) 1400 + hiddenRef := fmt.Sprintf("hidden/%s/%s", pull.PullSource.Branch, pull.TargetBranch) 1386 1401 comparison, err := ksClient.Compare(forkRepo.Did, forkRepo.Name, hiddenRef, pull.PullSource.Branch) 1387 1402 if err != nil { 1388 1403 log.Printf("failed to compare branches: %s", err) ··· 1417 1432 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1418 1433 return 1419 1434 } 1420 - client, _ := s.auth.AuthorizedClient(r) 1435 + client, err := s.oauth.AuthorizedClient(r) 1436 + if err != nil { 1437 + log.Println("failed to get client") 1438 + s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1439 + return 1440 + } 1421 1441 1422 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1442 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoPullNSID, user.Did, pull.Rkey) 1423 1443 if err != nil { 1424 1444 // failed to get record 1425 1445 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1431 1451 Branch: pull.PullSource.Branch, 1432 1452 Repo: &repoAt, 1433 1453 } 1434 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1454 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1435 1455 Collection: tangled.RepoPullNSID, 1436 1456 Repo: user.Did, 1437 1457 Rkey: pull.Rkey, ··· 1481 1501 } 1482 1502 1483 1503 func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1484 - f, err := fullyResolvedRepo(r) 1504 + f, err := s.fullyResolvedRepo(r) 1485 1505 if err != nil { 1486 1506 log.Println("failed to resolve repo:", err) 1487 1507 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1514 1534 log.Printf("failed to get primary email: %s", err) 1515 1535 } 1516 1536 1517 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1537 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1518 1538 if err != nil { 1519 1539 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1520 1540 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1544 1564 } 1545 1565 1546 1566 func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1547 - user := s.auth.GetUser(r) 1567 + user := s.oauth.GetUser(r) 1548 1568 1549 - f, err := fullyResolvedRepo(r) 1569 + f, err := s.fullyResolvedRepo(r) 1550 1570 if err != nil { 1551 1571 log.Println("malformed middleware") 1552 1572 return ··· 1598 1618 } 1599 1619 1600 1620 func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1601 - user := s.auth.GetUser(r) 1621 + user := s.oauth.GetUser(r) 1602 1622 1603 - f, err := fullyResolvedRepo(r) 1623 + f, err := s.fullyResolvedRepo(r) 1604 1624 if err != nil { 1605 1625 log.Println("failed to resolve repo", err) 1606 1626 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
+276 -147
appview/state/repo.go
··· 16 16 "strings" 17 17 "time" 18 18 19 + "tangled.sh/tangled.sh/core/api/tangled" 20 + "tangled.sh/tangled.sh/core/appview" 21 + "tangled.sh/tangled.sh/core/appview/db" 22 + "tangled.sh/tangled.sh/core/appview/knotclient" 23 + "tangled.sh/tangled.sh/core/appview/oauth" 24 + "tangled.sh/tangled.sh/core/appview/pages" 25 + "tangled.sh/tangled.sh/core/appview/pages/markup" 26 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 27 + "tangled.sh/tangled.sh/core/appview/pagination" 28 + "tangled.sh/tangled.sh/core/types" 29 + 19 30 "github.com/bluesky-social/indigo/atproto/data" 20 31 "github.com/bluesky-social/indigo/atproto/identity" 21 32 "github.com/bluesky-social/indigo/atproto/syntax" 22 33 securejoin "github.com/cyphar/filepath-securejoin" 23 34 "github.com/go-chi/chi/v5" 24 35 "github.com/go-git/go-git/v5/plumbing" 25 - "tangled.sh/tangled.sh/core/api/tangled" 26 - "tangled.sh/tangled.sh/core/appview/auth" 27 - "tangled.sh/tangled.sh/core/appview/db" 28 - "tangled.sh/tangled.sh/core/appview/pages" 29 - "tangled.sh/tangled.sh/core/appview/pages/markup" 30 - "tangled.sh/tangled.sh/core/types" 31 36 32 37 comatproto "github.com/bluesky-social/indigo/api/atproto" 33 38 lexutil "github.com/bluesky-social/indigo/lex/util" ··· 35 40 36 41 func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 37 42 ref := chi.URLParam(r, "ref") 38 - f, err := fullyResolvedRepo(r) 43 + f, err := s.fullyResolvedRepo(r) 39 44 if err != nil { 40 45 log.Println("failed to fully resolve repo", err) 41 46 return 42 47 } 43 48 44 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 49 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 45 50 if err != nil { 46 51 log.Printf("failed to create unsigned client for %s", f.Knot) 47 52 s.pages.Error503(w) ··· 72 77 tagMap := make(map[string][]string) 73 78 for _, tag := range result.Tags { 74 79 hash := tag.Hash 80 + if tag.Tag != nil { 81 + hash = tag.Tag.Target.String() 82 + } 75 83 tagMap[hash] = append(tagMap[hash], tag.Name) 76 84 } 77 85 ··· 80 88 tagMap[hash] = append(tagMap[hash], branch.Name) 81 89 } 82 90 83 - emails := uniqueEmails(result.Commits) 91 + slices.SortFunc(result.Branches, func(a, b types.Branch) int { 92 + if a.Name == result.Ref { 93 + return -1 94 + } 95 + if a.IsDefault { 96 + return -1 97 + } 98 + if b.IsDefault { 99 + return 1 100 + } 101 + if a.Commit != nil { 102 + if a.Commit.Author.When.Before(b.Commit.Author.When) { 103 + return 1 104 + } else { 105 + return -1 106 + } 107 + } 108 + return strings.Compare(a.Name, b.Name) * -1 109 + }) 84 110 85 - user := s.auth.GetUser(r) 111 + commitCount := len(result.Commits) 112 + branchCount := len(result.Branches) 113 + tagCount := len(result.Tags) 114 + fileCount := len(result.Files) 115 + 116 + commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount) 117 + commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))] 118 + tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))] 119 + branchesTrunc := result.Branches[:min(branchCount, len(result.Branches))] 120 + 121 + emails := uniqueEmails(commitsTrunc) 122 + 123 + user := s.oauth.GetUser(r) 86 124 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 87 125 LoggedInUser: user, 88 126 RepoInfo: f.RepoInfo(s, user), 89 127 TagMap: tagMap, 90 128 RepoIndexResponse: result, 129 + CommitsTrunc: commitsTrunc, 130 + TagsTrunc: tagsTrunc, 131 + BranchesTrunc: branchesTrunc, 91 132 EmailToDidOrHandle: EmailToDidOrHandle(s, emails), 92 133 }) 93 134 return 94 135 } 95 136 96 137 func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 97 - f, err := fullyResolvedRepo(r) 138 + f, err := s.fullyResolvedRepo(r) 98 139 if err != nil { 99 140 log.Println("failed to fully resolve repo", err) 100 141 return ··· 110 151 111 152 ref := chi.URLParam(r, "ref") 112 153 113 - protocol := "http" 114 - if !s.config.Dev { 115 - protocol = "https" 154 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 155 + if err != nil { 156 + log.Println("failed to create unsigned client", err) 157 + return 116 158 } 117 159 118 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/log/%s?page=%d&per_page=30", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, page)) 160 + resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page) 119 161 if err != nil { 120 162 log.Println("failed to reach knotserver", err) 121 163 return ··· 134 176 return 135 177 } 136 178 137 - user := s.auth.GetUser(r) 179 + result, err := us.Tags(f.OwnerDid(), f.RepoName) 180 + if err != nil { 181 + log.Println("failed to reach knotserver", err) 182 + return 183 + } 184 + 185 + tagMap := make(map[string][]string) 186 + for _, tag := range result.Tags { 187 + hash := tag.Hash 188 + if tag.Tag != nil { 189 + hash = tag.Tag.Target.String() 190 + } 191 + tagMap[hash] = append(tagMap[hash], tag.Name) 192 + } 193 + 194 + user := s.oauth.GetUser(r) 138 195 s.pages.RepoLog(w, pages.RepoLogParams{ 139 196 LoggedInUser: user, 197 + TagMap: tagMap, 140 198 RepoInfo: f.RepoInfo(s, user), 141 199 RepoLogResponse: repolog, 142 200 EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), ··· 145 203 } 146 204 147 205 func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 148 - f, err := fullyResolvedRepo(r) 206 + f, err := s.fullyResolvedRepo(r) 149 207 if err != nil { 150 208 log.Println("failed to get repo and knot", err) 151 209 w.WriteHeader(http.StatusBadRequest) 152 210 return 153 211 } 154 212 155 - user := s.auth.GetUser(r) 213 + user := s.oauth.GetUser(r) 156 214 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 157 215 RepoInfo: f.RepoInfo(s, user), 158 216 }) ··· 160 218 } 161 219 162 220 func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 163 - f, err := fullyResolvedRepo(r) 221 + f, err := s.fullyResolvedRepo(r) 164 222 if err != nil { 165 223 log.Println("failed to get repo and knot", err) 166 224 w.WriteHeader(http.StatusBadRequest) ··· 175 233 return 176 234 } 177 235 178 - user := s.auth.GetUser(r) 236 + user := s.oauth.GetUser(r) 179 237 180 238 switch r.Method { 181 239 case http.MethodGet: ··· 184 242 }) 185 243 return 186 244 case http.MethodPut: 187 - user := s.auth.GetUser(r) 245 + user := s.oauth.GetUser(r) 188 246 newDescription := r.FormValue("description") 189 - client, _ := s.auth.AuthorizedClient(r) 247 + client, err := s.oauth.AuthorizedClient(r) 248 + if err != nil { 249 + log.Println("failed to get client") 250 + s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 251 + return 252 + } 190 253 191 254 // optimistic update 192 255 err = db.UpdateDescription(s.db, string(repoAt), newDescription) ··· 199 262 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 200 263 // 201 264 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 202 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey) 265 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 203 266 if err != nil { 204 267 // failed to get record 205 268 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 206 269 return 207 270 } 208 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 271 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 209 272 Collection: tangled.RepoNSID, 210 273 Repo: user.Did, 211 274 Rkey: rkey, ··· 215 278 Knot: f.Knot, 216 279 Name: f.RepoName, 217 280 Owner: user.Did, 218 - AddedAt: &f.AddedAt, 281 + CreatedAt: f.CreatedAt, 219 282 Description: &newDescription, 220 283 }, 221 284 }, ··· 239 302 } 240 303 241 304 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 242 - f, err := fullyResolvedRepo(r) 305 + f, err := s.fullyResolvedRepo(r) 243 306 if err != nil { 244 307 log.Println("failed to fully resolve repo", err) 245 308 return 246 309 } 247 310 ref := chi.URLParam(r, "ref") 248 311 protocol := "http" 249 - if !s.config.Dev { 312 + if !s.config.Core.Dev { 250 313 protocol = "https" 251 314 } 252 315 ··· 274 337 return 275 338 } 276 339 277 - user := s.auth.GetUser(r) 340 + user := s.oauth.GetUser(r) 278 341 s.pages.RepoCommit(w, pages.RepoCommitParams{ 279 342 LoggedInUser: user, 280 343 RepoInfo: f.RepoInfo(s, user), ··· 285 348 } 286 349 287 350 func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 288 - f, err := fullyResolvedRepo(r) 351 + f, err := s.fullyResolvedRepo(r) 289 352 if err != nil { 290 353 log.Println("failed to fully resolve repo", err) 291 354 return ··· 294 357 ref := chi.URLParam(r, "ref") 295 358 treePath := chi.URLParam(r, "*") 296 359 protocol := "http" 297 - if !s.config.Dev { 360 + if !s.config.Core.Dev { 298 361 protocol = "https" 299 362 } 300 363 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) ··· 316 379 return 317 380 } 318 381 319 - user := s.auth.GetUser(r) 382 + // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 383 + // so we can safely redirect to the "parent" (which is the same file). 384 + if len(result.Files) == 0 && result.Parent == treePath { 385 + http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound) 386 + return 387 + } 388 + 389 + user := s.oauth.GetUser(r) 320 390 321 391 var breadcrumbs [][]string 322 392 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) ··· 341 411 } 342 412 343 413 func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 344 - f, err := fullyResolvedRepo(r) 414 + f, err := s.fullyResolvedRepo(r) 345 415 if err != nil { 346 416 log.Println("failed to get repo and knot", err) 347 417 return 348 418 } 349 419 350 - protocol := "http" 351 - if !s.config.Dev { 352 - protocol = "https" 420 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 421 + if err != nil { 422 + log.Println("failed to create unsigned client", err) 423 + return 353 424 } 354 425 355 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tags", protocol, f.Knot, f.OwnerDid(), f.RepoName)) 426 + result, err := us.Tags(f.OwnerDid(), f.RepoName) 356 427 if err != nil { 357 428 log.Println("failed to reach knotserver", err) 358 429 return 359 430 } 360 431 361 - body, err := io.ReadAll(resp.Body) 432 + artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt)) 362 433 if err != nil { 363 - log.Printf("Error reading response body: %v", err) 434 + log.Println("failed grab artifacts", err) 364 435 return 365 436 } 366 437 367 - var result types.RepoTagsResponse 368 - err = json.Unmarshal(body, &result) 369 - if err != nil { 370 - log.Println("failed to parse response:", err) 371 - return 438 + // convert artifacts to map for easy UI building 439 + artifactMap := make(map[plumbing.Hash][]db.Artifact) 440 + for _, a := range artifacts { 441 + artifactMap[a.Tag] = append(artifactMap[a.Tag], a) 372 442 } 373 443 374 - user := s.auth.GetUser(r) 444 + var danglingArtifacts []db.Artifact 445 + for _, a := range artifacts { 446 + found := false 447 + for _, t := range result.Tags { 448 + if t.Tag != nil { 449 + if t.Tag.Hash == a.Tag { 450 + found = true 451 + } 452 + } 453 + } 454 + 455 + if !found { 456 + danglingArtifacts = append(danglingArtifacts, a) 457 + } 458 + } 459 + 460 + user := s.oauth.GetUser(r) 375 461 s.pages.RepoTags(w, pages.RepoTagsParams{ 376 - LoggedInUser: user, 377 - RepoInfo: f.RepoInfo(s, user), 378 - RepoTagsResponse: result, 462 + LoggedInUser: user, 463 + RepoInfo: f.RepoInfo(s, user), 464 + RepoTagsResponse: *result, 465 + ArtifactMap: artifactMap, 466 + DanglingArtifacts: danglingArtifacts, 379 467 }) 380 468 return 381 469 } 382 470 383 471 func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 384 - f, err := fullyResolvedRepo(r) 472 + f, err := s.fullyResolvedRepo(r) 385 473 if err != nil { 386 474 log.Println("failed to get repo and knot", err) 387 475 return 388 476 } 389 477 390 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 478 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 391 479 if err != nil { 392 480 log.Println("failed to create unsigned client", err) 393 481 return ··· 412 500 return 413 501 } 414 502 415 - user := s.auth.GetUser(r) 503 + slices.SortFunc(result.Branches, func(a, b types.Branch) int { 504 + if a.IsDefault { 505 + return -1 506 + } 507 + if b.IsDefault { 508 + return 1 509 + } 510 + if a.Commit != nil { 511 + if a.Commit.Author.When.Before(b.Commit.Author.When) { 512 + return 1 513 + } else { 514 + return -1 515 + } 516 + } 517 + return strings.Compare(a.Name, b.Name) * -1 518 + }) 519 + 520 + user := s.oauth.GetUser(r) 416 521 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 417 522 LoggedInUser: user, 418 523 RepoInfo: f.RepoInfo(s, user), ··· 422 527 } 423 528 424 529 func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 425 - f, err := fullyResolvedRepo(r) 530 + f, err := s.fullyResolvedRepo(r) 426 531 if err != nil { 427 532 log.Println("failed to get repo and knot", err) 428 533 return ··· 431 536 ref := chi.URLParam(r, "ref") 432 537 filePath := chi.URLParam(r, "*") 433 538 protocol := "http" 434 - if !s.config.Dev { 539 + if !s.config.Core.Dev { 435 540 protocol = "https" 436 541 } 437 542 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 469 574 showRendered = r.URL.Query().Get("code") != "true" 470 575 } 471 576 472 - user := s.auth.GetUser(r) 577 + user := s.oauth.GetUser(r) 473 578 s.pages.RepoBlob(w, pages.RepoBlobParams{ 474 579 LoggedInUser: user, 475 580 RepoInfo: f.RepoInfo(s, user), ··· 482 587 } 483 588 484 589 func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 485 - f, err := fullyResolvedRepo(r) 590 + f, err := s.fullyResolvedRepo(r) 486 591 if err != nil { 487 592 log.Println("failed to get repo and knot", err) 488 593 return ··· 492 597 filePath := chi.URLParam(r, "*") 493 598 494 599 protocol := "http" 495 - if !s.config.Dev { 600 + if !s.config.Core.Dev { 496 601 protocol = "https" 497 602 } 498 603 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 526 631 } 527 632 528 633 func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 529 - f, err := fullyResolvedRepo(r) 634 + f, err := s.fullyResolvedRepo(r) 530 635 if err != nil { 531 636 log.Println("failed to get repo and knot", err) 532 637 return ··· 553 658 return 554 659 } 555 660 556 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 661 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 557 662 if err != nil { 558 663 log.Println("failed to create client to ", f.Knot) 559 664 return ··· 615 720 } 616 721 617 722 func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 618 - user := s.auth.GetUser(r) 723 + user := s.oauth.GetUser(r) 619 724 620 - f, err := fullyResolvedRepo(r) 725 + f, err := s.fullyResolvedRepo(r) 621 726 if err != nil { 622 727 log.Println("failed to get repo and knot", err) 623 728 return 624 729 } 625 730 626 731 // remove record from pds 627 - xrpcClient, _ := s.auth.AuthorizedClient(r) 732 + xrpcClient, err := s.oauth.AuthorizedClient(r) 733 + if err != nil { 734 + log.Println("failed to get authorized client", err) 735 + return 736 + } 628 737 repoRkey := f.RepoAt.RecordKey().String() 629 - _, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{ 738 + _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 630 739 Collection: tangled.RepoNSID, 631 740 Repo: user.Did, 632 741 Rkey: repoRkey, ··· 644 753 return 645 754 } 646 755 647 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 756 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 648 757 if err != nil { 649 758 log.Println("failed to create client to ", f.Knot) 650 759 return ··· 721 830 } 722 831 723 832 func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 724 - f, err := fullyResolvedRepo(r) 833 + f, err := s.fullyResolvedRepo(r) 725 834 if err != nil { 726 835 log.Println("failed to get repo and knot", err) 727 836 return ··· 739 848 return 740 849 } 741 850 742 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 851 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 743 852 if err != nil { 744 853 log.Println("failed to create client to ", f.Knot) 745 854 return ··· 760 869 } 761 870 762 871 func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 763 - f, err := fullyResolvedRepo(r) 872 + f, err := s.fullyResolvedRepo(r) 764 873 if err != nil { 765 874 log.Println("failed to get repo and knot", err) 766 875 return ··· 769 878 switch r.Method { 770 879 case http.MethodGet: 771 880 // for now, this is just pubkeys 772 - user := s.auth.GetUser(r) 881 + user := s.oauth.GetUser(r) 773 882 repoCollaborators, err := f.Collaborators(r.Context(), s) 774 883 if err != nil { 775 884 log.Println("failed to get collaborators", err) ··· 785 894 786 895 var branchNames []string 787 896 var defaultBranch string 788 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 897 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 789 898 if err != nil { 790 899 log.Println("failed to create unsigned client", err) 791 900 } else { ··· 811 920 } 812 921 } 813 922 814 - resp, err = us.DefaultBranch(f.OwnerDid(), f.RepoName) 923 + defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName) 815 924 if err != nil { 816 925 log.Println("failed to reach knotserver", err) 817 926 } else { 818 - defer resp.Body.Close() 819 - 820 - body, err := io.ReadAll(resp.Body) 821 - if err != nil { 822 - log.Printf("Error reading response body: %v", err) 823 - } else { 824 - var result types.RepoDefaultBranchResponse 825 - err = json.Unmarshal(body, &result) 826 - if err != nil { 827 - log.Println("failed to parse response:", err) 828 - } else { 829 - defaultBranch = result.Branch 830 - } 831 - } 927 + defaultBranch = defaultBranchResp.Branch 832 928 } 833 929 } 834 - 835 930 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 836 931 LoggedInUser: user, 837 932 RepoInfo: f.RepoInfo(s, user), ··· 849 944 RepoName string 850 945 RepoAt syntax.ATURI 851 946 Description string 852 - AddedAt string 947 + CreatedAt string 948 + Ref string 853 949 } 854 950 855 951 func (f *FullyResolvedRepo) OwnerDid() string { ··· 922 1018 return collaborators, nil 923 1019 } 924 1020 925 - func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo { 1021 + func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo { 926 1022 isStarred := false 927 1023 if u != nil { 928 1024 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) ··· 965 1061 966 1062 knot := f.Knot 967 1063 var disableFork bool 968 - us, err := NewUnsignedClient(knot, s.config.Dev) 1064 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 969 1065 if err != nil { 970 1066 log.Printf("failed to create unsigned client for %s: %v", knot, err) 971 1067 } else { ··· 992 1088 } 993 1089 } 994 1090 995 - if knot == "knot1.tangled.sh" { 996 - knot = "tangled.sh" 997 - } 998 - 999 - repoInfo := pages.RepoInfo{ 1091 + repoInfo := repoinfo.RepoInfo{ 1000 1092 OwnerDid: f.OwnerDid(), 1001 1093 OwnerHandle: f.OwnerHandle(), 1002 1094 Name: f.RepoName, 1003 1095 RepoAt: f.RepoAt, 1004 1096 Description: f.Description, 1097 + Ref: f.Ref, 1005 1098 IsStarred: isStarred, 1006 1099 Knot: knot, 1007 1100 Roles: RolesInRepo(s, u, f), ··· 1022 1115 } 1023 1116 1024 1117 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1025 - user := s.auth.GetUser(r) 1026 - f, err := fullyResolvedRepo(r) 1118 + user := s.oauth.GetUser(r) 1119 + f, err := s.fullyResolvedRepo(r) 1027 1120 if err != nil { 1028 1121 log.Println("failed to get repo and knot", err) 1029 1122 return ··· 1076 1169 } 1077 1170 1078 1171 func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1079 - user := s.auth.GetUser(r) 1080 - f, err := fullyResolvedRepo(r) 1172 + user := s.oauth.GetUser(r) 1173 + f, err := s.fullyResolvedRepo(r) 1081 1174 if err != nil { 1082 1175 log.Println("failed to get repo and knot", err) 1083 1176 return ··· 1112 1205 1113 1206 closed := tangled.RepoIssueStateClosed 1114 1207 1115 - client, _ := s.auth.AuthorizedClient(r) 1116 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1208 + client, err := s.oauth.AuthorizedClient(r) 1209 + if err != nil { 1210 + log.Println("failed to get authorized client", err) 1211 + return 1212 + } 1213 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1117 1214 Collection: tangled.RepoIssueStateNSID, 1118 1215 Repo: user.Did, 1119 - Rkey: s.TID(), 1216 + Rkey: appview.TID(), 1120 1217 Record: &lexutil.LexiconTypeDecoder{ 1121 1218 Val: &tangled.RepoIssueState{ 1122 1219 Issue: issue.IssueAt, 1123 - State: &closed, 1220 + State: closed, 1124 1221 }, 1125 1222 }, 1126 1223 }) ··· 1131 1228 return 1132 1229 } 1133 1230 1134 - err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1231 + err = db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1135 1232 if err != nil { 1136 1233 log.Println("failed to close issue", err) 1137 1234 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 1148 1245 } 1149 1246 1150 1247 func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1151 - user := s.auth.GetUser(r) 1152 - f, err := fullyResolvedRepo(r) 1248 + user := s.oauth.GetUser(r) 1249 + f, err := s.fullyResolvedRepo(r) 1153 1250 if err != nil { 1154 1251 log.Println("failed to get repo and knot", err) 1155 1252 return ··· 1196 1293 } 1197 1294 1198 1295 func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1199 - user := s.auth.GetUser(r) 1200 - f, err := fullyResolvedRepo(r) 1296 + user := s.oauth.GetUser(r) 1297 + f, err := s.fullyResolvedRepo(r) 1201 1298 if err != nil { 1202 1299 log.Println("failed to get repo and knot", err) 1203 1300 return ··· 1220 1317 } 1221 1318 1222 1319 commentId := mathrand.IntN(1000000) 1223 - rkey := s.TID() 1320 + rkey := appview.TID() 1224 1321 1225 1322 err := db.NewIssueComment(s.db, &db.Comment{ 1226 1323 OwnerDid: user.Did, ··· 1247 1344 } 1248 1345 1249 1346 atUri := f.RepoAt.String() 1250 - client, _ := s.auth.AuthorizedClient(r) 1251 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1347 + client, err := s.oauth.AuthorizedClient(r) 1348 + if err != nil { 1349 + log.Println("failed to get authorized client", err) 1350 + s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1351 + return 1352 + } 1353 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1252 1354 Collection: tangled.RepoIssueCommentNSID, 1253 1355 Repo: user.Did, 1254 1356 Rkey: rkey, ··· 1258 1360 Issue: issueAt, 1259 1361 CommentId: &commentIdInt64, 1260 1362 Owner: &ownerDid, 1261 - Body: &body, 1262 - CreatedAt: &createdAt, 1363 + Body: body, 1364 + CreatedAt: createdAt, 1263 1365 }, 1264 1366 }, 1265 1367 }) ··· 1275 1377 } 1276 1378 1277 1379 func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1278 - user := s.auth.GetUser(r) 1279 - f, err := fullyResolvedRepo(r) 1380 + user := s.oauth.GetUser(r) 1381 + f, err := s.fullyResolvedRepo(r) 1280 1382 if err != nil { 1281 1383 log.Println("failed to get repo and knot", err) 1282 1384 return ··· 1334 1436 } 1335 1437 1336 1438 func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1337 - user := s.auth.GetUser(r) 1338 - f, err := fullyResolvedRepo(r) 1439 + user := s.oauth.GetUser(r) 1440 + f, err := s.fullyResolvedRepo(r) 1339 1441 if err != nil { 1340 1442 log.Println("failed to get repo and knot", err) 1341 1443 return ··· 1386 1488 case http.MethodPost: 1387 1489 // extract form value 1388 1490 newBody := r.FormValue("body") 1389 - client, _ := s.auth.AuthorizedClient(r) 1491 + client, err := s.oauth.AuthorizedClient(r) 1492 + if err != nil { 1493 + log.Println("failed to get authorized client", err) 1494 + s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1495 + return 1496 + } 1390 1497 rkey := comment.Rkey 1391 1498 1392 1499 // optimistic update ··· 1401 1508 // rkey is optional, it was introduced later 1402 1509 if comment.Rkey != "" { 1403 1510 // update the record on pds 1404 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1511 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1405 1512 if err != nil { 1406 1513 // failed to get record 1407 1514 log.Println(err, rkey) ··· 1416 1523 createdAt := record["createdAt"].(string) 1417 1524 commentIdInt64 := int64(commentIdInt) 1418 1525 1419 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1526 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1420 1527 Collection: tangled.RepoIssueCommentNSID, 1421 1528 Repo: user.Did, 1422 1529 Rkey: rkey, ··· 1427 1534 Issue: issueAt, 1428 1535 CommentId: &commentIdInt64, 1429 1536 Owner: &comment.OwnerDid, 1430 - Body: &newBody, 1431 - CreatedAt: &createdAt, 1537 + Body: newBody, 1538 + CreatedAt: createdAt, 1432 1539 }, 1433 1540 }, 1434 1541 }) ··· 1459 1566 } 1460 1567 1461 1568 func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1462 - user := s.auth.GetUser(r) 1463 - f, err := fullyResolvedRepo(r) 1569 + user := s.oauth.GetUser(r) 1570 + f, err := s.fullyResolvedRepo(r) 1464 1571 if err != nil { 1465 1572 log.Println("failed to get repo and knot", err) 1466 1573 return ··· 1516 1623 1517 1624 // delete from pds 1518 1625 if comment.Rkey != "" { 1519 - client, _ := s.auth.AuthorizedClient(r) 1520 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 1626 + client, err := s.oauth.AuthorizedClient(r) 1627 + if err != nil { 1628 + log.Println("failed to get authorized client", err) 1629 + s.pages.Notice(w, "issue-comment", "Failed to delete comment.") 1630 + return 1631 + } 1632 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 1521 1633 Collection: tangled.GraphFollowNSID, 1522 1634 Repo: user.Did, 1523 1635 Rkey: comment.Rkey, ··· 1558 1670 isOpen = true 1559 1671 } 1560 1672 1561 - user := s.auth.GetUser(r) 1562 - f, err := fullyResolvedRepo(r) 1673 + page, ok := r.Context().Value("page").(pagination.Page) 1674 + if !ok { 1675 + log.Println("failed to get page") 1676 + page = pagination.FirstPage() 1677 + } 1678 + 1679 + user := s.oauth.GetUser(r) 1680 + f, err := s.fullyResolvedRepo(r) 1563 1681 if err != nil { 1564 1682 log.Println("failed to get repo and knot", err) 1565 1683 return 1566 1684 } 1567 1685 1568 - issues, err := db.GetIssues(s.db, f.RepoAt, isOpen) 1686 + issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page) 1569 1687 if err != nil { 1570 1688 log.Println("failed to get issues", err) 1571 1689 s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") ··· 1587 1705 } 1588 1706 1589 1707 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1590 - LoggedInUser: s.auth.GetUser(r), 1708 + LoggedInUser: s.oauth.GetUser(r), 1591 1709 RepoInfo: f.RepoInfo(s, user), 1592 1710 Issues: issues, 1593 1711 DidHandleMap: didHandleMap, 1594 1712 FilteringByOpen: isOpen, 1713 + Page: page, 1595 1714 }) 1596 1715 return 1597 1716 } 1598 1717 1599 1718 func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1600 - user := s.auth.GetUser(r) 1719 + user := s.oauth.GetUser(r) 1601 1720 1602 - f, err := fullyResolvedRepo(r) 1721 + f, err := s.fullyResolvedRepo(r) 1603 1722 if err != nil { 1604 1723 log.Println("failed to get repo and knot", err) 1605 1724 return ··· 1645 1764 return 1646 1765 } 1647 1766 1648 - client, _ := s.auth.AuthorizedClient(r) 1767 + client, err := s.oauth.AuthorizedClient(r) 1768 + if err != nil { 1769 + log.Println("failed to get authorized client", err) 1770 + s.pages.Notice(w, "issues", "Failed to create issue.") 1771 + return 1772 + } 1649 1773 atUri := f.RepoAt.String() 1650 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1774 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1651 1775 Collection: tangled.RepoIssueNSID, 1652 1776 Repo: user.Did, 1653 - Rkey: s.TID(), 1777 + Rkey: appview.TID(), 1654 1778 Record: &lexutil.LexiconTypeDecoder{ 1655 1779 Val: &tangled.RepoIssue{ 1656 1780 Repo: atUri, ··· 1680 1804 } 1681 1805 1682 1806 func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1683 - user := s.auth.GetUser(r) 1684 - f, err := fullyResolvedRepo(r) 1807 + user := s.oauth.GetUser(r) 1808 + f, err := s.fullyResolvedRepo(r) 1685 1809 if err != nil { 1686 1810 log.Printf("failed to resolve source repo: %v", err) 1687 1811 return ··· 1689 1813 1690 1814 switch r.Method { 1691 1815 case http.MethodGet: 1692 - user := s.auth.GetUser(r) 1816 + user := s.oauth.GetUser(r) 1693 1817 knots, err := s.enforcer.GetDomainsForUser(user.Did) 1694 1818 if err != nil { 1695 1819 s.pages.Notice(w, "repo", "Invalid user account.") ··· 1739 1863 return 1740 1864 } 1741 1865 1742 - client, err := NewSignedClient(knot, secret, s.config.Dev) 1866 + client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev) 1743 1867 if err != nil { 1744 1868 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1745 1869 return 1746 1870 } 1747 1871 1748 1872 var uri string 1749 - if s.config.Dev { 1873 + if s.config.Core.Dev { 1750 1874 uri = "http" 1751 1875 } else { 1752 1876 uri = "https" 1753 1877 } 1754 - sourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1878 + forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName) 1755 1879 sourceAt := f.RepoAt.String() 1756 1880 1757 - rkey := s.TID() 1881 + rkey := appview.TID() 1758 1882 repo := &db.Repo{ 1759 1883 Did: user.Did, 1760 1884 Name: forkName, ··· 1777 1901 } 1778 1902 }() 1779 1903 1780 - resp, err := client.ForkRepo(user.Did, sourceUrl, forkName) 1904 + resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 1781 1905 if err != nil { 1782 1906 s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1783 1907 return ··· 1793 1917 // continue 1794 1918 } 1795 1919 1796 - xrpcClient, _ := s.auth.AuthorizedClient(r) 1920 + xrpcClient, err := s.oauth.AuthorizedClient(r) 1921 + if err != nil { 1922 + log.Println("failed to get authorized client", err) 1923 + s.pages.Notice(w, "repo", "Failed to create repository.") 1924 + return 1925 + } 1797 1926 1798 - addedAt := time.Now().Format(time.RFC3339) 1799 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 1927 + createdAt := time.Now().Format(time.RFC3339) 1928 + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1800 1929 Collection: tangled.RepoNSID, 1801 1930 Repo: user.Did, 1802 1931 Rkey: rkey, 1803 1932 Record: &lexutil.LexiconTypeDecoder{ 1804 1933 Val: &tangled.Repo{ 1805 - Knot: repo.Knot, 1806 - Name: repo.Name, 1807 - AddedAt: &addedAt, 1808 - Owner: user.Did, 1809 - Source: &sourceAt, 1934 + Knot: repo.Knot, 1935 + Name: repo.Name, 1936 + CreatedAt: createdAt, 1937 + Owner: user.Did, 1938 + Source: &sourceAt, 1810 1939 }}, 1811 1940 }) 1812 1941 if err != nil {
+53 -7
appview/state/repo_util.go
··· 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 "github.com/go-chi/chi/v5" 14 14 "github.com/go-git/go-git/v5/plumbing/object" 15 - "tangled.sh/tangled.sh/core/appview/auth" 16 15 "tangled.sh/tangled.sh/core/appview/db" 17 - "tangled.sh/tangled.sh/core/appview/pages" 16 + "tangled.sh/tangled.sh/core/appview/knotclient" 17 + "tangled.sh/tangled.sh/core/appview/oauth" 18 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 18 19 ) 19 20 20 - func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 21 + func (s *State) fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) { 21 22 repoName := chi.URLParam(r, "repo") 22 23 knot, ok := r.Context().Value("knot").(string) 23 24 if !ok { ··· 42 43 return nil, fmt.Errorf("malformed middleware") 43 44 } 44 45 46 + ref := chi.URLParam(r, "ref") 47 + 48 + if ref == "" { 49 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 50 + if err != nil { 51 + return nil, err 52 + } 53 + 54 + defaultBranch, err := us.DefaultBranch(id.DID.String(), repoName) 55 + if err != nil { 56 + return nil, err 57 + } 58 + 59 + ref = defaultBranch.Branch 60 + } 61 + 45 62 // pass through values from the middleware 46 63 description, ok := r.Context().Value("repoDescription").(string) 47 64 addedAt, ok := r.Context().Value("repoAddedAt").(string) ··· 52 69 RepoName: repoName, 53 70 RepoAt: parsedRepoAt, 54 71 Description: description, 55 - AddedAt: addedAt, 72 + CreatedAt: addedAt, 73 + Ref: ref, 56 74 }, nil 57 75 } 58 76 59 - func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo { 77 + func RolesInRepo(s *State, u *oauth.User, f *FullyResolvedRepo) repoinfo.RolesInRepo { 60 78 if u != nil { 61 79 r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo()) 62 - return pages.RolesInRepo{r} 80 + return repoinfo.RolesInRepo{r} 63 81 } else { 64 - return pages.RolesInRepo{} 82 + return repoinfo.RolesInRepo{} 65 83 } 66 84 } 67 85 ··· 80 98 uniqueEmails = append(uniqueEmails, email) 81 99 } 82 100 return uniqueEmails 101 + } 102 + 103 + func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) { 104 + if commitCount == 0 && tagCount == 0 && branchCount == 0 { 105 + return 106 + } 107 + 108 + // typically 1 item on right side = 2 files in height 109 + availableSpace := fileCount / 2 110 + 111 + // clamp tagcount 112 + if tagCount > 0 { 113 + tagsTrunc = 1 114 + availableSpace -= 1 // an extra subtracted for headers etc. 115 + } 116 + 117 + // clamp branchcount 118 + if branchCount > 0 { 119 + branchesTrunc = min(max(branchCount, 1), 2) 120 + availableSpace -= branchesTrunc // an extra subtracted for headers etc. 121 + } 122 + 123 + // show 124 + if commitCount > 0 { 125 + commitsTrunc = max(availableSpace, 3) 126 + } 127 + 128 + return 83 129 } 84 130 85 131 func EmailToDidOrHandle(s *State, emails []string) map[string]string {
+72 -30
appview/state/router.go
··· 5 5 "strings" 6 6 7 7 "github.com/go-chi/chi/v5" 8 + "github.com/gorilla/sessions" 9 + "tangled.sh/tangled.sh/core/appview/middleware" 10 + oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" 11 + "tangled.sh/tangled.sh/core/appview/settings" 8 12 "tangled.sh/tangled.sh/core/appview/state/userutil" 9 13 ) 10 14 ··· 51 55 r.Use(StripLeadingAt) 52 56 53 57 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 54 - r.Get("/", s.ProfilePage) 58 + r.Get("/", s.Profile) 59 + 55 60 r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) { 61 + r.Use(GoImport(s)) 62 + 56 63 r.Get("/", s.RepoIndex) 57 64 r.Get("/commits/{ref}", s.RepoLog) 58 65 r.Route("/tree/{ref}", func(r chi.Router) { ··· 61 68 }) 62 69 r.Get("/commit/{ref}", s.RepoCommit) 63 70 r.Get("/branches", s.RepoBranches) 64 - r.Get("/tags", s.RepoTags) 71 + r.Route("/tags", func(r chi.Router) { 72 + r.Get("/", s.RepoTags) 73 + r.Route("/{tag}", func(r chi.Router) { 74 + r.Use(middleware.AuthMiddleware(s.oauth)) 75 + // require auth to download for now 76 + r.Get("/download/{file}", s.DownloadArtifact) 77 + 78 + // require repo:push to upload or delete artifacts 79 + // 80 + // additionally: only the uploader can truly delete an artifact 81 + // (record+blob will live on their pds) 82 + r.Group(func(r chi.Router) { 83 + r.With(RepoPermissionMiddleware(s, "repo:push")) 84 + r.Post("/upload", s.AttachArtifact) 85 + r.Delete("/{file}", s.DeleteArtifact) 86 + }) 87 + }) 88 + }) 65 89 r.Get("/blob/{ref}/*", s.RepoBlob) 66 - r.Get("/blob/{ref}/raw/*", s.RepoBlobRaw) 90 + r.Get("/raw/{ref}/*", s.RepoBlobRaw) 67 91 68 92 r.Route("/issues", func(r chi.Router) { 69 - r.Get("/", s.RepoIssues) 93 + r.With(middleware.Paginate).Get("/", s.RepoIssues) 70 94 r.Get("/{issue}", s.RepoSingleIssue) 71 95 72 96 r.Group(func(r chi.Router) { 73 - r.Use(AuthMiddleware(s)) 97 + r.Use(middleware.AuthMiddleware(s.oauth)) 74 98 r.Get("/new", s.NewIssue) 75 99 r.Post("/new", s.NewIssue) 76 100 r.Post("/{issue}/comment", s.NewIssueComment) ··· 86 110 }) 87 111 88 112 r.Route("/fork", func(r chi.Router) { 89 - r.Use(AuthMiddleware(s)) 113 + r.Use(middleware.AuthMiddleware(s.oauth)) 90 114 r.Get("/", s.ForkRepo) 91 115 r.Post("/", s.ForkRepo) 92 116 }) 93 117 94 118 r.Route("/pulls", func(r chi.Router) { 95 119 r.Get("/", s.RepoPulls) 96 - r.With(AuthMiddleware(s)).Route("/new", func(r chi.Router) { 120 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) { 97 121 r.Get("/", s.NewPull) 98 122 r.Get("/patch-upload", s.PatchUploadFragment) 99 123 r.Post("/validate-patch", s.ValidatePatch) ··· 111 135 r.Get("/", s.RepoPullPatch) 112 136 r.Get("/interdiff", s.RepoPullInterdiff) 113 137 r.Get("/actions", s.PullActions) 114 - r.With(AuthMiddleware(s)).Route("/comment", func(r chi.Router) { 138 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 115 139 r.Get("/", s.PullComment) 116 140 r.Post("/", s.PullComment) 117 141 }) ··· 122 146 }) 123 147 124 148 r.Group(func(r chi.Router) { 125 - r.Use(AuthMiddleware(s)) 149 + r.Use(middleware.AuthMiddleware(s.oauth)) 126 150 r.Route("/resubmit", func(r chi.Router) { 127 151 r.Get("/", s.ResubmitPull) 128 152 r.Post("/", s.ResubmitPull) ··· 145 169 146 170 // settings routes, needs auth 147 171 r.Group(func(r chi.Router) { 148 - r.Use(AuthMiddleware(s)) 172 + r.Use(middleware.AuthMiddleware(s.oauth)) 149 173 // repo description can only be edited by owner 150 174 r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 151 175 r.Put("/", s.RepoDescription) ··· 176 200 177 201 r.Get("/", s.Timeline) 178 202 179 - r.With(AuthMiddleware(s)).Post("/logout", s.Logout) 180 - 181 - r.Route("/login", func(r chi.Router) { 182 - r.Get("/", s.Login) 183 - r.Post("/", s.Login) 184 - }) 203 + r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout) 185 204 186 205 r.Route("/knots", func(r chi.Router) { 187 - r.Use(AuthMiddleware(s)) 206 + r.Use(middleware.AuthMiddleware(s.oauth)) 188 207 r.Get("/", s.Knots) 189 208 r.Post("/key", s.RegistrationKey) 190 209 ··· 202 221 203 222 r.Route("/repo", func(r chi.Router) { 204 223 r.Route("/new", func(r chi.Router) { 205 - r.Use(AuthMiddleware(s)) 224 + r.Use(middleware.AuthMiddleware(s.oauth)) 206 225 r.Get("/", s.NewRepo) 207 226 r.Post("/", s.NewRepo) 208 227 }) 209 228 // r.Post("/import", s.ImportRepo) 210 229 }) 211 230 212 - r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 231 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { 213 232 r.Post("/", s.Follow) 214 233 r.Delete("/", s.Follow) 215 234 }) 216 235 217 - r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) { 236 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) { 218 237 r.Post("/", s.Star) 219 238 r.Delete("/", s.Star) 220 239 }) 221 240 222 - r.Route("/settings", func(r chi.Router) { 223 - r.Use(AuthMiddleware(s)) 224 - r.Get("/", s.Settings) 225 - r.Put("/keys", s.SettingsKeys) 226 - r.Delete("/keys", s.SettingsKeys) 227 - r.Put("/emails", s.SettingsEmails) 228 - r.Delete("/emails", s.SettingsEmails) 229 - r.Get("/emails/verify", s.SettingsEmailsVerify) 230 - r.Post("/emails/verify/resend", s.SettingsEmailsVerifyResend) 231 - r.Post("/emails/primary", s.SettingsEmailsPrimary) 241 + r.Route("/profile", func(r chi.Router) { 242 + r.Use(middleware.AuthMiddleware(s.oauth)) 243 + r.Get("/edit-bio", s.EditBioFragment) 244 + r.Get("/edit-pins", s.EditPinsFragment) 245 + r.Post("/bio", s.UpdateProfileBio) 246 + r.Post("/pins", s.UpdateProfilePins) 232 247 }) 233 248 249 + r.Mount("/settings", s.SettingsRouter()) 250 + r.Mount("/", s.OAuthRouter()) 234 251 r.Get("/keys/{user}", s.Keys) 235 252 236 253 r.NotFound(func(w http.ResponseWriter, r *http.Request) { ··· 238 255 }) 239 256 return r 240 257 } 258 + 259 + func (s *State) OAuthRouter() http.Handler { 260 + oauth := &oauthhandler.OAuthHandler{ 261 + Config: s.config, 262 + Pages: s.pages, 263 + Resolver: s.resolver, 264 + Db: s.db, 265 + Store: sessions.NewCookieStore([]byte(s.config.Core.CookieSecret)), 266 + OAuth: s.oauth, 267 + Enforcer: s.enforcer, 268 + } 269 + 270 + return oauth.Router() 271 + } 272 + 273 + func (s *State) SettingsRouter() http.Handler { 274 + settings := &settings.Settings{ 275 + Db: s.db, 276 + OAuth: s.oauth, 277 + Pages: s.pages, 278 + Config: s.config, 279 + } 280 + 281 + return settings.Router() 282 + }
-416
appview/state/settings.go
··· 1 - package state 2 - 3 - import ( 4 - "database/sql" 5 - "errors" 6 - "fmt" 7 - "log" 8 - "net/http" 9 - "net/url" 10 - "strings" 11 - "time" 12 - 13 - comatproto "github.com/bluesky-social/indigo/api/atproto" 14 - lexutil "github.com/bluesky-social/indigo/lex/util" 15 - "github.com/gliderlabs/ssh" 16 - "github.com/google/uuid" 17 - "tangled.sh/tangled.sh/core/api/tangled" 18 - "tangled.sh/tangled.sh/core/appview/db" 19 - "tangled.sh/tangled.sh/core/appview/email" 20 - "tangled.sh/tangled.sh/core/appview/pages" 21 - ) 22 - 23 - func (s *State) Settings(w http.ResponseWriter, r *http.Request) { 24 - user := s.auth.GetUser(r) 25 - pubKeys, err := db.GetPublicKeys(s.db, user.Did) 26 - if err != nil { 27 - log.Println(err) 28 - } 29 - 30 - emails, err := db.GetAllEmails(s.db, user.Did) 31 - if err != nil { 32 - log.Println(err) 33 - } 34 - 35 - s.pages.Settings(w, pages.SettingsParams{ 36 - LoggedInUser: user, 37 - PubKeys: pubKeys, 38 - Emails: emails, 39 - }) 40 - } 41 - 42 - // buildVerificationEmail creates an email.Email struct for verification emails 43 - func (s *State) buildVerificationEmail(emailAddr, did, code string) email.Email { 44 - verifyURL := s.verifyUrl(did, emailAddr, code) 45 - 46 - return email.Email{ 47 - APIKey: s.config.ResendApiKey, 48 - From: "noreply@notifs.tangled.sh", 49 - To: emailAddr, 50 - Subject: "Verify your Tangled email", 51 - Text: `Click the link below (or copy and paste it into your browser) to verify your email address. 52 - ` + verifyURL, 53 - Html: `<p>Click the link (or copy and paste it into your browser) to verify your email address.</p> 54 - <p><a href="` + verifyURL + `">` + verifyURL + `</a></p>`, 55 - } 56 - } 57 - 58 - // sendVerificationEmail handles the common logic for sending verification emails 59 - func (s *State) sendVerificationEmail(w http.ResponseWriter, did, emailAddr, code string, errorContext string) error { 60 - emailToSend := s.buildVerificationEmail(emailAddr, did, code) 61 - 62 - err := email.SendEmail(emailToSend) 63 - if err != nil { 64 - log.Printf("sending email: %s", err) 65 - s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Unable to send verification email at this moment, try again later. %s", errorContext)) 66 - return err 67 - } 68 - 69 - return nil 70 - } 71 - 72 - func (s *State) SettingsEmails(w http.ResponseWriter, r *http.Request) { 73 - switch r.Method { 74 - case http.MethodGet: 75 - s.pages.Notice(w, "settings-emails", "Unimplemented.") 76 - log.Println("unimplemented") 77 - return 78 - case http.MethodPut: 79 - did := s.auth.GetDid(r) 80 - emAddr := r.FormValue("email") 81 - emAddr = strings.TrimSpace(emAddr) 82 - 83 - if !email.IsValidEmail(emAddr) { 84 - s.pages.Notice(w, "settings-emails-error", "Invalid email address.") 85 - return 86 - } 87 - 88 - // check if email already exists in database 89 - existingEmail, err := db.GetEmail(s.db, did, emAddr) 90 - if err != nil && !errors.Is(err, sql.ErrNoRows) { 91 - log.Printf("checking for existing email: %s", err) 92 - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 93 - return 94 - } 95 - 96 - if err == nil { 97 - if existingEmail.Verified { 98 - s.pages.Notice(w, "settings-emails-error", "This email is already verified.") 99 - return 100 - } 101 - 102 - s.pages.Notice(w, "settings-emails-error", "This email is already added but not verified. Check your inbox for the verification link.") 103 - return 104 - } 105 - 106 - code := uuid.New().String() 107 - 108 - // Begin transaction 109 - tx, err := s.db.Begin() 110 - if err != nil { 111 - log.Printf("failed to start transaction: %s", err) 112 - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 113 - return 114 - } 115 - defer tx.Rollback() 116 - 117 - if err := db.AddEmail(tx, db.Email{ 118 - Did: did, 119 - Address: emAddr, 120 - Verified: false, 121 - VerificationCode: code, 122 - }); err != nil { 123 - log.Printf("adding email: %s", err) 124 - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 125 - return 126 - } 127 - 128 - if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 129 - return 130 - } 131 - 132 - // Commit transaction 133 - if err := tx.Commit(); err != nil { 134 - log.Printf("failed to commit transaction: %s", err) 135 - s.pages.Notice(w, "settings-emails-error", "Unable to add email at this moment, try again later.") 136 - return 137 - } 138 - 139 - s.pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") 140 - return 141 - case http.MethodDelete: 142 - did := s.auth.GetDid(r) 143 - emailAddr := r.FormValue("email") 144 - emailAddr = strings.TrimSpace(emailAddr) 145 - 146 - // Begin transaction 147 - tx, err := s.db.Begin() 148 - if err != nil { 149 - log.Printf("failed to start transaction: %s", err) 150 - s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 151 - return 152 - } 153 - defer tx.Rollback() 154 - 155 - if err := db.DeleteEmail(tx, did, emailAddr); err != nil { 156 - log.Printf("deleting email: %s", err) 157 - s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 158 - return 159 - } 160 - 161 - // Commit transaction 162 - if err := tx.Commit(); err != nil { 163 - log.Printf("failed to commit transaction: %s", err) 164 - s.pages.Notice(w, "settings-emails-error", "Unable to delete email at this moment, try again later.") 165 - return 166 - } 167 - 168 - s.pages.HxLocation(w, "/settings") 169 - return 170 - } 171 - } 172 - 173 - func (s *State) verifyUrl(did string, email string, code string) string { 174 - var appUrl string 175 - if s.config.Dev { 176 - appUrl = "http://" + s.config.ListenAddr 177 - } else { 178 - appUrl = "https://tangled.sh" 179 - } 180 - 181 - return fmt.Sprintf("%s/settings/emails/verify?did=%s&email=%s&code=%s", appUrl, url.QueryEscape(did), url.QueryEscape(email), url.QueryEscape(code)) 182 - } 183 - 184 - func (s *State) SettingsEmailsVerify(w http.ResponseWriter, r *http.Request) { 185 - q := r.URL.Query() 186 - 187 - // Get the parameters directly from the query 188 - emailAddr := q.Get("email") 189 - did := q.Get("did") 190 - code := q.Get("code") 191 - 192 - valid, err := db.CheckValidVerificationCode(s.db, did, emailAddr, code) 193 - if err != nil { 194 - log.Printf("checking email verification: %s", err) 195 - s.pages.Notice(w, "settings-emails-error", "Error verifying email. Please try again later.") 196 - return 197 - } 198 - 199 - if !valid { 200 - s.pages.Notice(w, "settings-emails-error", "Invalid verification code. Please request a new verification email.") 201 - return 202 - } 203 - 204 - // Mark email as verified in the database 205 - if err := db.MarkEmailVerified(s.db, did, emailAddr); err != nil { 206 - log.Printf("marking email as verified: %s", err) 207 - s.pages.Notice(w, "settings-emails-error", "Error updating email verification status. Please try again later.") 208 - return 209 - } 210 - 211 - http.Redirect(w, r, "/settings", http.StatusSeeOther) 212 - } 213 - 214 - func (s *State) SettingsEmailsVerifyResend(w http.ResponseWriter, r *http.Request) { 215 - if r.Method != http.MethodPost { 216 - s.pages.Notice(w, "settings-emails-error", "Invalid request method.") 217 - return 218 - } 219 - 220 - did := s.auth.GetDid(r) 221 - emAddr := r.FormValue("email") 222 - emAddr = strings.TrimSpace(emAddr) 223 - 224 - if !email.IsValidEmail(emAddr) { 225 - s.pages.Notice(w, "settings-emails-error", "Invalid email address.") 226 - return 227 - } 228 - 229 - // Check if email exists and is unverified 230 - existingEmail, err := db.GetEmail(s.db, did, emAddr) 231 - if err != nil { 232 - if errors.Is(err, sql.ErrNoRows) { 233 - s.pages.Notice(w, "settings-emails-error", "Email not found. Please add it first.") 234 - } else { 235 - log.Printf("checking for existing email: %s", err) 236 - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 237 - } 238 - return 239 - } 240 - 241 - if existingEmail.Verified { 242 - s.pages.Notice(w, "settings-emails-error", "This email is already verified.") 243 - return 244 - } 245 - 246 - // Check if last verification email was sent less than 10 minutes ago 247 - if existingEmail.LastSent != nil { 248 - timeSinceLastSent := time.Since(*existingEmail.LastSent) 249 - if timeSinceLastSent < 10*time.Minute { 250 - waitTime := 10*time.Minute - timeSinceLastSent 251 - s.pages.Notice(w, "settings-emails-error", fmt.Sprintf("Please wait %d minutes before requesting another verification email.", int(waitTime.Minutes()+1))) 252 - return 253 - } 254 - } 255 - 256 - // Generate new verification code 257 - code := uuid.New().String() 258 - 259 - // Begin transaction 260 - tx, err := s.db.Begin() 261 - if err != nil { 262 - log.Printf("failed to start transaction: %s", err) 263 - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 264 - return 265 - } 266 - defer tx.Rollback() 267 - 268 - // Update the verification code and last sent time 269 - if err := db.UpdateVerificationCode(tx, did, emAddr, code); err != nil { 270 - log.Printf("updating email verification: %s", err) 271 - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 272 - return 273 - } 274 - 275 - // Send verification email 276 - if err := s.sendVerificationEmail(w, did, emAddr, code, ""); err != nil { 277 - return 278 - } 279 - 280 - // Commit transaction 281 - if err := tx.Commit(); err != nil { 282 - log.Printf("failed to commit transaction: %s", err) 283 - s.pages.Notice(w, "settings-emails-error", "Unable to resend verification email at this moment, try again later.") 284 - return 285 - } 286 - 287 - s.pages.Notice(w, "settings-emails-success", "Verification email resent. Click the link in the email we sent you to verify your email address.") 288 - } 289 - 290 - func (s *State) SettingsEmailsPrimary(w http.ResponseWriter, r *http.Request) { 291 - did := s.auth.GetDid(r) 292 - emailAddr := r.FormValue("email") 293 - emailAddr = strings.TrimSpace(emailAddr) 294 - 295 - if emailAddr == "" { 296 - s.pages.Notice(w, "settings-emails-error", "Email address cannot be empty.") 297 - return 298 - } 299 - 300 - if err := db.MakeEmailPrimary(s.db, did, emailAddr); err != nil { 301 - log.Printf("setting primary email: %s", err) 302 - s.pages.Notice(w, "settings-emails-error", "Error setting primary email. Please try again later.") 303 - return 304 - } 305 - 306 - s.pages.HxLocation(w, "/settings") 307 - } 308 - 309 - func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { 310 - switch r.Method { 311 - case http.MethodGet: 312 - s.pages.Notice(w, "settings-keys", "Unimplemented.") 313 - log.Println("unimplemented") 314 - return 315 - case http.MethodPut: 316 - did := s.auth.GetDid(r) 317 - key := r.FormValue("key") 318 - key = strings.TrimSpace(key) 319 - name := r.FormValue("name") 320 - client, _ := s.auth.AuthorizedClient(r) 321 - 322 - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 323 - if err != nil { 324 - log.Printf("parsing public key: %s", err) 325 - s.pages.Notice(w, "settings-keys", "That doesn't look like a valid public key. Make sure it's a <strong>public</strong> key.") 326 - return 327 - } 328 - 329 - rkey := s.TID() 330 - 331 - tx, err := s.db.Begin() 332 - if err != nil { 333 - log.Printf("failed to start tx; adding public key: %s", err) 334 - s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") 335 - return 336 - } 337 - defer tx.Rollback() 338 - 339 - if err := db.AddPublicKey(tx, did, name, key, rkey); err != nil { 340 - log.Printf("adding public key: %s", err) 341 - s.pages.Notice(w, "settings-keys", "Failed to add public key.") 342 - return 343 - } 344 - 345 - // store in pds too 346 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 347 - Collection: tangled.PublicKeyNSID, 348 - Repo: did, 349 - Rkey: rkey, 350 - Record: &lexutil.LexiconTypeDecoder{ 351 - Val: &tangled.PublicKey{ 352 - Created: time.Now().Format(time.RFC3339), 353 - Key: key, 354 - Name: name, 355 - }}, 356 - }) 357 - // invalid record 358 - if err != nil { 359 - log.Printf("failed to create record: %s", err) 360 - s.pages.Notice(w, "settings-keys", "Failed to create record.") 361 - return 362 - } 363 - 364 - log.Println("created atproto record: ", resp.Uri) 365 - 366 - err = tx.Commit() 367 - if err != nil { 368 - log.Printf("failed to commit tx; adding public key: %s", err) 369 - s.pages.Notice(w, "settings-keys", "Unable to add public key at this moment, try again later.") 370 - return 371 - } 372 - 373 - s.pages.HxLocation(w, "/settings") 374 - return 375 - 376 - case http.MethodDelete: 377 - did := s.auth.GetDid(r) 378 - q := r.URL.Query() 379 - 380 - name := q.Get("name") 381 - rkey := q.Get("rkey") 382 - key := q.Get("key") 383 - 384 - log.Println(name) 385 - log.Println(rkey) 386 - log.Println(key) 387 - 388 - client, _ := s.auth.AuthorizedClient(r) 389 - 390 - if err := db.RemovePublicKey(s.db, did, name, key); err != nil { 391 - log.Printf("removing public key: %s", err) 392 - s.pages.Notice(w, "settings-keys", "Failed to remove public key.") 393 - return 394 - } 395 - 396 - if rkey != "" { 397 - // remove from pds too 398 - _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 399 - Collection: tangled.PublicKeyNSID, 400 - Repo: did, 401 - Rkey: rkey, 402 - }) 403 - 404 - // invalid record 405 - if err != nil { 406 - log.Printf("failed to delete record from PDS: %s", err) 407 - s.pages.Notice(w, "settings-keys", "Failed to remove key from PDS.") 408 - return 409 - } 410 - } 411 - log.Println("deleted successfully") 412 - 413 - s.pages.HxLocation(w, "/settings") 414 - return 415 - } 416 - }
-420
appview/state/signer.go
··· 1 - package state 2 - 3 - import ( 4 - "bytes" 5 - "crypto/hmac" 6 - "crypto/sha256" 7 - "encoding/hex" 8 - "encoding/json" 9 - "fmt" 10 - "io" 11 - "log" 12 - "net/http" 13 - "net/url" 14 - "time" 15 - 16 - "tangled.sh/tangled.sh/core/types" 17 - ) 18 - 19 - type SignerTransport struct { 20 - Secret string 21 - } 22 - 23 - func (s SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) { 24 - timestamp := time.Now().Format(time.RFC3339) 25 - mac := hmac.New(sha256.New, []byte(s.Secret)) 26 - message := req.Method + req.URL.Path + timestamp 27 - mac.Write([]byte(message)) 28 - signature := hex.EncodeToString(mac.Sum(nil)) 29 - req.Header.Set("X-Signature", signature) 30 - req.Header.Set("X-Timestamp", timestamp) 31 - return http.DefaultTransport.RoundTrip(req) 32 - } 33 - 34 - type SignedClient struct { 35 - Secret string 36 - Url *url.URL 37 - client *http.Client 38 - } 39 - 40 - func NewSignedClient(domain, secret string, dev bool) (*SignedClient, error) { 41 - client := &http.Client{ 42 - Timeout: 5 * time.Second, 43 - Transport: SignerTransport{ 44 - Secret: secret, 45 - }, 46 - } 47 - 48 - scheme := "https" 49 - if dev { 50 - scheme = "http" 51 - } 52 - url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 53 - if err != nil { 54 - return nil, err 55 - } 56 - 57 - signedClient := &SignedClient{ 58 - Secret: secret, 59 - client: client, 60 - Url: url, 61 - } 62 - 63 - return signedClient, nil 64 - } 65 - 66 - func (s *SignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) { 67 - return http.NewRequest(method, s.Url.JoinPath(endpoint).String(), bytes.NewReader(body)) 68 - } 69 - 70 - func (s *SignedClient) Init(did string) (*http.Response, error) { 71 - const ( 72 - Method = "POST" 73 - Endpoint = "/init" 74 - ) 75 - 76 - body, _ := json.Marshal(map[string]any{ 77 - "did": did, 78 - }) 79 - 80 - req, err := s.newRequest(Method, Endpoint, body) 81 - if err != nil { 82 - return nil, err 83 - } 84 - 85 - return s.client.Do(req) 86 - } 87 - 88 - func (s *SignedClient) NewRepo(did, repoName, defaultBranch string) (*http.Response, error) { 89 - const ( 90 - Method = "PUT" 91 - Endpoint = "/repo/new" 92 - ) 93 - 94 - body, _ := json.Marshal(map[string]any{ 95 - "did": did, 96 - "name": repoName, 97 - "default_branch": defaultBranch, 98 - }) 99 - 100 - req, err := s.newRequest(Method, Endpoint, body) 101 - if err != nil { 102 - return nil, err 103 - } 104 - 105 - return s.client.Do(req) 106 - } 107 - 108 - func (s *SignedClient) ForkRepo(ownerDid, source, name string) (*http.Response, error) { 109 - const ( 110 - Method = "POST" 111 - Endpoint = "/repo/fork" 112 - ) 113 - 114 - body, _ := json.Marshal(map[string]any{ 115 - "did": ownerDid, 116 - "source": source, 117 - "name": name, 118 - }) 119 - 120 - req, err := s.newRequest(Method, Endpoint, body) 121 - if err != nil { 122 - return nil, err 123 - } 124 - 125 - return s.client.Do(req) 126 - } 127 - 128 - func (s *SignedClient) RemoveRepo(did, repoName string) (*http.Response, error) { 129 - const ( 130 - Method = "DELETE" 131 - Endpoint = "/repo" 132 - ) 133 - 134 - body, _ := json.Marshal(map[string]any{ 135 - "did": did, 136 - "name": repoName, 137 - }) 138 - 139 - req, err := s.newRequest(Method, Endpoint, body) 140 - if err != nil { 141 - return nil, err 142 - } 143 - 144 - return s.client.Do(req) 145 - } 146 - 147 - func (s *SignedClient) AddMember(did string) (*http.Response, error) { 148 - const ( 149 - Method = "PUT" 150 - Endpoint = "/member/add" 151 - ) 152 - 153 - body, _ := json.Marshal(map[string]any{ 154 - "did": did, 155 - }) 156 - 157 - req, err := s.newRequest(Method, Endpoint, body) 158 - if err != nil { 159 - return nil, err 160 - } 161 - 162 - return s.client.Do(req) 163 - } 164 - 165 - func (s *SignedClient) SetDefaultBranch(ownerDid, repoName, branch string) (*http.Response, error) { 166 - const ( 167 - Method = "PUT" 168 - ) 169 - endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 170 - 171 - body, _ := json.Marshal(map[string]any{ 172 - "branch": branch, 173 - }) 174 - 175 - req, err := s.newRequest(Method, endpoint, body) 176 - if err != nil { 177 - return nil, err 178 - } 179 - 180 - return s.client.Do(req) 181 - } 182 - 183 - func (s *SignedClient) AddCollaborator(ownerDid, repoName, memberDid string) (*http.Response, error) { 184 - const ( 185 - Method = "POST" 186 - ) 187 - endpoint := fmt.Sprintf("/%s/%s/collaborator/add", ownerDid, repoName) 188 - 189 - body, _ := json.Marshal(map[string]any{ 190 - "did": memberDid, 191 - }) 192 - 193 - req, err := s.newRequest(Method, endpoint, body) 194 - if err != nil { 195 - return nil, err 196 - } 197 - 198 - return s.client.Do(req) 199 - } 200 - 201 - func (s *SignedClient) Merge( 202 - patch []byte, 203 - ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 204 - ) (*http.Response, error) { 205 - const ( 206 - Method = "POST" 207 - ) 208 - endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 209 - 210 - mr := types.MergeRequest{ 211 - Branch: branch, 212 - CommitMessage: commitMessage, 213 - CommitBody: commitBody, 214 - AuthorName: authorName, 215 - AuthorEmail: authorEmail, 216 - Patch: string(patch), 217 - } 218 - 219 - body, _ := json.Marshal(mr) 220 - 221 - req, err := s.newRequest(Method, endpoint, body) 222 - if err != nil { 223 - return nil, err 224 - } 225 - 226 - return s.client.Do(req) 227 - } 228 - 229 - func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 230 - const ( 231 - Method = "POST" 232 - ) 233 - endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo) 234 - 235 - body, _ := json.Marshal(map[string]any{ 236 - "patch": string(patch), 237 - "branch": branch, 238 - }) 239 - 240 - req, err := s.newRequest(Method, endpoint, body) 241 - if err != nil { 242 - return nil, err 243 - } 244 - 245 - return s.client.Do(req) 246 - } 247 - 248 - func (s *SignedClient) NewHiddenRef(ownerDid, targetRepo, forkBranch, remoteBranch string) (*http.Response, error) { 249 - const ( 250 - Method = "POST" 251 - ) 252 - endpoint := fmt.Sprintf("/%s/%s/hidden-ref/%s/%s", ownerDid, targetRepo, forkBranch, remoteBranch) 253 - 254 - req, err := s.newRequest(Method, endpoint, nil) 255 - if err != nil { 256 - return nil, err 257 - } 258 - 259 - return s.client.Do(req) 260 - } 261 - 262 - type UnsignedClient struct { 263 - Url *url.URL 264 - client *http.Client 265 - } 266 - 267 - func NewUnsignedClient(domain string, dev bool) (*UnsignedClient, error) { 268 - client := &http.Client{ 269 - Timeout: 5 * time.Second, 270 - } 271 - 272 - scheme := "https" 273 - if dev { 274 - scheme = "http" 275 - } 276 - url, err := url.Parse(fmt.Sprintf("%s://%s", scheme, domain)) 277 - if err != nil { 278 - return nil, err 279 - } 280 - 281 - unsignedClient := &UnsignedClient{ 282 - client: client, 283 - Url: url, 284 - } 285 - 286 - return unsignedClient, nil 287 - } 288 - 289 - func (us *UnsignedClient) newRequest(method, endpoint string, body []byte) (*http.Request, error) { 290 - return http.NewRequest(method, us.Url.JoinPath(endpoint).String(), bytes.NewReader(body)) 291 - } 292 - 293 - func (us *UnsignedClient) Index(ownerDid, repoName, ref string) (*http.Response, error) { 294 - const ( 295 - Method = "GET" 296 - ) 297 - 298 - endpoint := fmt.Sprintf("/%s/%s/tree/%s", ownerDid, repoName, ref) 299 - if ref == "" { 300 - endpoint = fmt.Sprintf("/%s/%s", ownerDid, repoName) 301 - } 302 - 303 - req, err := us.newRequest(Method, endpoint, nil) 304 - if err != nil { 305 - return nil, err 306 - } 307 - 308 - return us.client.Do(req) 309 - } 310 - 311 - func (us *UnsignedClient) Branches(ownerDid, repoName string) (*http.Response, error) { 312 - const ( 313 - Method = "GET" 314 - ) 315 - 316 - endpoint := fmt.Sprintf("/%s/%s/branches", ownerDid, repoName) 317 - 318 - req, err := us.newRequest(Method, endpoint, nil) 319 - if err != nil { 320 - return nil, err 321 - } 322 - 323 - return us.client.Do(req) 324 - } 325 - 326 - func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) { 327 - const ( 328 - Method = "GET" 329 - ) 330 - 331 - endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, branch) 332 - 333 - req, err := us.newRequest(Method, endpoint, nil) 334 - if err != nil { 335 - return nil, err 336 - } 337 - 338 - return us.client.Do(req) 339 - } 340 - 341 - func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*http.Response, error) { 342 - const ( 343 - Method = "GET" 344 - ) 345 - 346 - endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 347 - 348 - req, err := us.newRequest(Method, endpoint, nil) 349 - if err != nil { 350 - return nil, err 351 - } 352 - 353 - return us.client.Do(req) 354 - } 355 - 356 - func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) { 357 - const ( 358 - Method = "GET" 359 - Endpoint = "/capabilities" 360 - ) 361 - 362 - req, err := us.newRequest(Method, Endpoint, nil) 363 - if err != nil { 364 - return nil, err 365 - } 366 - 367 - resp, err := us.client.Do(req) 368 - if err != nil { 369 - return nil, err 370 - } 371 - defer resp.Body.Close() 372 - 373 - var capabilities types.Capabilities 374 - if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil { 375 - return nil, err 376 - } 377 - 378 - return &capabilities, nil 379 - } 380 - 381 - func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) { 382 - const ( 383 - Method = "GET" 384 - ) 385 - 386 - endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2)) 387 - 388 - req, err := us.newRequest(Method, endpoint, nil) 389 - if err != nil { 390 - return nil, fmt.Errorf("Failed to create request.") 391 - } 392 - 393 - compareResp, err := us.client.Do(req) 394 - if err != nil { 395 - return nil, fmt.Errorf("Failed to create request.") 396 - } 397 - defer compareResp.Body.Close() 398 - 399 - switch compareResp.StatusCode { 400 - case 404: 401 - case 400: 402 - return nil, fmt.Errorf("Branch comparisons not supported on this knot.") 403 - } 404 - 405 - respBody, err := io.ReadAll(compareResp.Body) 406 - if err != nil { 407 - log.Println("failed to compare across branches") 408 - return nil, fmt.Errorf("Failed to compare branches.") 409 - } 410 - defer compareResp.Body.Close() 411 - 412 - var formatPatchResponse types.RepoFormatPatchResponse 413 - err = json.Unmarshal(respBody, &formatPatchResponse) 414 - if err != nil { 415 - log.Println("failed to unmarshal format-patch response", err) 416 - return nil, fmt.Errorf("failed to compare branches.") 417 - } 418 - 419 - return &formatPatchResponse, nil 420 - }
+13 -7
appview/state/star.go
··· 8 8 comatproto "github.com/bluesky-social/indigo/api/atproto" 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 lexutil "github.com/bluesky-social/indigo/lex/util" 11 - tangled "tangled.sh/tangled.sh/core/api/tangled" 11 + "tangled.sh/tangled.sh/core/api/tangled" 12 + "tangled.sh/tangled.sh/core/appview" 12 13 "tangled.sh/tangled.sh/core/appview/db" 13 14 "tangled.sh/tangled.sh/core/appview/pages" 14 15 ) 15 16 16 17 func (s *State) Star(w http.ResponseWriter, r *http.Request) { 17 - currentUser := s.auth.GetUser(r) 18 + currentUser := s.oauth.GetUser(r) 18 19 19 20 subject := r.URL.Query().Get("subject") 20 21 if subject == "" { ··· 28 29 return 29 30 } 30 31 31 - client, _ := s.auth.AuthorizedClient(r) 32 + client, err := s.oauth.AuthorizedClient(r) 33 + if err != nil { 34 + log.Println("failed to authorize client", err) 35 + return 36 + } 32 37 33 38 switch r.Method { 34 39 case http.MethodPost: 35 40 createdAt := time.Now().Format(time.RFC3339) 36 - rkey := s.TID() 37 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 41 + rkey := appview.TID() 42 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 38 43 Collection: tangled.FeedStarNSID, 39 44 Repo: currentUser.Did, 40 45 Rkey: rkey, ··· 79 84 return 80 85 } 81 86 82 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 87 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 83 88 Collection: tangled.FeedStarNSID, 84 89 Repo: currentUser.Did, 85 90 Rkey: star.Rkey, ··· 90 95 return 91 96 } 92 97 93 - err = db.DeleteStar(s.db, currentUser.Did, subjectUri) 98 + err = db.DeleteStarByRkey(s.db, currentUser.Did, star.Rkey) 94 99 if err != nil { 95 100 log.Println("failed to delete star from DB") 96 101 // this is not an issue, the firehose event might have already done this ··· 99 104 starCount, err := db.GetStarCount(s.db, subjectUri) 100 105 if err != nil { 101 106 log.Println("failed to get star count for ", subjectUri) 107 + return 102 108 } 103 109 104 110 s.pages.RepoActionsFragment(w, pages.RepoActionsFragmentParams{
+163 -130
appview/state/state.go
··· 17 17 lexutil "github.com/bluesky-social/indigo/lex/util" 18 18 securejoin "github.com/cyphar/filepath-securejoin" 19 19 "github.com/go-chi/chi/v5" 20 - tangled "tangled.sh/tangled.sh/core/api/tangled" 20 + "tangled.sh/tangled.sh/core/api/tangled" 21 21 "tangled.sh/tangled.sh/core/appview" 22 - "tangled.sh/tangled.sh/core/appview/auth" 23 22 "tangled.sh/tangled.sh/core/appview/db" 23 + "tangled.sh/tangled.sh/core/appview/knotclient" 24 + "tangled.sh/tangled.sh/core/appview/oauth" 24 25 "tangled.sh/tangled.sh/core/appview/pages" 25 26 "tangled.sh/tangled.sh/core/jetstream" 26 27 "tangled.sh/tangled.sh/core/rbac" ··· 28 29 29 30 type State struct { 30 31 db *db.DB 31 - auth *auth.Auth 32 + oauth *oauth.OAuth 32 33 enforcer *rbac.Enforcer 33 - tidClock *syntax.TIDClock 34 + tidClock syntax.TIDClock 34 35 pages *pages.Pages 35 36 resolver *appview.Resolver 36 37 jc *jetstream.JetstreamClient ··· 38 39 } 39 40 40 41 func Make(config *appview.Config) (*State, error) { 41 - d, err := db.Make(config.DbPath) 42 + d, err := db.Make(config.Core.DbPath) 42 43 if err != nil { 43 44 return nil, err 44 45 } 45 46 46 - auth, err := auth.Make(config.CookieSecret) 47 - if err != nil { 48 - return nil, err 49 - } 50 - 51 - enforcer, err := rbac.NewEnforcer(config.DbPath) 47 + enforcer, err := rbac.NewEnforcer(config.Core.DbPath) 52 48 if err != nil { 53 49 return nil, err 54 50 } 55 51 56 52 clock := syntax.NewTIDClock(0) 57 53 58 - pgs := pages.NewPages() 54 + pgs := pages.NewPages(config) 59 55 60 56 resolver := appview.NewResolver() 57 + 58 + oauth := oauth.NewOAuth(d, config) 61 59 62 60 wrapper := db.DbWrapper{d} 63 61 jc, err := jetstream.NewJetstreamClient( 64 - config.JetstreamEndpoint, 62 + config.Jetstream.Endpoint, 65 63 "appview", 66 - []string{tangled.GraphFollowNSID, tangled.FeedStarNSID}, 64 + []string{ 65 + tangled.GraphFollowNSID, 66 + tangled.FeedStarNSID, 67 + tangled.PublicKeyNSID, 68 + tangled.RepoArtifactNSID, 69 + tangled.ActorProfileNSID, 70 + }, 67 71 nil, 68 72 slog.Default(), 69 73 wrapper, ··· 72 76 if err != nil { 73 77 return nil, fmt.Errorf("failed to create jetstream client: %w", err) 74 78 } 75 - err = jc.StartJetstream(context.Background(), jetstreamIngester(wrapper)) 79 + err = jc.StartJetstream(context.Background(), appview.Ingest(wrapper, enforcer)) 76 80 if err != nil { 77 81 return nil, fmt.Errorf("failed to start jetstream watcher: %w", err) 78 82 } 79 83 80 84 state := &State{ 81 85 d, 82 - auth, 86 + oauth, 83 87 enforcer, 84 88 clock, 85 89 pgs, ··· 91 95 return state, nil 92 96 } 93 97 94 - func (s *State) TID() string { 95 - return s.tidClock.Next().String() 98 + func TID(c *syntax.TIDClock) string { 99 + return c.Next().String() 96 100 } 97 101 98 - func (s *State) Login(w http.ResponseWriter, r *http.Request) { 99 - ctx := r.Context() 102 + // func (s *State) Login(w http.ResponseWriter, r *http.Request) { 103 + // ctx := r.Context() 100 104 101 - switch r.Method { 102 - case http.MethodGet: 103 - err := s.pages.Login(w, pages.LoginParams{}) 104 - if err != nil { 105 - log.Printf("rendering login page: %s", err) 106 - } 105 + // switch r.Method { 106 + // case http.MethodGet: 107 + // err := s.pages.Login(w, pages.LoginParams{}) 108 + // if err != nil { 109 + // log.Printf("rendering login page: %s", err) 110 + // } 107 111 108 - return 109 - case http.MethodPost: 110 - handle := strings.TrimPrefix(r.FormValue("handle"), "@") 111 - appPassword := r.FormValue("app_password") 112 + // return 113 + // case http.MethodPost: 114 + // handle := strings.TrimPrefix(r.FormValue("handle"), "@") 115 + // appPassword := r.FormValue("app_password") 112 116 113 - resolved, err := s.resolver.ResolveIdent(ctx, handle) 114 - if err != nil { 115 - log.Println("failed to resolve handle:", err) 116 - s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 117 - return 118 - } 117 + // resolved, err := s.resolver.ResolveIdent(ctx, handle) 118 + // if err != nil { 119 + // log.Println("failed to resolve handle:", err) 120 + // s.pages.Notice(w, "login-msg", fmt.Sprintf("\"%s\" is an invalid handle.", handle)) 121 + // return 122 + // } 119 123 120 - atSession, err := s.auth.CreateInitialSession(ctx, resolved, appPassword) 121 - if err != nil { 122 - s.pages.Notice(w, "login-msg", "Invalid handle or password.") 123 - return 124 - } 125 - sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 124 + // atSession, err := s.oauth.CreateInitialSession(ctx, resolved, appPassword) 125 + // if err != nil { 126 + // s.pages.Notice(w, "login-msg", "Invalid handle or password.") 127 + // return 128 + // } 129 + // sessionish := auth.CreateSessionWrapper{ServerCreateSession_Output: atSession} 126 130 127 - err = s.auth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 128 - if err != nil { 129 - s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 130 - return 131 - } 131 + // err = s.oauth.StoreSession(r, w, &sessionish, resolved.PDSEndpoint()) 132 + // if err != nil { 133 + // s.pages.Notice(w, "login-msg", "Failed to login, try again later.") 134 + // return 135 + // } 132 136 133 - log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 137 + // log.Printf("successfully saved session for %s (%s)", atSession.Handle, atSession.Did) 134 138 135 - did := resolved.DID.String() 136 - defaultKnot := "knot1.tangled.sh" 139 + // did := resolved.DID.String() 140 + // defaultKnot := "knot1.tangled.sh" 137 141 138 - go func() { 139 - log.Printf("adding %s to default knot", did) 140 - err = s.enforcer.AddMember(defaultKnot, did) 141 - if err != nil { 142 - log.Println("failed to add user to knot1.tangled.sh: ", err) 143 - return 144 - } 145 - err = s.enforcer.E.SavePolicy() 146 - if err != nil { 147 - log.Println("failed to add user to knot1.tangled.sh: ", err) 148 - return 149 - } 142 + // go func() { 143 + // log.Printf("adding %s to default knot", did) 144 + // err = s.enforcer.AddMember(defaultKnot, did) 145 + // if err != nil { 146 + // log.Println("failed to add user to knot1.tangled.sh: ", err) 147 + // return 148 + // } 149 + // err = s.enforcer.E.SavePolicy() 150 + // if err != nil { 151 + // log.Println("failed to add user to knot1.tangled.sh: ", err) 152 + // return 153 + // } 150 154 151 - secret, err := db.GetRegistrationKey(s.db, defaultKnot) 152 - if err != nil { 153 - log.Println("failed to get registration key for knot1.tangled.sh") 154 - return 155 - } 156 - signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Dev) 157 - resp, err := signedClient.AddMember(did) 158 - if err != nil { 159 - log.Println("failed to add user to knot1.tangled.sh: ", err) 160 - return 161 - } 155 + // secret, err := db.GetRegistrationKey(s.db, defaultKnot) 156 + // if err != nil { 157 + // log.Println("failed to get registration key for knot1.tangled.sh") 158 + // return 159 + // } 160 + // signedClient, err := NewSignedClient(defaultKnot, secret, s.config.Core.Dev) 161 + // resp, err := signedClient.AddMember(did) 162 + // if err != nil { 163 + // log.Println("failed to add user to knot1.tangled.sh: ", err) 164 + // return 165 + // } 162 166 163 - if resp.StatusCode != http.StatusNoContent { 164 - log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 165 - return 166 - } 167 - }() 167 + // if resp.StatusCode != http.StatusNoContent { 168 + // log.Println("failed to add user to knot1.tangled.sh: ", resp.StatusCode) 169 + // return 170 + // } 171 + // }() 168 172 169 - s.pages.HxRedirect(w, "/") 170 - return 171 - } 172 - } 173 + // s.pages.HxRedirect(w, "/") 174 + // return 175 + // } 176 + // } 173 177 174 178 func (s *State) Logout(w http.ResponseWriter, r *http.Request) { 175 - s.auth.ClearSession(r, w) 179 + s.oauth.ClearSession(r, w) 176 180 w.Header().Set("HX-Redirect", "/login") 177 181 w.WriteHeader(http.StatusSeeOther) 178 182 } 179 183 180 184 func (s *State) Timeline(w http.ResponseWriter, r *http.Request) { 181 - user := s.auth.GetUser(r) 185 + user := s.oauth.GetUser(r) 182 186 183 187 timeline, err := db.MakeTimeline(s.db) 184 188 if err != nil { ··· 229 233 230 234 return 231 235 case http.MethodPost: 232 - session, err := s.auth.Store.Get(r, appview.SessionName) 236 + session, err := s.oauth.Store.Get(r, appview.SessionName) 233 237 if err != nil || session.IsNew { 234 238 log.Println("unauthorized attempt to generate registration key") 235 239 http.Error(w, "Forbidden", http.StatusUnauthorized) ··· 291 295 292 296 // create a signed request and check if a node responds to that 293 297 func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { 294 - user := s.auth.GetUser(r) 298 + user := s.oauth.GetUser(r) 295 299 296 300 domain := chi.URLParam(r, "domain") 297 301 if domain == "" { ··· 306 310 return 307 311 } 308 312 309 - client, err := NewSignedClient(domain, secret, s.config.Dev) 313 + client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) 310 314 if err != nil { 311 315 log.Println("failed to create client to ", domain) 312 316 } ··· 415 419 return 416 420 } 417 421 418 - user := s.auth.GetUser(r) 422 + user := s.oauth.GetUser(r) 419 423 reg, err := db.RegistrationByDomain(s.db, domain) 420 424 if err != nil { 421 425 w.Write([]byte("failed to pull up registration info")) ··· 463 467 // get knots registered by this user 464 468 func (s *State) Knots(w http.ResponseWriter, r *http.Request) { 465 469 // for now, this is just pubkeys 466 - user := s.auth.GetUser(r) 470 + user := s.oauth.GetUser(r) 467 471 registrations, err := db.RegistrationsByDid(s.db, user.Did) 468 472 if err != nil { 469 473 log.Println(err) ··· 502 506 return 503 507 } 504 508 505 - memberDid := r.FormValue("member") 506 - if memberDid == "" { 509 + subjectIdentifier := r.FormValue("subject") 510 + if subjectIdentifier == "" { 507 511 http.Error(w, "malformed form", http.StatusBadRequest) 508 512 return 509 513 } 510 514 511 - memberIdent, err := s.resolver.ResolveIdent(r.Context(), memberDid) 515 + subjectIdentity, err := s.resolver.ResolveIdent(r.Context(), subjectIdentifier) 512 516 if err != nil { 513 517 w.Write([]byte("failed to resolve member did to a handle")) 514 518 return 515 519 } 516 - log.Printf("adding %s to %s\n", memberIdent.Handle.String(), domain) 520 + log.Printf("adding %s to %s\n", subjectIdentity.Handle.String(), domain) 517 521 518 522 // announce this relation into the firehose, store into owners' pds 519 - client, _ := s.auth.AuthorizedClient(r) 520 - currentUser := s.auth.GetUser(r) 521 - addedAt := time.Now().Format(time.RFC3339) 522 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 523 + client, err := s.oauth.AuthorizedClient(r) 524 + if err != nil { 525 + http.Error(w, "failed to authorize client", http.StatusInternalServerError) 526 + return 527 + } 528 + currentUser := s.oauth.GetUser(r) 529 + createdAt := time.Now().Format(time.RFC3339) 530 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 523 531 Collection: tangled.KnotMemberNSID, 524 532 Repo: currentUser.Did, 525 - Rkey: s.TID(), 533 + Rkey: appview.TID(), 526 534 Record: &lexutil.LexiconTypeDecoder{ 527 535 Val: &tangled.KnotMember{ 528 - Member: memberIdent.DID.String(), 529 - Domain: domain, 530 - AddedAt: &addedAt, 536 + Subject: subjectIdentity.DID.String(), 537 + Domain: domain, 538 + CreatedAt: createdAt, 531 539 }}, 532 540 }) 533 541 ··· 544 552 return 545 553 } 546 554 547 - ksClient, err := NewSignedClient(domain, secret, s.config.Dev) 555 + ksClient, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) 548 556 if err != nil { 549 557 log.Println("failed to create client to ", domain) 550 558 return 551 559 } 552 560 553 - ksResp, err := ksClient.AddMember(memberIdent.DID.String()) 561 + ksResp, err := ksClient.AddMember(subjectIdentity.DID.String()) 554 562 if err != nil { 555 563 log.Printf("failed to make request to %s: %s", domain, err) 556 564 return ··· 561 569 return 562 570 } 563 571 564 - err = s.enforcer.AddMember(domain, memberIdent.DID.String()) 572 + err = s.enforcer.AddMember(domain, subjectIdentity.DID.String()) 565 573 if err != nil { 566 574 w.Write([]byte(fmt.Sprint("failed to add member: ", err))) 567 575 return 568 576 } 569 577 570 - w.Write([]byte(fmt.Sprint("added member: ", memberIdent.Handle.String()))) 578 + w.Write([]byte(fmt.Sprint("added member: ", subjectIdentity.Handle.String()))) 571 579 } 572 580 573 581 func (s *State) RemoveMember(w http.ResponseWriter, r *http.Request) { 574 582 } 575 583 584 + func validateRepoName(name string) error { 585 + // check for path traversal attempts 586 + if name == "." || name == ".." || 587 + strings.Contains(name, "/") || strings.Contains(name, "\\") { 588 + return fmt.Errorf("Repository name contains invalid path characters") 589 + } 590 + 591 + // check for sequences that could be used for traversal when normalized 592 + if strings.Contains(name, "./") || strings.Contains(name, "../") || 593 + strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 594 + return fmt.Errorf("Repository name contains invalid path sequence") 595 + } 596 + 597 + // then continue with character validation 598 + for _, char := range name { 599 + if !((char >= 'a' && char <= 'z') || 600 + (char >= 'A' && char <= 'Z') || 601 + (char >= '0' && char <= '9') || 602 + char == '-' || char == '_' || char == '.') { 603 + return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 604 + } 605 + } 606 + 607 + // additional check to prevent multiple sequential dots 608 + if strings.Contains(name, "..") { 609 + return fmt.Errorf("Repository name cannot contain sequential dots") 610 + } 611 + 612 + // if all checks pass 613 + return nil 614 + } 615 + 576 616 func (s *State) NewRepo(w http.ResponseWriter, r *http.Request) { 577 617 switch r.Method { 578 618 case http.MethodGet: 579 - user := s.auth.GetUser(r) 619 + user := s.oauth.GetUser(r) 580 620 knots, err := s.enforcer.GetDomainsForUser(user.Did) 581 621 if err != nil { 582 622 s.pages.Notice(w, "repo", "Invalid user account.") ··· 589 629 }) 590 630 591 631 case http.MethodPost: 592 - user := s.auth.GetUser(r) 632 + user := s.oauth.GetUser(r) 593 633 594 634 domain := r.FormValue("domain") 595 635 if domain == "" { ··· 603 643 return 604 644 } 605 645 606 - // Check for valid repository name (GitHub-like rules) 607 - // No spaces, only alphanumeric characters, dashes, and underscores 608 - for _, char := range repoName { 609 - if !((char >= 'a' && char <= 'z') || 610 - (char >= 'A' && char <= 'Z') || 611 - (char >= '0' && char <= '9') || 612 - char == '-' || char == '_' || char == '.') { 613 - s.pages.Notice(w, "repo", "Repository name can only contain alphanumeric characters, periods, hyphens, and underscores.") 614 - return 615 - } 646 + if err := validateRepoName(repoName); err != nil { 647 + s.pages.Notice(w, "repo", err.Error()) 648 + return 616 649 } 617 650 618 651 defaultBranch := r.FormValue("branch") ··· 640 673 return 641 674 } 642 675 643 - client, err := NewSignedClient(domain, secret, s.config.Dev) 676 + client, err := knotclient.NewSignedClient(domain, secret, s.config.Core.Dev) 644 677 if err != nil { 645 678 s.pages.Notice(w, "repo", "Failed to connect to knot server.") 646 679 return 647 680 } 648 681 649 - rkey := s.TID() 682 + rkey := appview.TID() 650 683 repo := &db.Repo{ 651 684 Did: user.Did, 652 685 Name: repoName, ··· 655 688 Description: description, 656 689 } 657 690 658 - xrpcClient, _ := s.auth.AuthorizedClient(r) 691 + xrpcClient, err := s.oauth.AuthorizedClient(r) 692 + if err != nil { 693 + s.pages.Notice(w, "repo", "Failed to write record to PDS.") 694 + return 695 + } 659 696 660 - addedAt := time.Now().Format(time.RFC3339) 661 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 697 + createdAt := time.Now().Format(time.RFC3339) 698 + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 662 699 Collection: tangled.RepoNSID, 663 700 Repo: user.Did, 664 701 Rkey: rkey, 665 702 Record: &lexutil.LexiconTypeDecoder{ 666 703 Val: &tangled.Repo{ 667 - Knot: repo.Knot, 668 - Name: repoName, 669 - AddedAt: &addedAt, 670 - Owner: user.Did, 704 + Knot: repo.Knot, 705 + Name: repoName, 706 + CreatedAt: createdAt, 707 + Owner: user.Did, 671 708 }}, 672 709 }) 673 710 if err != nil { ··· 742 779 return 743 780 } 744 781 } 745 - 746 - func GetAvatarUri(handle string) (string, error) { 747 - return fmt.Sprintf("https://avatars.dog/%s@webp", handle), nil 748 - }
+11
appview/tid.go
··· 1 + package appview 2 + 3 + import ( 4 + "github.com/bluesky-social/indigo/atproto/syntax" 5 + ) 6 + 7 + var c syntax.TIDClock = syntax.NewTIDClock(0) 8 + 9 + func TID() string { 10 + return c.Next().String() 11 + }
+80
appview/xrpcclient/xrpc.go
··· 1 + package xrpcclient 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "io" 7 + 8 + "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/xrpc" 10 + oauth "github.com/haileyok/atproto-oauth-golang" 11 + ) 12 + 13 + type Client struct { 14 + *oauth.XrpcClient 15 + authArgs *oauth.XrpcAuthedRequestArgs 16 + } 17 + 18 + func NewClient(client *oauth.XrpcClient, authArgs *oauth.XrpcAuthedRequestArgs) *Client { 19 + return &Client{ 20 + XrpcClient: client, 21 + authArgs: authArgs, 22 + } 23 + } 24 + 25 + func (c *Client) RepoPutRecord(ctx context.Context, input *atproto.RepoPutRecord_Input) (*atproto.RepoPutRecord_Output, error) { 26 + var out atproto.RepoPutRecord_Output 27 + if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil { 28 + return nil, err 29 + } 30 + 31 + return &out, nil 32 + } 33 + 34 + func (c *Client) RepoGetRecord(ctx context.Context, cid string, collection string, repo string, rkey string) (*atproto.RepoGetRecord_Output, error) { 35 + var out atproto.RepoGetRecord_Output 36 + 37 + params := map[string]interface{}{ 38 + "cid": cid, 39 + "collection": collection, 40 + "repo": repo, 41 + "rkey": rkey, 42 + } 43 + if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.repo.getRecord", params, nil, &out); err != nil { 44 + return nil, err 45 + } 46 + 47 + return &out, nil 48 + } 49 + 50 + func (c *Client) RepoUploadBlob(ctx context.Context, input io.Reader) (*atproto.RepoUploadBlob_Output, error) { 51 + var out atproto.RepoUploadBlob_Output 52 + if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "*/*", "com.atproto.repo.uploadBlob", nil, input, &out); err != nil { 53 + return nil, err 54 + } 55 + 56 + return &out, nil 57 + } 58 + 59 + func (c *Client) SyncGetBlob(ctx context.Context, cid string, did string) ([]byte, error) { 60 + buf := new(bytes.Buffer) 61 + 62 + params := map[string]interface{}{ 63 + "cid": cid, 64 + "did": did, 65 + } 66 + if err := c.Do(ctx, c.authArgs, xrpc.Query, "", "com.atproto.sync.getBlob", params, nil, buf); err != nil { 67 + return nil, err 68 + } 69 + 70 + return buf.Bytes(), nil 71 + } 72 + 73 + func (c *Client) RepoDeleteRecord(ctx context.Context, input *atproto.RepoDeleteRecord_Input) (*atproto.RepoDeleteRecord_Output, error) { 74 + var out atproto.RepoDeleteRecord_Output 75 + if err := c.Do(ctx, c.authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.deleteRecord", nil, input, &out); err != nil { 76 + return nil, err 77 + } 78 + 79 + return &out, nil 80 + }
+172
avatar/.gitignore
··· 1 + # Logs 2 + 3 + logs 4 + _.log 5 + npm-debug.log_ 6 + yarn-debug.log* 7 + yarn-error.log* 8 + lerna-debug.log* 9 + .pnpm-debug.log* 10 + 11 + # Diagnostic reports (https://nodejs.org/api/report.html) 12 + 13 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 + 15 + # Runtime data 16 + 17 + pids 18 + _.pid 19 + _.seed 20 + \*.pid.lock 21 + 22 + # Directory for instrumented libs generated by jscoverage/JSCover 23 + 24 + lib-cov 25 + 26 + # Coverage directory used by tools like istanbul 27 + 28 + coverage 29 + \*.lcov 30 + 31 + # nyc test coverage 32 + 33 + .nyc_output 34 + 35 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 + 37 + .grunt 38 + 39 + # Bower dependency directory (https://bower.io/) 40 + 41 + bower_components 42 + 43 + # node-waf configuration 44 + 45 + .lock-wscript 46 + 47 + # Compiled binary addons (https://nodejs.org/api/addons.html) 48 + 49 + build/Release 50 + 51 + # Dependency directories 52 + 53 + node_modules/ 54 + jspm_packages/ 55 + 56 + # Snowpack dependency directory (https://snowpack.dev/) 57 + 58 + web_modules/ 59 + 60 + # TypeScript cache 61 + 62 + \*.tsbuildinfo 63 + 64 + # Optional npm cache directory 65 + 66 + .npm 67 + 68 + # Optional eslint cache 69 + 70 + .eslintcache 71 + 72 + # Optional stylelint cache 73 + 74 + .stylelintcache 75 + 76 + # Microbundle cache 77 + 78 + .rpt2_cache/ 79 + .rts2_cache_cjs/ 80 + .rts2_cache_es/ 81 + .rts2_cache_umd/ 82 + 83 + # Optional REPL history 84 + 85 + .node_repl_history 86 + 87 + # Output of 'npm pack' 88 + 89 + \*.tgz 90 + 91 + # Yarn Integrity file 92 + 93 + .yarn-integrity 94 + 95 + # dotenv environment variable files 96 + 97 + .env 98 + .env.development.local 99 + .env.test.local 100 + .env.production.local 101 + .env.local 102 + 103 + # parcel-bundler cache (https://parceljs.org/) 104 + 105 + .cache 106 + .parcel-cache 107 + 108 + # Next.js build output 109 + 110 + .next 111 + out 112 + 113 + # Nuxt.js build / generate output 114 + 115 + .nuxt 116 + dist 117 + 118 + # Gatsby files 119 + 120 + .cache/ 121 + 122 + # Comment in the public line in if your project uses Gatsby and not Next.js 123 + 124 + # https://nextjs.org/blog/next-9-1#public-directory-support 125 + 126 + # public 127 + 128 + # vuepress build output 129 + 130 + .vuepress/dist 131 + 132 + # vuepress v2.x temp and cache directory 133 + 134 + .temp 135 + .cache 136 + 137 + # Docusaurus cache and generated files 138 + 139 + .docusaurus 140 + 141 + # Serverless directories 142 + 143 + .serverless/ 144 + 145 + # FuseBox cache 146 + 147 + .fusebox/ 148 + 149 + # DynamoDB Local files 150 + 151 + .dynamodb/ 152 + 153 + # TernJS port file 154 + 155 + .tern-port 156 + 157 + # Stores VSCode versions used for testing VSCode extensions 158 + 159 + .vscode-test 160 + 161 + # yarn v2 162 + 163 + .yarn/cache 164 + .yarn/unplugged 165 + .yarn/build-state.yml 166 + .yarn/install-state.gz 167 + .pnp.\* 168 + 169 + # wrangler project 170 + 171 + .dev.vars 172 + .wrangler/
+3024
avatar/package-lock.json
··· 1 + { 2 + "name": "avatar", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "avatar", 9 + "version": "0.0.0", 10 + "devDependencies": { 11 + "@cloudflare/vitest-pool-workers": "^0.8.19", 12 + "vitest": "~3.0.7", 13 + "wrangler": "^4.14.1" 14 + } 15 + }, 16 + "node_modules/@cloudflare/kv-asset-handler": { 17 + "version": "0.4.0", 18 + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", 19 + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", 20 + "dev": true, 21 + "license": "MIT OR Apache-2.0", 22 + "dependencies": { 23 + "mime": "^3.0.0" 24 + }, 25 + "engines": { 26 + "node": ">=18.0.0" 27 + } 28 + }, 29 + "node_modules/@cloudflare/unenv-preset": { 30 + "version": "2.3.1", 31 + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz", 32 + "integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==", 33 + "dev": true, 34 + "license": "MIT OR Apache-2.0", 35 + "peerDependencies": { 36 + "unenv": "2.0.0-rc.15", 37 + "workerd": "^1.20250320.0" 38 + }, 39 + "peerDependenciesMeta": { 40 + "workerd": { 41 + "optional": true 42 + } 43 + } 44 + }, 45 + "node_modules/@cloudflare/vitest-pool-workers": { 46 + "version": "0.8.24", 47 + "resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.24.tgz", 48 + "integrity": "sha512-wT2PABJQ9YLYWrVu4CRZOjvmjHkdbMyLTZPU9n/7JEMM3pgG8dY41F1Rj31UsXRQaXX39A/CTPGlk58dcMUysA==", 49 + "dev": true, 50 + "license": "MIT", 51 + "dependencies": { 52 + "birpc": "0.2.14", 53 + "cjs-module-lexer": "^1.2.3", 54 + "devalue": "^4.3.0", 55 + "miniflare": "4.20250428.1", 56 + "semver": "^7.7.1", 57 + "wrangler": "4.14.1", 58 + "zod": "^3.22.3" 59 + }, 60 + "peerDependencies": { 61 + "@vitest/runner": "2.0.x - 3.1.x", 62 + "@vitest/snapshot": "2.0.x - 3.1.x", 63 + "vitest": "2.0.x - 3.1.x" 64 + } 65 + }, 66 + "node_modules/@cloudflare/workerd-darwin-64": { 67 + "version": "1.20250428.0", 68 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250428.0.tgz", 69 + "integrity": "sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==", 70 + "cpu": [ 71 + "x64" 72 + ], 73 + "dev": true, 74 + "license": "Apache-2.0", 75 + "optional": true, 76 + "os": [ 77 + "darwin" 78 + ], 79 + "engines": { 80 + "node": ">=16" 81 + } 82 + }, 83 + "node_modules/@cloudflare/workerd-darwin-arm64": { 84 + "version": "1.20250428.0", 85 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250428.0.tgz", 86 + "integrity": "sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==", 87 + "cpu": [ 88 + "arm64" 89 + ], 90 + "dev": true, 91 + "license": "Apache-2.0", 92 + "optional": true, 93 + "os": [ 94 + "darwin" 95 + ], 96 + "engines": { 97 + "node": ">=16" 98 + } 99 + }, 100 + "node_modules/@cloudflare/workerd-linux-64": { 101 + "version": "1.20250428.0", 102 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250428.0.tgz", 103 + "integrity": "sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==", 104 + "cpu": [ 105 + "x64" 106 + ], 107 + "dev": true, 108 + "license": "Apache-2.0", 109 + "optional": true, 110 + "os": [ 111 + "linux" 112 + ], 113 + "engines": { 114 + "node": ">=16" 115 + } 116 + }, 117 + "node_modules/@cloudflare/workerd-linux-arm64": { 118 + "version": "1.20250428.0", 119 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250428.0.tgz", 120 + "integrity": "sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==", 121 + "cpu": [ 122 + "arm64" 123 + ], 124 + "dev": true, 125 + "license": "Apache-2.0", 126 + "optional": true, 127 + "os": [ 128 + "linux" 129 + ], 130 + "engines": { 131 + "node": ">=16" 132 + } 133 + }, 134 + "node_modules/@cloudflare/workerd-windows-64": { 135 + "version": "1.20250428.0", 136 + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250428.0.tgz", 137 + "integrity": "sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==", 138 + "cpu": [ 139 + "x64" 140 + ], 141 + "dev": true, 142 + "license": "Apache-2.0", 143 + "optional": true, 144 + "os": [ 145 + "win32" 146 + ], 147 + "engines": { 148 + "node": ">=16" 149 + } 150 + }, 151 + "node_modules/@cspotcode/source-map-support": { 152 + "version": "0.8.1", 153 + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 154 + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 155 + "dev": true, 156 + "license": "MIT", 157 + "dependencies": { 158 + "@jridgewell/trace-mapping": "0.3.9" 159 + }, 160 + "engines": { 161 + "node": ">=12" 162 + } 163 + }, 164 + "node_modules/@emnapi/runtime": { 165 + "version": "1.4.3", 166 + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", 167 + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", 168 + "dev": true, 169 + "license": "MIT", 170 + "optional": true, 171 + "dependencies": { 172 + "tslib": "^2.4.0" 173 + } 174 + }, 175 + "node_modules/@esbuild/aix-ppc64": { 176 + "version": "0.25.3", 177 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", 178 + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", 179 + "cpu": [ 180 + "ppc64" 181 + ], 182 + "dev": true, 183 + "license": "MIT", 184 + "optional": true, 185 + "os": [ 186 + "aix" 187 + ], 188 + "engines": { 189 + "node": ">=18" 190 + } 191 + }, 192 + "node_modules/@esbuild/android-arm": { 193 + "version": "0.25.3", 194 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", 195 + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", 196 + "cpu": [ 197 + "arm" 198 + ], 199 + "dev": true, 200 + "license": "MIT", 201 + "optional": true, 202 + "os": [ 203 + "android" 204 + ], 205 + "engines": { 206 + "node": ">=18" 207 + } 208 + }, 209 + "node_modules/@esbuild/android-arm64": { 210 + "version": "0.25.3", 211 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", 212 + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", 213 + "cpu": [ 214 + "arm64" 215 + ], 216 + "dev": true, 217 + "license": "MIT", 218 + "optional": true, 219 + "os": [ 220 + "android" 221 + ], 222 + "engines": { 223 + "node": ">=18" 224 + } 225 + }, 226 + "node_modules/@esbuild/android-x64": { 227 + "version": "0.25.3", 228 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", 229 + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", 230 + "cpu": [ 231 + "x64" 232 + ], 233 + "dev": true, 234 + "license": "MIT", 235 + "optional": true, 236 + "os": [ 237 + "android" 238 + ], 239 + "engines": { 240 + "node": ">=18" 241 + } 242 + }, 243 + "node_modules/@esbuild/darwin-arm64": { 244 + "version": "0.25.3", 245 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", 246 + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", 247 + "cpu": [ 248 + "arm64" 249 + ], 250 + "dev": true, 251 + "license": "MIT", 252 + "optional": true, 253 + "os": [ 254 + "darwin" 255 + ], 256 + "engines": { 257 + "node": ">=18" 258 + } 259 + }, 260 + "node_modules/@esbuild/darwin-x64": { 261 + "version": "0.25.3", 262 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", 263 + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", 264 + "cpu": [ 265 + "x64" 266 + ], 267 + "dev": true, 268 + "license": "MIT", 269 + "optional": true, 270 + "os": [ 271 + "darwin" 272 + ], 273 + "engines": { 274 + "node": ">=18" 275 + } 276 + }, 277 + "node_modules/@esbuild/freebsd-arm64": { 278 + "version": "0.25.3", 279 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", 280 + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", 281 + "cpu": [ 282 + "arm64" 283 + ], 284 + "dev": true, 285 + "license": "MIT", 286 + "optional": true, 287 + "os": [ 288 + "freebsd" 289 + ], 290 + "engines": { 291 + "node": ">=18" 292 + } 293 + }, 294 + "node_modules/@esbuild/freebsd-x64": { 295 + "version": "0.25.3", 296 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", 297 + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", 298 + "cpu": [ 299 + "x64" 300 + ], 301 + "dev": true, 302 + "license": "MIT", 303 + "optional": true, 304 + "os": [ 305 + "freebsd" 306 + ], 307 + "engines": { 308 + "node": ">=18" 309 + } 310 + }, 311 + "node_modules/@esbuild/linux-arm": { 312 + "version": "0.25.3", 313 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", 314 + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", 315 + "cpu": [ 316 + "arm" 317 + ], 318 + "dev": true, 319 + "license": "MIT", 320 + "optional": true, 321 + "os": [ 322 + "linux" 323 + ], 324 + "engines": { 325 + "node": ">=18" 326 + } 327 + }, 328 + "node_modules/@esbuild/linux-arm64": { 329 + "version": "0.25.3", 330 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", 331 + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", 332 + "cpu": [ 333 + "arm64" 334 + ], 335 + "dev": true, 336 + "license": "MIT", 337 + "optional": true, 338 + "os": [ 339 + "linux" 340 + ], 341 + "engines": { 342 + "node": ">=18" 343 + } 344 + }, 345 + "node_modules/@esbuild/linux-ia32": { 346 + "version": "0.25.3", 347 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", 348 + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", 349 + "cpu": [ 350 + "ia32" 351 + ], 352 + "dev": true, 353 + "license": "MIT", 354 + "optional": true, 355 + "os": [ 356 + "linux" 357 + ], 358 + "engines": { 359 + "node": ">=18" 360 + } 361 + }, 362 + "node_modules/@esbuild/linux-loong64": { 363 + "version": "0.25.3", 364 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", 365 + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", 366 + "cpu": [ 367 + "loong64" 368 + ], 369 + "dev": true, 370 + "license": "MIT", 371 + "optional": true, 372 + "os": [ 373 + "linux" 374 + ], 375 + "engines": { 376 + "node": ">=18" 377 + } 378 + }, 379 + "node_modules/@esbuild/linux-mips64el": { 380 + "version": "0.25.3", 381 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", 382 + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", 383 + "cpu": [ 384 + "mips64el" 385 + ], 386 + "dev": true, 387 + "license": "MIT", 388 + "optional": true, 389 + "os": [ 390 + "linux" 391 + ], 392 + "engines": { 393 + "node": ">=18" 394 + } 395 + }, 396 + "node_modules/@esbuild/linux-ppc64": { 397 + "version": "0.25.3", 398 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", 399 + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", 400 + "cpu": [ 401 + "ppc64" 402 + ], 403 + "dev": true, 404 + "license": "MIT", 405 + "optional": true, 406 + "os": [ 407 + "linux" 408 + ], 409 + "engines": { 410 + "node": ">=18" 411 + } 412 + }, 413 + "node_modules/@esbuild/linux-riscv64": { 414 + "version": "0.25.3", 415 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", 416 + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", 417 + "cpu": [ 418 + "riscv64" 419 + ], 420 + "dev": true, 421 + "license": "MIT", 422 + "optional": true, 423 + "os": [ 424 + "linux" 425 + ], 426 + "engines": { 427 + "node": ">=18" 428 + } 429 + }, 430 + "node_modules/@esbuild/linux-s390x": { 431 + "version": "0.25.3", 432 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", 433 + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", 434 + "cpu": [ 435 + "s390x" 436 + ], 437 + "dev": true, 438 + "license": "MIT", 439 + "optional": true, 440 + "os": [ 441 + "linux" 442 + ], 443 + "engines": { 444 + "node": ">=18" 445 + } 446 + }, 447 + "node_modules/@esbuild/linux-x64": { 448 + "version": "0.25.3", 449 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", 450 + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", 451 + "cpu": [ 452 + "x64" 453 + ], 454 + "dev": true, 455 + "license": "MIT", 456 + "optional": true, 457 + "os": [ 458 + "linux" 459 + ], 460 + "engines": { 461 + "node": ">=18" 462 + } 463 + }, 464 + "node_modules/@esbuild/netbsd-arm64": { 465 + "version": "0.25.3", 466 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", 467 + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", 468 + "cpu": [ 469 + "arm64" 470 + ], 471 + "dev": true, 472 + "license": "MIT", 473 + "optional": true, 474 + "os": [ 475 + "netbsd" 476 + ], 477 + "engines": { 478 + "node": ">=18" 479 + } 480 + }, 481 + "node_modules/@esbuild/netbsd-x64": { 482 + "version": "0.25.3", 483 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", 484 + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", 485 + "cpu": [ 486 + "x64" 487 + ], 488 + "dev": true, 489 + "license": "MIT", 490 + "optional": true, 491 + "os": [ 492 + "netbsd" 493 + ], 494 + "engines": { 495 + "node": ">=18" 496 + } 497 + }, 498 + "node_modules/@esbuild/openbsd-arm64": { 499 + "version": "0.25.3", 500 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", 501 + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", 502 + "cpu": [ 503 + "arm64" 504 + ], 505 + "dev": true, 506 + "license": "MIT", 507 + "optional": true, 508 + "os": [ 509 + "openbsd" 510 + ], 511 + "engines": { 512 + "node": ">=18" 513 + } 514 + }, 515 + "node_modules/@esbuild/openbsd-x64": { 516 + "version": "0.25.3", 517 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", 518 + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", 519 + "cpu": [ 520 + "x64" 521 + ], 522 + "dev": true, 523 + "license": "MIT", 524 + "optional": true, 525 + "os": [ 526 + "openbsd" 527 + ], 528 + "engines": { 529 + "node": ">=18" 530 + } 531 + }, 532 + "node_modules/@esbuild/sunos-x64": { 533 + "version": "0.25.3", 534 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", 535 + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", 536 + "cpu": [ 537 + "x64" 538 + ], 539 + "dev": true, 540 + "license": "MIT", 541 + "optional": true, 542 + "os": [ 543 + "sunos" 544 + ], 545 + "engines": { 546 + "node": ">=18" 547 + } 548 + }, 549 + "node_modules/@esbuild/win32-arm64": { 550 + "version": "0.25.3", 551 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", 552 + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", 553 + "cpu": [ 554 + "arm64" 555 + ], 556 + "dev": true, 557 + "license": "MIT", 558 + "optional": true, 559 + "os": [ 560 + "win32" 561 + ], 562 + "engines": { 563 + "node": ">=18" 564 + } 565 + }, 566 + "node_modules/@esbuild/win32-ia32": { 567 + "version": "0.25.3", 568 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", 569 + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", 570 + "cpu": [ 571 + "ia32" 572 + ], 573 + "dev": true, 574 + "license": "MIT", 575 + "optional": true, 576 + "os": [ 577 + "win32" 578 + ], 579 + "engines": { 580 + "node": ">=18" 581 + } 582 + }, 583 + "node_modules/@esbuild/win32-x64": { 584 + "version": "0.25.3", 585 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", 586 + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", 587 + "cpu": [ 588 + "x64" 589 + ], 590 + "dev": true, 591 + "license": "MIT", 592 + "optional": true, 593 + "os": [ 594 + "win32" 595 + ], 596 + "engines": { 597 + "node": ">=18" 598 + } 599 + }, 600 + "node_modules/@fastify/busboy": { 601 + "version": "2.1.1", 602 + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 603 + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 604 + "dev": true, 605 + "license": "MIT", 606 + "engines": { 607 + "node": ">=14" 608 + } 609 + }, 610 + "node_modules/@img/sharp-darwin-arm64": { 611 + "version": "0.33.5", 612 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", 613 + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", 614 + "cpu": [ 615 + "arm64" 616 + ], 617 + "dev": true, 618 + "license": "Apache-2.0", 619 + "optional": true, 620 + "os": [ 621 + "darwin" 622 + ], 623 + "engines": { 624 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 625 + }, 626 + "funding": { 627 + "url": "https://opencollective.com/libvips" 628 + }, 629 + "optionalDependencies": { 630 + "@img/sharp-libvips-darwin-arm64": "1.0.4" 631 + } 632 + }, 633 + "node_modules/@img/sharp-darwin-x64": { 634 + "version": "0.33.5", 635 + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", 636 + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", 637 + "cpu": [ 638 + "x64" 639 + ], 640 + "dev": true, 641 + "license": "Apache-2.0", 642 + "optional": true, 643 + "os": [ 644 + "darwin" 645 + ], 646 + "engines": { 647 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 648 + }, 649 + "funding": { 650 + "url": "https://opencollective.com/libvips" 651 + }, 652 + "optionalDependencies": { 653 + "@img/sharp-libvips-darwin-x64": "1.0.4" 654 + } 655 + }, 656 + "node_modules/@img/sharp-libvips-darwin-arm64": { 657 + "version": "1.0.4", 658 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", 659 + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", 660 + "cpu": [ 661 + "arm64" 662 + ], 663 + "dev": true, 664 + "license": "LGPL-3.0-or-later", 665 + "optional": true, 666 + "os": [ 667 + "darwin" 668 + ], 669 + "funding": { 670 + "url": "https://opencollective.com/libvips" 671 + } 672 + }, 673 + "node_modules/@img/sharp-libvips-darwin-x64": { 674 + "version": "1.0.4", 675 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", 676 + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", 677 + "cpu": [ 678 + "x64" 679 + ], 680 + "dev": true, 681 + "license": "LGPL-3.0-or-later", 682 + "optional": true, 683 + "os": [ 684 + "darwin" 685 + ], 686 + "funding": { 687 + "url": "https://opencollective.com/libvips" 688 + } 689 + }, 690 + "node_modules/@img/sharp-libvips-linux-arm": { 691 + "version": "1.0.5", 692 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", 693 + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", 694 + "cpu": [ 695 + "arm" 696 + ], 697 + "dev": true, 698 + "license": "LGPL-3.0-or-later", 699 + "optional": true, 700 + "os": [ 701 + "linux" 702 + ], 703 + "funding": { 704 + "url": "https://opencollective.com/libvips" 705 + } 706 + }, 707 + "node_modules/@img/sharp-libvips-linux-arm64": { 708 + "version": "1.0.4", 709 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", 710 + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", 711 + "cpu": [ 712 + "arm64" 713 + ], 714 + "dev": true, 715 + "license": "LGPL-3.0-or-later", 716 + "optional": true, 717 + "os": [ 718 + "linux" 719 + ], 720 + "funding": { 721 + "url": "https://opencollective.com/libvips" 722 + } 723 + }, 724 + "node_modules/@img/sharp-libvips-linux-s390x": { 725 + "version": "1.0.4", 726 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", 727 + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", 728 + "cpu": [ 729 + "s390x" 730 + ], 731 + "dev": true, 732 + "license": "LGPL-3.0-or-later", 733 + "optional": true, 734 + "os": [ 735 + "linux" 736 + ], 737 + "funding": { 738 + "url": "https://opencollective.com/libvips" 739 + } 740 + }, 741 + "node_modules/@img/sharp-libvips-linux-x64": { 742 + "version": "1.0.4", 743 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", 744 + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", 745 + "cpu": [ 746 + "x64" 747 + ], 748 + "dev": true, 749 + "license": "LGPL-3.0-or-later", 750 + "optional": true, 751 + "os": [ 752 + "linux" 753 + ], 754 + "funding": { 755 + "url": "https://opencollective.com/libvips" 756 + } 757 + }, 758 + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 759 + "version": "1.0.4", 760 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", 761 + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", 762 + "cpu": [ 763 + "arm64" 764 + ], 765 + "dev": true, 766 + "license": "LGPL-3.0-or-later", 767 + "optional": true, 768 + "os": [ 769 + "linux" 770 + ], 771 + "funding": { 772 + "url": "https://opencollective.com/libvips" 773 + } 774 + }, 775 + "node_modules/@img/sharp-libvips-linuxmusl-x64": { 776 + "version": "1.0.4", 777 + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", 778 + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", 779 + "cpu": [ 780 + "x64" 781 + ], 782 + "dev": true, 783 + "license": "LGPL-3.0-or-later", 784 + "optional": true, 785 + "os": [ 786 + "linux" 787 + ], 788 + "funding": { 789 + "url": "https://opencollective.com/libvips" 790 + } 791 + }, 792 + "node_modules/@img/sharp-linux-arm": { 793 + "version": "0.33.5", 794 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", 795 + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", 796 + "cpu": [ 797 + "arm" 798 + ], 799 + "dev": true, 800 + "license": "Apache-2.0", 801 + "optional": true, 802 + "os": [ 803 + "linux" 804 + ], 805 + "engines": { 806 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 807 + }, 808 + "funding": { 809 + "url": "https://opencollective.com/libvips" 810 + }, 811 + "optionalDependencies": { 812 + "@img/sharp-libvips-linux-arm": "1.0.5" 813 + } 814 + }, 815 + "node_modules/@img/sharp-linux-arm64": { 816 + "version": "0.33.5", 817 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", 818 + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", 819 + "cpu": [ 820 + "arm64" 821 + ], 822 + "dev": true, 823 + "license": "Apache-2.0", 824 + "optional": true, 825 + "os": [ 826 + "linux" 827 + ], 828 + "engines": { 829 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 830 + }, 831 + "funding": { 832 + "url": "https://opencollective.com/libvips" 833 + }, 834 + "optionalDependencies": { 835 + "@img/sharp-libvips-linux-arm64": "1.0.4" 836 + } 837 + }, 838 + "node_modules/@img/sharp-linux-s390x": { 839 + "version": "0.33.5", 840 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", 841 + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", 842 + "cpu": [ 843 + "s390x" 844 + ], 845 + "dev": true, 846 + "license": "Apache-2.0", 847 + "optional": true, 848 + "os": [ 849 + "linux" 850 + ], 851 + "engines": { 852 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 853 + }, 854 + "funding": { 855 + "url": "https://opencollective.com/libvips" 856 + }, 857 + "optionalDependencies": { 858 + "@img/sharp-libvips-linux-s390x": "1.0.4" 859 + } 860 + }, 861 + "node_modules/@img/sharp-linux-x64": { 862 + "version": "0.33.5", 863 + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", 864 + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", 865 + "cpu": [ 866 + "x64" 867 + ], 868 + "dev": true, 869 + "license": "Apache-2.0", 870 + "optional": true, 871 + "os": [ 872 + "linux" 873 + ], 874 + "engines": { 875 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 876 + }, 877 + "funding": { 878 + "url": "https://opencollective.com/libvips" 879 + }, 880 + "optionalDependencies": { 881 + "@img/sharp-libvips-linux-x64": "1.0.4" 882 + } 883 + }, 884 + "node_modules/@img/sharp-linuxmusl-arm64": { 885 + "version": "0.33.5", 886 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", 887 + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", 888 + "cpu": [ 889 + "arm64" 890 + ], 891 + "dev": true, 892 + "license": "Apache-2.0", 893 + "optional": true, 894 + "os": [ 895 + "linux" 896 + ], 897 + "engines": { 898 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 899 + }, 900 + "funding": { 901 + "url": "https://opencollective.com/libvips" 902 + }, 903 + "optionalDependencies": { 904 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" 905 + } 906 + }, 907 + "node_modules/@img/sharp-linuxmusl-x64": { 908 + "version": "0.33.5", 909 + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", 910 + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", 911 + "cpu": [ 912 + "x64" 913 + ], 914 + "dev": true, 915 + "license": "Apache-2.0", 916 + "optional": true, 917 + "os": [ 918 + "linux" 919 + ], 920 + "engines": { 921 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 922 + }, 923 + "funding": { 924 + "url": "https://opencollective.com/libvips" 925 + }, 926 + "optionalDependencies": { 927 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" 928 + } 929 + }, 930 + "node_modules/@img/sharp-wasm32": { 931 + "version": "0.33.5", 932 + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", 933 + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", 934 + "cpu": [ 935 + "wasm32" 936 + ], 937 + "dev": true, 938 + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 939 + "optional": true, 940 + "dependencies": { 941 + "@emnapi/runtime": "^1.2.0" 942 + }, 943 + "engines": { 944 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 945 + }, 946 + "funding": { 947 + "url": "https://opencollective.com/libvips" 948 + } 949 + }, 950 + "node_modules/@img/sharp-win32-ia32": { 951 + "version": "0.33.5", 952 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", 953 + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", 954 + "cpu": [ 955 + "ia32" 956 + ], 957 + "dev": true, 958 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 959 + "optional": true, 960 + "os": [ 961 + "win32" 962 + ], 963 + "engines": { 964 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 965 + }, 966 + "funding": { 967 + "url": "https://opencollective.com/libvips" 968 + } 969 + }, 970 + "node_modules/@img/sharp-win32-x64": { 971 + "version": "0.33.5", 972 + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", 973 + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", 974 + "cpu": [ 975 + "x64" 976 + ], 977 + "dev": true, 978 + "license": "Apache-2.0 AND LGPL-3.0-or-later", 979 + "optional": true, 980 + "os": [ 981 + "win32" 982 + ], 983 + "engines": { 984 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 985 + }, 986 + "funding": { 987 + "url": "https://opencollective.com/libvips" 988 + } 989 + }, 990 + "node_modules/@jridgewell/resolve-uri": { 991 + "version": "3.1.2", 992 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 993 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 994 + "dev": true, 995 + "license": "MIT", 996 + "engines": { 997 + "node": ">=6.0.0" 998 + } 999 + }, 1000 + "node_modules/@jridgewell/sourcemap-codec": { 1001 + "version": "1.5.0", 1002 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 1003 + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 1004 + "dev": true, 1005 + "license": "MIT" 1006 + }, 1007 + "node_modules/@jridgewell/trace-mapping": { 1008 + "version": "0.3.9", 1009 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 1010 + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 1011 + "dev": true, 1012 + "license": "MIT", 1013 + "dependencies": { 1014 + "@jridgewell/resolve-uri": "^3.0.3", 1015 + "@jridgewell/sourcemap-codec": "^1.4.10" 1016 + } 1017 + }, 1018 + "node_modules/@rollup/rollup-android-arm-eabi": { 1019 + "version": "4.40.1", 1020 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz", 1021 + "integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==", 1022 + "cpu": [ 1023 + "arm" 1024 + ], 1025 + "dev": true, 1026 + "license": "MIT", 1027 + "optional": true, 1028 + "os": [ 1029 + "android" 1030 + ] 1031 + }, 1032 + "node_modules/@rollup/rollup-android-arm64": { 1033 + "version": "4.40.1", 1034 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz", 1035 + "integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==", 1036 + "cpu": [ 1037 + "arm64" 1038 + ], 1039 + "dev": true, 1040 + "license": "MIT", 1041 + "optional": true, 1042 + "os": [ 1043 + "android" 1044 + ] 1045 + }, 1046 + "node_modules/@rollup/rollup-darwin-arm64": { 1047 + "version": "4.40.1", 1048 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz", 1049 + "integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==", 1050 + "cpu": [ 1051 + "arm64" 1052 + ], 1053 + "dev": true, 1054 + "license": "MIT", 1055 + "optional": true, 1056 + "os": [ 1057 + "darwin" 1058 + ] 1059 + }, 1060 + "node_modules/@rollup/rollup-darwin-x64": { 1061 + "version": "4.40.1", 1062 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz", 1063 + "integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==", 1064 + "cpu": [ 1065 + "x64" 1066 + ], 1067 + "dev": true, 1068 + "license": "MIT", 1069 + "optional": true, 1070 + "os": [ 1071 + "darwin" 1072 + ] 1073 + }, 1074 + "node_modules/@rollup/rollup-freebsd-arm64": { 1075 + "version": "4.40.1", 1076 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz", 1077 + "integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==", 1078 + "cpu": [ 1079 + "arm64" 1080 + ], 1081 + "dev": true, 1082 + "license": "MIT", 1083 + "optional": true, 1084 + "os": [ 1085 + "freebsd" 1086 + ] 1087 + }, 1088 + "node_modules/@rollup/rollup-freebsd-x64": { 1089 + "version": "4.40.1", 1090 + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz", 1091 + "integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==", 1092 + "cpu": [ 1093 + "x64" 1094 + ], 1095 + "dev": true, 1096 + "license": "MIT", 1097 + "optional": true, 1098 + "os": [ 1099 + "freebsd" 1100 + ] 1101 + }, 1102 + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 1103 + "version": "4.40.1", 1104 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz", 1105 + "integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==", 1106 + "cpu": [ 1107 + "arm" 1108 + ], 1109 + "dev": true, 1110 + "license": "MIT", 1111 + "optional": true, 1112 + "os": [ 1113 + "linux" 1114 + ] 1115 + }, 1116 + "node_modules/@rollup/rollup-linux-arm-musleabihf": { 1117 + "version": "4.40.1", 1118 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz", 1119 + "integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==", 1120 + "cpu": [ 1121 + "arm" 1122 + ], 1123 + "dev": true, 1124 + "license": "MIT", 1125 + "optional": true, 1126 + "os": [ 1127 + "linux" 1128 + ] 1129 + }, 1130 + "node_modules/@rollup/rollup-linux-arm64-gnu": { 1131 + "version": "4.40.1", 1132 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz", 1133 + "integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==", 1134 + "cpu": [ 1135 + "arm64" 1136 + ], 1137 + "dev": true, 1138 + "license": "MIT", 1139 + "optional": true, 1140 + "os": [ 1141 + "linux" 1142 + ] 1143 + }, 1144 + "node_modules/@rollup/rollup-linux-arm64-musl": { 1145 + "version": "4.40.1", 1146 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz", 1147 + "integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==", 1148 + "cpu": [ 1149 + "arm64" 1150 + ], 1151 + "dev": true, 1152 + "license": "MIT", 1153 + "optional": true, 1154 + "os": [ 1155 + "linux" 1156 + ] 1157 + }, 1158 + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { 1159 + "version": "4.40.1", 1160 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz", 1161 + "integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==", 1162 + "cpu": [ 1163 + "loong64" 1164 + ], 1165 + "dev": true, 1166 + "license": "MIT", 1167 + "optional": true, 1168 + "os": [ 1169 + "linux" 1170 + ] 1171 + }, 1172 + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 1173 + "version": "4.40.1", 1174 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz", 1175 + "integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==", 1176 + "cpu": [ 1177 + "ppc64" 1178 + ], 1179 + "dev": true, 1180 + "license": "MIT", 1181 + "optional": true, 1182 + "os": [ 1183 + "linux" 1184 + ] 1185 + }, 1186 + "node_modules/@rollup/rollup-linux-riscv64-gnu": { 1187 + "version": "4.40.1", 1188 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz", 1189 + "integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==", 1190 + "cpu": [ 1191 + "riscv64" 1192 + ], 1193 + "dev": true, 1194 + "license": "MIT", 1195 + "optional": true, 1196 + "os": [ 1197 + "linux" 1198 + ] 1199 + }, 1200 + "node_modules/@rollup/rollup-linux-riscv64-musl": { 1201 + "version": "4.40.1", 1202 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz", 1203 + "integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==", 1204 + "cpu": [ 1205 + "riscv64" 1206 + ], 1207 + "dev": true, 1208 + "license": "MIT", 1209 + "optional": true, 1210 + "os": [ 1211 + "linux" 1212 + ] 1213 + }, 1214 + "node_modules/@rollup/rollup-linux-s390x-gnu": { 1215 + "version": "4.40.1", 1216 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz", 1217 + "integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==", 1218 + "cpu": [ 1219 + "s390x" 1220 + ], 1221 + "dev": true, 1222 + "license": "MIT", 1223 + "optional": true, 1224 + "os": [ 1225 + "linux" 1226 + ] 1227 + }, 1228 + "node_modules/@rollup/rollup-linux-x64-gnu": { 1229 + "version": "4.40.1", 1230 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz", 1231 + "integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==", 1232 + "cpu": [ 1233 + "x64" 1234 + ], 1235 + "dev": true, 1236 + "license": "MIT", 1237 + "optional": true, 1238 + "os": [ 1239 + "linux" 1240 + ] 1241 + }, 1242 + "node_modules/@rollup/rollup-linux-x64-musl": { 1243 + "version": "4.40.1", 1244 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz", 1245 + "integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==", 1246 + "cpu": [ 1247 + "x64" 1248 + ], 1249 + "dev": true, 1250 + "license": "MIT", 1251 + "optional": true, 1252 + "os": [ 1253 + "linux" 1254 + ] 1255 + }, 1256 + "node_modules/@rollup/rollup-win32-arm64-msvc": { 1257 + "version": "4.40.1", 1258 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz", 1259 + "integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==", 1260 + "cpu": [ 1261 + "arm64" 1262 + ], 1263 + "dev": true, 1264 + "license": "MIT", 1265 + "optional": true, 1266 + "os": [ 1267 + "win32" 1268 + ] 1269 + }, 1270 + "node_modules/@rollup/rollup-win32-ia32-msvc": { 1271 + "version": "4.40.1", 1272 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz", 1273 + "integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==", 1274 + "cpu": [ 1275 + "ia32" 1276 + ], 1277 + "dev": true, 1278 + "license": "MIT", 1279 + "optional": true, 1280 + "os": [ 1281 + "win32" 1282 + ] 1283 + }, 1284 + "node_modules/@rollup/rollup-win32-x64-msvc": { 1285 + "version": "4.40.1", 1286 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz", 1287 + "integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==", 1288 + "cpu": [ 1289 + "x64" 1290 + ], 1291 + "dev": true, 1292 + "license": "MIT", 1293 + "optional": true, 1294 + "os": [ 1295 + "win32" 1296 + ] 1297 + }, 1298 + "node_modules/@types/estree": { 1299 + "version": "1.0.7", 1300 + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 1301 + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 1302 + "dev": true, 1303 + "license": "MIT" 1304 + }, 1305 + "node_modules/@vitest/expect": { 1306 + "version": "3.0.9", 1307 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", 1308 + "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", 1309 + "dev": true, 1310 + "license": "MIT", 1311 + "dependencies": { 1312 + "@vitest/spy": "3.0.9", 1313 + "@vitest/utils": "3.0.9", 1314 + "chai": "^5.2.0", 1315 + "tinyrainbow": "^2.0.0" 1316 + }, 1317 + "funding": { 1318 + "url": "https://opencollective.com/vitest" 1319 + } 1320 + }, 1321 + "node_modules/@vitest/mocker": { 1322 + "version": "3.0.9", 1323 + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", 1324 + "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", 1325 + "dev": true, 1326 + "license": "MIT", 1327 + "dependencies": { 1328 + "@vitest/spy": "3.0.9", 1329 + "estree-walker": "^3.0.3", 1330 + "magic-string": "^0.30.17" 1331 + }, 1332 + "funding": { 1333 + "url": "https://opencollective.com/vitest" 1334 + }, 1335 + "peerDependencies": { 1336 + "msw": "^2.4.9", 1337 + "vite": "^5.0.0 || ^6.0.0" 1338 + }, 1339 + "peerDependenciesMeta": { 1340 + "msw": { 1341 + "optional": true 1342 + }, 1343 + "vite": { 1344 + "optional": true 1345 + } 1346 + } 1347 + }, 1348 + "node_modules/@vitest/pretty-format": { 1349 + "version": "3.1.2", 1350 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz", 1351 + "integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==", 1352 + "dev": true, 1353 + "license": "MIT", 1354 + "dependencies": { 1355 + "tinyrainbow": "^2.0.0" 1356 + }, 1357 + "funding": { 1358 + "url": "https://opencollective.com/vitest" 1359 + } 1360 + }, 1361 + "node_modules/@vitest/runner": { 1362 + "version": "3.0.9", 1363 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", 1364 + "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", 1365 + "dev": true, 1366 + "license": "MIT", 1367 + "dependencies": { 1368 + "@vitest/utils": "3.0.9", 1369 + "pathe": "^2.0.3" 1370 + }, 1371 + "funding": { 1372 + "url": "https://opencollective.com/vitest" 1373 + } 1374 + }, 1375 + "node_modules/@vitest/snapshot": { 1376 + "version": "3.0.9", 1377 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", 1378 + "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", 1379 + "dev": true, 1380 + "license": "MIT", 1381 + "dependencies": { 1382 + "@vitest/pretty-format": "3.0.9", 1383 + "magic-string": "^0.30.17", 1384 + "pathe": "^2.0.3" 1385 + }, 1386 + "funding": { 1387 + "url": "https://opencollective.com/vitest" 1388 + } 1389 + }, 1390 + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { 1391 + "version": "3.0.9", 1392 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1393 + "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1394 + "dev": true, 1395 + "license": "MIT", 1396 + "dependencies": { 1397 + "tinyrainbow": "^2.0.0" 1398 + }, 1399 + "funding": { 1400 + "url": "https://opencollective.com/vitest" 1401 + } 1402 + }, 1403 + "node_modules/@vitest/spy": { 1404 + "version": "3.0.9", 1405 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", 1406 + "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", 1407 + "dev": true, 1408 + "license": "MIT", 1409 + "dependencies": { 1410 + "tinyspy": "^3.0.2" 1411 + }, 1412 + "funding": { 1413 + "url": "https://opencollective.com/vitest" 1414 + } 1415 + }, 1416 + "node_modules/@vitest/utils": { 1417 + "version": "3.0.9", 1418 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", 1419 + "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", 1420 + "dev": true, 1421 + "license": "MIT", 1422 + "dependencies": { 1423 + "@vitest/pretty-format": "3.0.9", 1424 + "loupe": "^3.1.3", 1425 + "tinyrainbow": "^2.0.0" 1426 + }, 1427 + "funding": { 1428 + "url": "https://opencollective.com/vitest" 1429 + } 1430 + }, 1431 + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { 1432 + "version": "3.0.9", 1433 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", 1434 + "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", 1435 + "dev": true, 1436 + "license": "MIT", 1437 + "dependencies": { 1438 + "tinyrainbow": "^2.0.0" 1439 + }, 1440 + "funding": { 1441 + "url": "https://opencollective.com/vitest" 1442 + } 1443 + }, 1444 + "node_modules/acorn": { 1445 + "version": "8.14.0", 1446 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 1447 + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 1448 + "dev": true, 1449 + "license": "MIT", 1450 + "bin": { 1451 + "acorn": "bin/acorn" 1452 + }, 1453 + "engines": { 1454 + "node": ">=0.4.0" 1455 + } 1456 + }, 1457 + "node_modules/acorn-walk": { 1458 + "version": "8.3.2", 1459 + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", 1460 + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", 1461 + "dev": true, 1462 + "license": "MIT", 1463 + "engines": { 1464 + "node": ">=0.4.0" 1465 + } 1466 + }, 1467 + "node_modules/as-table": { 1468 + "version": "1.0.55", 1469 + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", 1470 + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", 1471 + "dev": true, 1472 + "license": "MIT", 1473 + "dependencies": { 1474 + "printable-characters": "^1.0.42" 1475 + } 1476 + }, 1477 + "node_modules/assertion-error": { 1478 + "version": "2.0.1", 1479 + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 1480 + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 1481 + "dev": true, 1482 + "license": "MIT", 1483 + "engines": { 1484 + "node": ">=12" 1485 + } 1486 + }, 1487 + "node_modules/birpc": { 1488 + "version": "0.2.14", 1489 + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz", 1490 + "integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==", 1491 + "dev": true, 1492 + "license": "MIT", 1493 + "funding": { 1494 + "url": "https://github.com/sponsors/antfu" 1495 + } 1496 + }, 1497 + "node_modules/blake3-wasm": { 1498 + "version": "2.1.5", 1499 + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 1500 + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 1501 + "dev": true, 1502 + "license": "MIT" 1503 + }, 1504 + "node_modules/cac": { 1505 + "version": "6.7.14", 1506 + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 1507 + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 1508 + "dev": true, 1509 + "license": "MIT", 1510 + "engines": { 1511 + "node": ">=8" 1512 + } 1513 + }, 1514 + "node_modules/chai": { 1515 + "version": "5.2.0", 1516 + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", 1517 + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", 1518 + "dev": true, 1519 + "license": "MIT", 1520 + "dependencies": { 1521 + "assertion-error": "^2.0.1", 1522 + "check-error": "^2.1.1", 1523 + "deep-eql": "^5.0.1", 1524 + "loupe": "^3.1.0", 1525 + "pathval": "^2.0.0" 1526 + }, 1527 + "engines": { 1528 + "node": ">=12" 1529 + } 1530 + }, 1531 + "node_modules/check-error": { 1532 + "version": "2.1.1", 1533 + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 1534 + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 1535 + "dev": true, 1536 + "license": "MIT", 1537 + "engines": { 1538 + "node": ">= 16" 1539 + } 1540 + }, 1541 + "node_modules/cjs-module-lexer": { 1542 + "version": "1.4.3", 1543 + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", 1544 + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", 1545 + "dev": true, 1546 + "license": "MIT" 1547 + }, 1548 + "node_modules/color": { 1549 + "version": "4.2.3", 1550 + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 1551 + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 1552 + "dev": true, 1553 + "license": "MIT", 1554 + "optional": true, 1555 + "dependencies": { 1556 + "color-convert": "^2.0.1", 1557 + "color-string": "^1.9.0" 1558 + }, 1559 + "engines": { 1560 + "node": ">=12.5.0" 1561 + } 1562 + }, 1563 + "node_modules/color-convert": { 1564 + "version": "2.0.1", 1565 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1566 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1567 + "dev": true, 1568 + "license": "MIT", 1569 + "optional": true, 1570 + "dependencies": { 1571 + "color-name": "~1.1.4" 1572 + }, 1573 + "engines": { 1574 + "node": ">=7.0.0" 1575 + } 1576 + }, 1577 + "node_modules/color-name": { 1578 + "version": "1.1.4", 1579 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1580 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1581 + "dev": true, 1582 + "license": "MIT", 1583 + "optional": true 1584 + }, 1585 + "node_modules/color-string": { 1586 + "version": "1.9.1", 1587 + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1588 + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1589 + "dev": true, 1590 + "license": "MIT", 1591 + "optional": true, 1592 + "dependencies": { 1593 + "color-name": "^1.0.0", 1594 + "simple-swizzle": "^0.2.2" 1595 + } 1596 + }, 1597 + "node_modules/cookie": { 1598 + "version": "0.7.2", 1599 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 1600 + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 1601 + "dev": true, 1602 + "license": "MIT", 1603 + "engines": { 1604 + "node": ">= 0.6" 1605 + } 1606 + }, 1607 + "node_modules/data-uri-to-buffer": { 1608 + "version": "2.0.2", 1609 + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", 1610 + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", 1611 + "dev": true, 1612 + "license": "MIT" 1613 + }, 1614 + "node_modules/debug": { 1615 + "version": "4.4.0", 1616 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 1617 + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 1618 + "dev": true, 1619 + "license": "MIT", 1620 + "dependencies": { 1621 + "ms": "^2.1.3" 1622 + }, 1623 + "engines": { 1624 + "node": ">=6.0" 1625 + }, 1626 + "peerDependenciesMeta": { 1627 + "supports-color": { 1628 + "optional": true 1629 + } 1630 + } 1631 + }, 1632 + "node_modules/deep-eql": { 1633 + "version": "5.0.2", 1634 + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 1635 + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 1636 + "dev": true, 1637 + "license": "MIT", 1638 + "engines": { 1639 + "node": ">=6" 1640 + } 1641 + }, 1642 + "node_modules/defu": { 1643 + "version": "6.1.4", 1644 + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 1645 + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 1646 + "dev": true, 1647 + "license": "MIT" 1648 + }, 1649 + "node_modules/detect-libc": { 1650 + "version": "2.0.4", 1651 + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", 1652 + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", 1653 + "dev": true, 1654 + "license": "Apache-2.0", 1655 + "optional": true, 1656 + "engines": { 1657 + "node": ">=8" 1658 + } 1659 + }, 1660 + "node_modules/devalue": { 1661 + "version": "4.3.3", 1662 + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", 1663 + "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", 1664 + "dev": true, 1665 + "license": "MIT" 1666 + }, 1667 + "node_modules/es-module-lexer": { 1668 + "version": "1.7.0", 1669 + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", 1670 + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", 1671 + "dev": true, 1672 + "license": "MIT" 1673 + }, 1674 + "node_modules/esbuild": { 1675 + "version": "0.25.3", 1676 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", 1677 + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", 1678 + "dev": true, 1679 + "hasInstallScript": true, 1680 + "license": "MIT", 1681 + "bin": { 1682 + "esbuild": "bin/esbuild" 1683 + }, 1684 + "engines": { 1685 + "node": ">=18" 1686 + }, 1687 + "optionalDependencies": { 1688 + "@esbuild/aix-ppc64": "0.25.3", 1689 + "@esbuild/android-arm": "0.25.3", 1690 + "@esbuild/android-arm64": "0.25.3", 1691 + "@esbuild/android-x64": "0.25.3", 1692 + "@esbuild/darwin-arm64": "0.25.3", 1693 + "@esbuild/darwin-x64": "0.25.3", 1694 + "@esbuild/freebsd-arm64": "0.25.3", 1695 + "@esbuild/freebsd-x64": "0.25.3", 1696 + "@esbuild/linux-arm": "0.25.3", 1697 + "@esbuild/linux-arm64": "0.25.3", 1698 + "@esbuild/linux-ia32": "0.25.3", 1699 + "@esbuild/linux-loong64": "0.25.3", 1700 + "@esbuild/linux-mips64el": "0.25.3", 1701 + "@esbuild/linux-ppc64": "0.25.3", 1702 + "@esbuild/linux-riscv64": "0.25.3", 1703 + "@esbuild/linux-s390x": "0.25.3", 1704 + "@esbuild/linux-x64": "0.25.3", 1705 + "@esbuild/netbsd-arm64": "0.25.3", 1706 + "@esbuild/netbsd-x64": "0.25.3", 1707 + "@esbuild/openbsd-arm64": "0.25.3", 1708 + "@esbuild/openbsd-x64": "0.25.3", 1709 + "@esbuild/sunos-x64": "0.25.3", 1710 + "@esbuild/win32-arm64": "0.25.3", 1711 + "@esbuild/win32-ia32": "0.25.3", 1712 + "@esbuild/win32-x64": "0.25.3" 1713 + } 1714 + }, 1715 + "node_modules/estree-walker": { 1716 + "version": "3.0.3", 1717 + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 1718 + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 1719 + "dev": true, 1720 + "license": "MIT", 1721 + "dependencies": { 1722 + "@types/estree": "^1.0.0" 1723 + } 1724 + }, 1725 + "node_modules/exit-hook": { 1726 + "version": "2.2.1", 1727 + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 1728 + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 1729 + "dev": true, 1730 + "license": "MIT", 1731 + "engines": { 1732 + "node": ">=6" 1733 + }, 1734 + "funding": { 1735 + "url": "https://github.com/sponsors/sindresorhus" 1736 + } 1737 + }, 1738 + "node_modules/expect-type": { 1739 + "version": "1.2.1", 1740 + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", 1741 + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", 1742 + "dev": true, 1743 + "license": "Apache-2.0", 1744 + "engines": { 1745 + "node": ">=12.0.0" 1746 + } 1747 + }, 1748 + "node_modules/exsolve": { 1749 + "version": "1.0.5", 1750 + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz", 1751 + "integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==", 1752 + "dev": true, 1753 + "license": "MIT" 1754 + }, 1755 + "node_modules/fdir": { 1756 + "version": "6.4.4", 1757 + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", 1758 + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", 1759 + "dev": true, 1760 + "license": "MIT", 1761 + "peerDependencies": { 1762 + "picomatch": "^3 || ^4" 1763 + }, 1764 + "peerDependenciesMeta": { 1765 + "picomatch": { 1766 + "optional": true 1767 + } 1768 + } 1769 + }, 1770 + "node_modules/fsevents": { 1771 + "version": "2.3.3", 1772 + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 1773 + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 1774 + "dev": true, 1775 + "hasInstallScript": true, 1776 + "license": "MIT", 1777 + "optional": true, 1778 + "os": [ 1779 + "darwin" 1780 + ], 1781 + "engines": { 1782 + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 1783 + } 1784 + }, 1785 + "node_modules/get-source": { 1786 + "version": "2.0.12", 1787 + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", 1788 + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", 1789 + "dev": true, 1790 + "license": "Unlicense", 1791 + "dependencies": { 1792 + "data-uri-to-buffer": "^2.0.0", 1793 + "source-map": "^0.6.1" 1794 + } 1795 + }, 1796 + "node_modules/glob-to-regexp": { 1797 + "version": "0.4.1", 1798 + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 1799 + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 1800 + "dev": true, 1801 + "license": "BSD-2-Clause" 1802 + }, 1803 + "node_modules/is-arrayish": { 1804 + "version": "0.3.2", 1805 + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 1806 + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 1807 + "dev": true, 1808 + "license": "MIT", 1809 + "optional": true 1810 + }, 1811 + "node_modules/loupe": { 1812 + "version": "3.1.3", 1813 + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", 1814 + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", 1815 + "dev": true, 1816 + "license": "MIT" 1817 + }, 1818 + "node_modules/magic-string": { 1819 + "version": "0.30.17", 1820 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 1821 + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 1822 + "dev": true, 1823 + "license": "MIT", 1824 + "dependencies": { 1825 + "@jridgewell/sourcemap-codec": "^1.5.0" 1826 + } 1827 + }, 1828 + "node_modules/mime": { 1829 + "version": "3.0.0", 1830 + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 1831 + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 1832 + "dev": true, 1833 + "license": "MIT", 1834 + "bin": { 1835 + "mime": "cli.js" 1836 + }, 1837 + "engines": { 1838 + "node": ">=10.0.0" 1839 + } 1840 + }, 1841 + "node_modules/miniflare": { 1842 + "version": "4.20250428.1", 1843 + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250428.1.tgz", 1844 + "integrity": "sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==", 1845 + "dev": true, 1846 + "license": "MIT", 1847 + "dependencies": { 1848 + "@cspotcode/source-map-support": "0.8.1", 1849 + "acorn": "8.14.0", 1850 + "acorn-walk": "8.3.2", 1851 + "exit-hook": "2.2.1", 1852 + "glob-to-regexp": "0.4.1", 1853 + "stoppable": "1.1.0", 1854 + "undici": "^5.28.5", 1855 + "workerd": "1.20250428.0", 1856 + "ws": "8.18.0", 1857 + "youch": "3.3.4", 1858 + "zod": "3.22.3" 1859 + }, 1860 + "bin": { 1861 + "miniflare": "bootstrap.js" 1862 + }, 1863 + "engines": { 1864 + "node": ">=18.0.0" 1865 + } 1866 + }, 1867 + "node_modules/miniflare/node_modules/zod": { 1868 + "version": "3.22.3", 1869 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", 1870 + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", 1871 + "dev": true, 1872 + "license": "MIT", 1873 + "funding": { 1874 + "url": "https://github.com/sponsors/colinhacks" 1875 + } 1876 + }, 1877 + "node_modules/ms": { 1878 + "version": "2.1.3", 1879 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1880 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1881 + "dev": true, 1882 + "license": "MIT" 1883 + }, 1884 + "node_modules/mustache": { 1885 + "version": "4.2.0", 1886 + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 1887 + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 1888 + "dev": true, 1889 + "license": "MIT", 1890 + "bin": { 1891 + "mustache": "bin/mustache" 1892 + } 1893 + }, 1894 + "node_modules/nanoid": { 1895 + "version": "3.3.11", 1896 + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", 1897 + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 1898 + "dev": true, 1899 + "funding": [ 1900 + { 1901 + "type": "github", 1902 + "url": "https://github.com/sponsors/ai" 1903 + } 1904 + ], 1905 + "license": "MIT", 1906 + "bin": { 1907 + "nanoid": "bin/nanoid.cjs" 1908 + }, 1909 + "engines": { 1910 + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1911 + } 1912 + }, 1913 + "node_modules/ohash": { 1914 + "version": "2.0.11", 1915 + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", 1916 + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", 1917 + "dev": true, 1918 + "license": "MIT" 1919 + }, 1920 + "node_modules/path-to-regexp": { 1921 + "version": "6.3.0", 1922 + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 1923 + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 1924 + "dev": true, 1925 + "license": "MIT" 1926 + }, 1927 + "node_modules/pathe": { 1928 + "version": "2.0.3", 1929 + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 1930 + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 1931 + "dev": true, 1932 + "license": "MIT" 1933 + }, 1934 + "node_modules/pathval": { 1935 + "version": "2.0.0", 1936 + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 1937 + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 1938 + "dev": true, 1939 + "license": "MIT", 1940 + "engines": { 1941 + "node": ">= 14.16" 1942 + } 1943 + }, 1944 + "node_modules/picocolors": { 1945 + "version": "1.1.1", 1946 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1947 + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1948 + "dev": true, 1949 + "license": "ISC" 1950 + }, 1951 + "node_modules/picomatch": { 1952 + "version": "4.0.2", 1953 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 1954 + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 1955 + "dev": true, 1956 + "license": "MIT", 1957 + "engines": { 1958 + "node": ">=12" 1959 + }, 1960 + "funding": { 1961 + "url": "https://github.com/sponsors/jonschlinkert" 1962 + } 1963 + }, 1964 + "node_modules/postcss": { 1965 + "version": "8.5.3", 1966 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", 1967 + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", 1968 + "dev": true, 1969 + "funding": [ 1970 + { 1971 + "type": "opencollective", 1972 + "url": "https://opencollective.com/postcss/" 1973 + }, 1974 + { 1975 + "type": "tidelift", 1976 + "url": "https://tidelift.com/funding/github/npm/postcss" 1977 + }, 1978 + { 1979 + "type": "github", 1980 + "url": "https://github.com/sponsors/ai" 1981 + } 1982 + ], 1983 + "license": "MIT", 1984 + "dependencies": { 1985 + "nanoid": "^3.3.8", 1986 + "picocolors": "^1.1.1", 1987 + "source-map-js": "^1.2.1" 1988 + }, 1989 + "engines": { 1990 + "node": "^10 || ^12 || >=14" 1991 + } 1992 + }, 1993 + "node_modules/printable-characters": { 1994 + "version": "1.0.42", 1995 + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", 1996 + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", 1997 + "dev": true, 1998 + "license": "Unlicense" 1999 + }, 2000 + "node_modules/rollup": { 2001 + "version": "4.40.1", 2002 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz", 2003 + "integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==", 2004 + "dev": true, 2005 + "license": "MIT", 2006 + "dependencies": { 2007 + "@types/estree": "1.0.7" 2008 + }, 2009 + "bin": { 2010 + "rollup": "dist/bin/rollup" 2011 + }, 2012 + "engines": { 2013 + "node": ">=18.0.0", 2014 + "npm": ">=8.0.0" 2015 + }, 2016 + "optionalDependencies": { 2017 + "@rollup/rollup-android-arm-eabi": "4.40.1", 2018 + "@rollup/rollup-android-arm64": "4.40.1", 2019 + "@rollup/rollup-darwin-arm64": "4.40.1", 2020 + "@rollup/rollup-darwin-x64": "4.40.1", 2021 + "@rollup/rollup-freebsd-arm64": "4.40.1", 2022 + "@rollup/rollup-freebsd-x64": "4.40.1", 2023 + "@rollup/rollup-linux-arm-gnueabihf": "4.40.1", 2024 + "@rollup/rollup-linux-arm-musleabihf": "4.40.1", 2025 + "@rollup/rollup-linux-arm64-gnu": "4.40.1", 2026 + "@rollup/rollup-linux-arm64-musl": "4.40.1", 2027 + "@rollup/rollup-linux-loongarch64-gnu": "4.40.1", 2028 + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.1", 2029 + "@rollup/rollup-linux-riscv64-gnu": "4.40.1", 2030 + "@rollup/rollup-linux-riscv64-musl": "4.40.1", 2031 + "@rollup/rollup-linux-s390x-gnu": "4.40.1", 2032 + "@rollup/rollup-linux-x64-gnu": "4.40.1", 2033 + "@rollup/rollup-linux-x64-musl": "4.40.1", 2034 + "@rollup/rollup-win32-arm64-msvc": "4.40.1", 2035 + "@rollup/rollup-win32-ia32-msvc": "4.40.1", 2036 + "@rollup/rollup-win32-x64-msvc": "4.40.1", 2037 + "fsevents": "~2.3.2" 2038 + } 2039 + }, 2040 + "node_modules/semver": { 2041 + "version": "7.7.1", 2042 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 2043 + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 2044 + "dev": true, 2045 + "license": "ISC", 2046 + "bin": { 2047 + "semver": "bin/semver.js" 2048 + }, 2049 + "engines": { 2050 + "node": ">=10" 2051 + } 2052 + }, 2053 + "node_modules/sharp": { 2054 + "version": "0.33.5", 2055 + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", 2056 + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", 2057 + "dev": true, 2058 + "hasInstallScript": true, 2059 + "license": "Apache-2.0", 2060 + "optional": true, 2061 + "dependencies": { 2062 + "color": "^4.2.3", 2063 + "detect-libc": "^2.0.3", 2064 + "semver": "^7.6.3" 2065 + }, 2066 + "engines": { 2067 + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 2068 + }, 2069 + "funding": { 2070 + "url": "https://opencollective.com/libvips" 2071 + }, 2072 + "optionalDependencies": { 2073 + "@img/sharp-darwin-arm64": "0.33.5", 2074 + "@img/sharp-darwin-x64": "0.33.5", 2075 + "@img/sharp-libvips-darwin-arm64": "1.0.4", 2076 + "@img/sharp-libvips-darwin-x64": "1.0.4", 2077 + "@img/sharp-libvips-linux-arm": "1.0.5", 2078 + "@img/sharp-libvips-linux-arm64": "1.0.4", 2079 + "@img/sharp-libvips-linux-s390x": "1.0.4", 2080 + "@img/sharp-libvips-linux-x64": "1.0.4", 2081 + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", 2082 + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", 2083 + "@img/sharp-linux-arm": "0.33.5", 2084 + "@img/sharp-linux-arm64": "0.33.5", 2085 + "@img/sharp-linux-s390x": "0.33.5", 2086 + "@img/sharp-linux-x64": "0.33.5", 2087 + "@img/sharp-linuxmusl-arm64": "0.33.5", 2088 + "@img/sharp-linuxmusl-x64": "0.33.5", 2089 + "@img/sharp-wasm32": "0.33.5", 2090 + "@img/sharp-win32-ia32": "0.33.5", 2091 + "@img/sharp-win32-x64": "0.33.5" 2092 + } 2093 + }, 2094 + "node_modules/siginfo": { 2095 + "version": "2.0.0", 2096 + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 2097 + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 2098 + "dev": true, 2099 + "license": "ISC" 2100 + }, 2101 + "node_modules/simple-swizzle": { 2102 + "version": "0.2.2", 2103 + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 2104 + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 2105 + "dev": true, 2106 + "license": "MIT", 2107 + "optional": true, 2108 + "dependencies": { 2109 + "is-arrayish": "^0.3.1" 2110 + } 2111 + }, 2112 + "node_modules/source-map": { 2113 + "version": "0.6.1", 2114 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 2115 + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 2116 + "dev": true, 2117 + "license": "BSD-3-Clause", 2118 + "engines": { 2119 + "node": ">=0.10.0" 2120 + } 2121 + }, 2122 + "node_modules/source-map-js": { 2123 + "version": "1.2.1", 2124 + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2125 + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2126 + "dev": true, 2127 + "license": "BSD-3-Clause", 2128 + "engines": { 2129 + "node": ">=0.10.0" 2130 + } 2131 + }, 2132 + "node_modules/stackback": { 2133 + "version": "0.0.2", 2134 + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 2135 + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 2136 + "dev": true, 2137 + "license": "MIT" 2138 + }, 2139 + "node_modules/stacktracey": { 2140 + "version": "2.1.8", 2141 + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", 2142 + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", 2143 + "dev": true, 2144 + "license": "Unlicense", 2145 + "dependencies": { 2146 + "as-table": "^1.0.36", 2147 + "get-source": "^2.0.12" 2148 + } 2149 + }, 2150 + "node_modules/std-env": { 2151 + "version": "3.9.0", 2152 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", 2153 + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", 2154 + "dev": true, 2155 + "license": "MIT" 2156 + }, 2157 + "node_modules/stoppable": { 2158 + "version": "1.1.0", 2159 + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 2160 + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 2161 + "dev": true, 2162 + "license": "MIT", 2163 + "engines": { 2164 + "node": ">=4", 2165 + "npm": ">=6" 2166 + } 2167 + }, 2168 + "node_modules/tinybench": { 2169 + "version": "2.9.0", 2170 + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 2171 + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 2172 + "dev": true, 2173 + "license": "MIT" 2174 + }, 2175 + "node_modules/tinyexec": { 2176 + "version": "0.3.2", 2177 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", 2178 + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", 2179 + "dev": true, 2180 + "license": "MIT" 2181 + }, 2182 + "node_modules/tinyglobby": { 2183 + "version": "0.2.13", 2184 + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", 2185 + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", 2186 + "dev": true, 2187 + "license": "MIT", 2188 + "dependencies": { 2189 + "fdir": "^6.4.4", 2190 + "picomatch": "^4.0.2" 2191 + }, 2192 + "engines": { 2193 + "node": ">=12.0.0" 2194 + }, 2195 + "funding": { 2196 + "url": "https://github.com/sponsors/SuperchupuDev" 2197 + } 2198 + }, 2199 + "node_modules/tinypool": { 2200 + "version": "1.0.2", 2201 + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", 2202 + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", 2203 + "dev": true, 2204 + "license": "MIT", 2205 + "engines": { 2206 + "node": "^18.0.0 || >=20.0.0" 2207 + } 2208 + }, 2209 + "node_modules/tinyrainbow": { 2210 + "version": "2.0.0", 2211 + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", 2212 + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", 2213 + "dev": true, 2214 + "license": "MIT", 2215 + "engines": { 2216 + "node": ">=14.0.0" 2217 + } 2218 + }, 2219 + "node_modules/tinyspy": { 2220 + "version": "3.0.2", 2221 + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", 2222 + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", 2223 + "dev": true, 2224 + "license": "MIT", 2225 + "engines": { 2226 + "node": ">=14.0.0" 2227 + } 2228 + }, 2229 + "node_modules/tslib": { 2230 + "version": "2.8.1", 2231 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2232 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2233 + "dev": true, 2234 + "license": "0BSD", 2235 + "optional": true 2236 + }, 2237 + "node_modules/ufo": { 2238 + "version": "1.6.1", 2239 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", 2240 + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", 2241 + "dev": true, 2242 + "license": "MIT" 2243 + }, 2244 + "node_modules/undici": { 2245 + "version": "5.29.0", 2246 + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 2247 + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 2248 + "dev": true, 2249 + "license": "MIT", 2250 + "dependencies": { 2251 + "@fastify/busboy": "^2.0.0" 2252 + }, 2253 + "engines": { 2254 + "node": ">=14.0" 2255 + } 2256 + }, 2257 + "node_modules/unenv": { 2258 + "version": "2.0.0-rc.15", 2259 + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz", 2260 + "integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==", 2261 + "dev": true, 2262 + "license": "MIT", 2263 + "dependencies": { 2264 + "defu": "^6.1.4", 2265 + "exsolve": "^1.0.4", 2266 + "ohash": "^2.0.11", 2267 + "pathe": "^2.0.3", 2268 + "ufo": "^1.5.4" 2269 + } 2270 + }, 2271 + "node_modules/vite": { 2272 + "version": "6.3.4", 2273 + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz", 2274 + "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==", 2275 + "dev": true, 2276 + "license": "MIT", 2277 + "dependencies": { 2278 + "esbuild": "^0.25.0", 2279 + "fdir": "^6.4.4", 2280 + "picomatch": "^4.0.2", 2281 + "postcss": "^8.5.3", 2282 + "rollup": "^4.34.9", 2283 + "tinyglobby": "^0.2.13" 2284 + }, 2285 + "bin": { 2286 + "vite": "bin/vite.js" 2287 + }, 2288 + "engines": { 2289 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2290 + }, 2291 + "funding": { 2292 + "url": "https://github.com/vitejs/vite?sponsor=1" 2293 + }, 2294 + "optionalDependencies": { 2295 + "fsevents": "~2.3.3" 2296 + }, 2297 + "peerDependencies": { 2298 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2299 + "jiti": ">=1.21.0", 2300 + "less": "*", 2301 + "lightningcss": "^1.21.0", 2302 + "sass": "*", 2303 + "sass-embedded": "*", 2304 + "stylus": "*", 2305 + "sugarss": "*", 2306 + "terser": "^5.16.0", 2307 + "tsx": "^4.8.1", 2308 + "yaml": "^2.4.2" 2309 + }, 2310 + "peerDependenciesMeta": { 2311 + "@types/node": { 2312 + "optional": true 2313 + }, 2314 + "jiti": { 2315 + "optional": true 2316 + }, 2317 + "less": { 2318 + "optional": true 2319 + }, 2320 + "lightningcss": { 2321 + "optional": true 2322 + }, 2323 + "sass": { 2324 + "optional": true 2325 + }, 2326 + "sass-embedded": { 2327 + "optional": true 2328 + }, 2329 + "stylus": { 2330 + "optional": true 2331 + }, 2332 + "sugarss": { 2333 + "optional": true 2334 + }, 2335 + "terser": { 2336 + "optional": true 2337 + }, 2338 + "tsx": { 2339 + "optional": true 2340 + }, 2341 + "yaml": { 2342 + "optional": true 2343 + } 2344 + } 2345 + }, 2346 + "node_modules/vite-node": { 2347 + "version": "3.0.9", 2348 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", 2349 + "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", 2350 + "dev": true, 2351 + "license": "MIT", 2352 + "dependencies": { 2353 + "cac": "^6.7.14", 2354 + "debug": "^4.4.0", 2355 + "es-module-lexer": "^1.6.0", 2356 + "pathe": "^2.0.3", 2357 + "vite": "^5.0.0 || ^6.0.0" 2358 + }, 2359 + "bin": { 2360 + "vite-node": "vite-node.mjs" 2361 + }, 2362 + "engines": { 2363 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2364 + }, 2365 + "funding": { 2366 + "url": "https://opencollective.com/vitest" 2367 + } 2368 + }, 2369 + "node_modules/vitest": { 2370 + "version": "3.0.9", 2371 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", 2372 + "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", 2373 + "dev": true, 2374 + "license": "MIT", 2375 + "dependencies": { 2376 + "@vitest/expect": "3.0.9", 2377 + "@vitest/mocker": "3.0.9", 2378 + "@vitest/pretty-format": "^3.0.9", 2379 + "@vitest/runner": "3.0.9", 2380 + "@vitest/snapshot": "3.0.9", 2381 + "@vitest/spy": "3.0.9", 2382 + "@vitest/utils": "3.0.9", 2383 + "chai": "^5.2.0", 2384 + "debug": "^4.4.0", 2385 + "expect-type": "^1.1.0", 2386 + "magic-string": "^0.30.17", 2387 + "pathe": "^2.0.3", 2388 + "std-env": "^3.8.0", 2389 + "tinybench": "^2.9.0", 2390 + "tinyexec": "^0.3.2", 2391 + "tinypool": "^1.0.2", 2392 + "tinyrainbow": "^2.0.0", 2393 + "vite": "^5.0.0 || ^6.0.0", 2394 + "vite-node": "3.0.9", 2395 + "why-is-node-running": "^2.3.0" 2396 + }, 2397 + "bin": { 2398 + "vitest": "vitest.mjs" 2399 + }, 2400 + "engines": { 2401 + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" 2402 + }, 2403 + "funding": { 2404 + "url": "https://opencollective.com/vitest" 2405 + }, 2406 + "peerDependencies": { 2407 + "@edge-runtime/vm": "*", 2408 + "@types/debug": "^4.1.12", 2409 + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", 2410 + "@vitest/browser": "3.0.9", 2411 + "@vitest/ui": "3.0.9", 2412 + "happy-dom": "*", 2413 + "jsdom": "*" 2414 + }, 2415 + "peerDependenciesMeta": { 2416 + "@edge-runtime/vm": { 2417 + "optional": true 2418 + }, 2419 + "@types/debug": { 2420 + "optional": true 2421 + }, 2422 + "@types/node": { 2423 + "optional": true 2424 + }, 2425 + "@vitest/browser": { 2426 + "optional": true 2427 + }, 2428 + "@vitest/ui": { 2429 + "optional": true 2430 + }, 2431 + "happy-dom": { 2432 + "optional": true 2433 + }, 2434 + "jsdom": { 2435 + "optional": true 2436 + } 2437 + } 2438 + }, 2439 + "node_modules/why-is-node-running": { 2440 + "version": "2.3.0", 2441 + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 2442 + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 2443 + "dev": true, 2444 + "license": "MIT", 2445 + "dependencies": { 2446 + "siginfo": "^2.0.0", 2447 + "stackback": "0.0.2" 2448 + }, 2449 + "bin": { 2450 + "why-is-node-running": "cli.js" 2451 + }, 2452 + "engines": { 2453 + "node": ">=8" 2454 + } 2455 + }, 2456 + "node_modules/workerd": { 2457 + "version": "1.20250428.0", 2458 + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250428.0.tgz", 2459 + "integrity": "sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==", 2460 + "dev": true, 2461 + "hasInstallScript": true, 2462 + "license": "Apache-2.0", 2463 + "bin": { 2464 + "workerd": "bin/workerd" 2465 + }, 2466 + "engines": { 2467 + "node": ">=16" 2468 + }, 2469 + "optionalDependencies": { 2470 + "@cloudflare/workerd-darwin-64": "1.20250428.0", 2471 + "@cloudflare/workerd-darwin-arm64": "1.20250428.0", 2472 + "@cloudflare/workerd-linux-64": "1.20250428.0", 2473 + "@cloudflare/workerd-linux-arm64": "1.20250428.0", 2474 + "@cloudflare/workerd-windows-64": "1.20250428.0" 2475 + } 2476 + }, 2477 + "node_modules/wrangler": { 2478 + "version": "4.14.1", 2479 + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.14.1.tgz", 2480 + "integrity": "sha512-EU7IThP7i68TBftJJSveogvWZ5k/WRijcJh3UclDWiWWhDZTPbL6LOJEFhHKqFzHOaC4Y2Aewt48rfTz0e7oCw==", 2481 + "dev": true, 2482 + "license": "MIT OR Apache-2.0", 2483 + "dependencies": { 2484 + "@cloudflare/kv-asset-handler": "0.4.0", 2485 + "@cloudflare/unenv-preset": "2.3.1", 2486 + "blake3-wasm": "2.1.5", 2487 + "esbuild": "0.25.2", 2488 + "miniflare": "4.20250428.1", 2489 + "path-to-regexp": "6.3.0", 2490 + "unenv": "2.0.0-rc.15", 2491 + "workerd": "1.20250428.0" 2492 + }, 2493 + "bin": { 2494 + "wrangler": "bin/wrangler.js", 2495 + "wrangler2": "bin/wrangler.js" 2496 + }, 2497 + "engines": { 2498 + "node": ">=18.0.0" 2499 + }, 2500 + "optionalDependencies": { 2501 + "fsevents": "~2.3.2", 2502 + "sharp": "^0.33.5" 2503 + }, 2504 + "peerDependencies": { 2505 + "@cloudflare/workers-types": "^4.20250428.0" 2506 + }, 2507 + "peerDependenciesMeta": { 2508 + "@cloudflare/workers-types": { 2509 + "optional": true 2510 + } 2511 + } 2512 + }, 2513 + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { 2514 + "version": "0.25.2", 2515 + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", 2516 + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", 2517 + "cpu": [ 2518 + "ppc64" 2519 + ], 2520 + "dev": true, 2521 + "license": "MIT", 2522 + "optional": true, 2523 + "os": [ 2524 + "aix" 2525 + ], 2526 + "engines": { 2527 + "node": ">=18" 2528 + } 2529 + }, 2530 + "node_modules/wrangler/node_modules/@esbuild/android-arm": { 2531 + "version": "0.25.2", 2532 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", 2533 + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", 2534 + "cpu": [ 2535 + "arm" 2536 + ], 2537 + "dev": true, 2538 + "license": "MIT", 2539 + "optional": true, 2540 + "os": [ 2541 + "android" 2542 + ], 2543 + "engines": { 2544 + "node": ">=18" 2545 + } 2546 + }, 2547 + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { 2548 + "version": "0.25.2", 2549 + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", 2550 + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", 2551 + "cpu": [ 2552 + "arm64" 2553 + ], 2554 + "dev": true, 2555 + "license": "MIT", 2556 + "optional": true, 2557 + "os": [ 2558 + "android" 2559 + ], 2560 + "engines": { 2561 + "node": ">=18" 2562 + } 2563 + }, 2564 + "node_modules/wrangler/node_modules/@esbuild/android-x64": { 2565 + "version": "0.25.2", 2566 + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", 2567 + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", 2568 + "cpu": [ 2569 + "x64" 2570 + ], 2571 + "dev": true, 2572 + "license": "MIT", 2573 + "optional": true, 2574 + "os": [ 2575 + "android" 2576 + ], 2577 + "engines": { 2578 + "node": ">=18" 2579 + } 2580 + }, 2581 + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { 2582 + "version": "0.25.2", 2583 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", 2584 + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", 2585 + "cpu": [ 2586 + "arm64" 2587 + ], 2588 + "dev": true, 2589 + "license": "MIT", 2590 + "optional": true, 2591 + "os": [ 2592 + "darwin" 2593 + ], 2594 + "engines": { 2595 + "node": ">=18" 2596 + } 2597 + }, 2598 + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { 2599 + "version": "0.25.2", 2600 + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", 2601 + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", 2602 + "cpu": [ 2603 + "x64" 2604 + ], 2605 + "dev": true, 2606 + "license": "MIT", 2607 + "optional": true, 2608 + "os": [ 2609 + "darwin" 2610 + ], 2611 + "engines": { 2612 + "node": ">=18" 2613 + } 2614 + }, 2615 + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { 2616 + "version": "0.25.2", 2617 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", 2618 + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", 2619 + "cpu": [ 2620 + "arm64" 2621 + ], 2622 + "dev": true, 2623 + "license": "MIT", 2624 + "optional": true, 2625 + "os": [ 2626 + "freebsd" 2627 + ], 2628 + "engines": { 2629 + "node": ">=18" 2630 + } 2631 + }, 2632 + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { 2633 + "version": "0.25.2", 2634 + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", 2635 + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", 2636 + "cpu": [ 2637 + "x64" 2638 + ], 2639 + "dev": true, 2640 + "license": "MIT", 2641 + "optional": true, 2642 + "os": [ 2643 + "freebsd" 2644 + ], 2645 + "engines": { 2646 + "node": ">=18" 2647 + } 2648 + }, 2649 + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { 2650 + "version": "0.25.2", 2651 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", 2652 + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", 2653 + "cpu": [ 2654 + "arm" 2655 + ], 2656 + "dev": true, 2657 + "license": "MIT", 2658 + "optional": true, 2659 + "os": [ 2660 + "linux" 2661 + ], 2662 + "engines": { 2663 + "node": ">=18" 2664 + } 2665 + }, 2666 + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { 2667 + "version": "0.25.2", 2668 + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", 2669 + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", 2670 + "cpu": [ 2671 + "arm64" 2672 + ], 2673 + "dev": true, 2674 + "license": "MIT", 2675 + "optional": true, 2676 + "os": [ 2677 + "linux" 2678 + ], 2679 + "engines": { 2680 + "node": ">=18" 2681 + } 2682 + }, 2683 + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { 2684 + "version": "0.25.2", 2685 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", 2686 + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", 2687 + "cpu": [ 2688 + "ia32" 2689 + ], 2690 + "dev": true, 2691 + "license": "MIT", 2692 + "optional": true, 2693 + "os": [ 2694 + "linux" 2695 + ], 2696 + "engines": { 2697 + "node": ">=18" 2698 + } 2699 + }, 2700 + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { 2701 + "version": "0.25.2", 2702 + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", 2703 + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", 2704 + "cpu": [ 2705 + "loong64" 2706 + ], 2707 + "dev": true, 2708 + "license": "MIT", 2709 + "optional": true, 2710 + "os": [ 2711 + "linux" 2712 + ], 2713 + "engines": { 2714 + "node": ">=18" 2715 + } 2716 + }, 2717 + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { 2718 + "version": "0.25.2", 2719 + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", 2720 + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", 2721 + "cpu": [ 2722 + "mips64el" 2723 + ], 2724 + "dev": true, 2725 + "license": "MIT", 2726 + "optional": true, 2727 + "os": [ 2728 + "linux" 2729 + ], 2730 + "engines": { 2731 + "node": ">=18" 2732 + } 2733 + }, 2734 + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { 2735 + "version": "0.25.2", 2736 + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", 2737 + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", 2738 + "cpu": [ 2739 + "ppc64" 2740 + ], 2741 + "dev": true, 2742 + "license": "MIT", 2743 + "optional": true, 2744 + "os": [ 2745 + "linux" 2746 + ], 2747 + "engines": { 2748 + "node": ">=18" 2749 + } 2750 + }, 2751 + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { 2752 + "version": "0.25.2", 2753 + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", 2754 + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", 2755 + "cpu": [ 2756 + "riscv64" 2757 + ], 2758 + "dev": true, 2759 + "license": "MIT", 2760 + "optional": true, 2761 + "os": [ 2762 + "linux" 2763 + ], 2764 + "engines": { 2765 + "node": ">=18" 2766 + } 2767 + }, 2768 + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { 2769 + "version": "0.25.2", 2770 + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", 2771 + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", 2772 + "cpu": [ 2773 + "s390x" 2774 + ], 2775 + "dev": true, 2776 + "license": "MIT", 2777 + "optional": true, 2778 + "os": [ 2779 + "linux" 2780 + ], 2781 + "engines": { 2782 + "node": ">=18" 2783 + } 2784 + }, 2785 + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { 2786 + "version": "0.25.2", 2787 + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", 2788 + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", 2789 + "cpu": [ 2790 + "x64" 2791 + ], 2792 + "dev": true, 2793 + "license": "MIT", 2794 + "optional": true, 2795 + "os": [ 2796 + "linux" 2797 + ], 2798 + "engines": { 2799 + "node": ">=18" 2800 + } 2801 + }, 2802 + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { 2803 + "version": "0.25.2", 2804 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", 2805 + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", 2806 + "cpu": [ 2807 + "arm64" 2808 + ], 2809 + "dev": true, 2810 + "license": "MIT", 2811 + "optional": true, 2812 + "os": [ 2813 + "netbsd" 2814 + ], 2815 + "engines": { 2816 + "node": ">=18" 2817 + } 2818 + }, 2819 + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { 2820 + "version": "0.25.2", 2821 + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", 2822 + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", 2823 + "cpu": [ 2824 + "x64" 2825 + ], 2826 + "dev": true, 2827 + "license": "MIT", 2828 + "optional": true, 2829 + "os": [ 2830 + "netbsd" 2831 + ], 2832 + "engines": { 2833 + "node": ">=18" 2834 + } 2835 + }, 2836 + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { 2837 + "version": "0.25.2", 2838 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", 2839 + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", 2840 + "cpu": [ 2841 + "arm64" 2842 + ], 2843 + "dev": true, 2844 + "license": "MIT", 2845 + "optional": true, 2846 + "os": [ 2847 + "openbsd" 2848 + ], 2849 + "engines": { 2850 + "node": ">=18" 2851 + } 2852 + }, 2853 + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { 2854 + "version": "0.25.2", 2855 + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", 2856 + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", 2857 + "cpu": [ 2858 + "x64" 2859 + ], 2860 + "dev": true, 2861 + "license": "MIT", 2862 + "optional": true, 2863 + "os": [ 2864 + "openbsd" 2865 + ], 2866 + "engines": { 2867 + "node": ">=18" 2868 + } 2869 + }, 2870 + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { 2871 + "version": "0.25.2", 2872 + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", 2873 + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", 2874 + "cpu": [ 2875 + "x64" 2876 + ], 2877 + "dev": true, 2878 + "license": "MIT", 2879 + "optional": true, 2880 + "os": [ 2881 + "sunos" 2882 + ], 2883 + "engines": { 2884 + "node": ">=18" 2885 + } 2886 + }, 2887 + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { 2888 + "version": "0.25.2", 2889 + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", 2890 + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", 2891 + "cpu": [ 2892 + "arm64" 2893 + ], 2894 + "dev": true, 2895 + "license": "MIT", 2896 + "optional": true, 2897 + "os": [ 2898 + "win32" 2899 + ], 2900 + "engines": { 2901 + "node": ">=18" 2902 + } 2903 + }, 2904 + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { 2905 + "version": "0.25.2", 2906 + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", 2907 + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", 2908 + "cpu": [ 2909 + "ia32" 2910 + ], 2911 + "dev": true, 2912 + "license": "MIT", 2913 + "optional": true, 2914 + "os": [ 2915 + "win32" 2916 + ], 2917 + "engines": { 2918 + "node": ">=18" 2919 + } 2920 + }, 2921 + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { 2922 + "version": "0.25.2", 2923 + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", 2924 + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", 2925 + "cpu": [ 2926 + "x64" 2927 + ], 2928 + "dev": true, 2929 + "license": "MIT", 2930 + "optional": true, 2931 + "os": [ 2932 + "win32" 2933 + ], 2934 + "engines": { 2935 + "node": ">=18" 2936 + } 2937 + }, 2938 + "node_modules/wrangler/node_modules/esbuild": { 2939 + "version": "0.25.2", 2940 + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", 2941 + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", 2942 + "dev": true, 2943 + "hasInstallScript": true, 2944 + "license": "MIT", 2945 + "bin": { 2946 + "esbuild": "bin/esbuild" 2947 + }, 2948 + "engines": { 2949 + "node": ">=18" 2950 + }, 2951 + "optionalDependencies": { 2952 + "@esbuild/aix-ppc64": "0.25.2", 2953 + "@esbuild/android-arm": "0.25.2", 2954 + "@esbuild/android-arm64": "0.25.2", 2955 + "@esbuild/android-x64": "0.25.2", 2956 + "@esbuild/darwin-arm64": "0.25.2", 2957 + "@esbuild/darwin-x64": "0.25.2", 2958 + "@esbuild/freebsd-arm64": "0.25.2", 2959 + "@esbuild/freebsd-x64": "0.25.2", 2960 + "@esbuild/linux-arm": "0.25.2", 2961 + "@esbuild/linux-arm64": "0.25.2", 2962 + "@esbuild/linux-ia32": "0.25.2", 2963 + "@esbuild/linux-loong64": "0.25.2", 2964 + "@esbuild/linux-mips64el": "0.25.2", 2965 + "@esbuild/linux-ppc64": "0.25.2", 2966 + "@esbuild/linux-riscv64": "0.25.2", 2967 + "@esbuild/linux-s390x": "0.25.2", 2968 + "@esbuild/linux-x64": "0.25.2", 2969 + "@esbuild/netbsd-arm64": "0.25.2", 2970 + "@esbuild/netbsd-x64": "0.25.2", 2971 + "@esbuild/openbsd-arm64": "0.25.2", 2972 + "@esbuild/openbsd-x64": "0.25.2", 2973 + "@esbuild/sunos-x64": "0.25.2", 2974 + "@esbuild/win32-arm64": "0.25.2", 2975 + "@esbuild/win32-ia32": "0.25.2", 2976 + "@esbuild/win32-x64": "0.25.2" 2977 + } 2978 + }, 2979 + "node_modules/ws": { 2980 + "version": "8.18.0", 2981 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 2982 + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 2983 + "dev": true, 2984 + "license": "MIT", 2985 + "engines": { 2986 + "node": ">=10.0.0" 2987 + }, 2988 + "peerDependencies": { 2989 + "bufferutil": "^4.0.1", 2990 + "utf-8-validate": ">=5.0.2" 2991 + }, 2992 + "peerDependenciesMeta": { 2993 + "bufferutil": { 2994 + "optional": true 2995 + }, 2996 + "utf-8-validate": { 2997 + "optional": true 2998 + } 2999 + } 3000 + }, 3001 + "node_modules/youch": { 3002 + "version": "3.3.4", 3003 + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", 3004 + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", 3005 + "dev": true, 3006 + "license": "MIT", 3007 + "dependencies": { 3008 + "cookie": "^0.7.1", 3009 + "mustache": "^4.2.0", 3010 + "stacktracey": "^2.1.8" 3011 + } 3012 + }, 3013 + "node_modules/zod": { 3014 + "version": "3.24.3", 3015 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", 3016 + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", 3017 + "dev": true, 3018 + "license": "MIT", 3019 + "funding": { 3020 + "url": "https://github.com/sponsors/colinhacks" 3021 + } 3022 + } 3023 + } 3024 + }
+16
avatar/package.json
··· 1 + { 2 + "name": "avatar", 3 + "version": "0.0.0", 4 + "private": true, 5 + "scripts": { 6 + "deploy": "wrangler deploy", 7 + "dev": "wrangler dev", 8 + "start": "wrangler dev", 9 + "test": "vitest" 10 + }, 11 + "devDependencies": { 12 + "@cloudflare/vitest-pool-workers": "^0.8.19", 13 + "vitest": "~3.0.7", 14 + "wrangler": "^4.14.1" 15 + } 16 + }
+11
avatar/readme.md
··· 1 + # avatar 2 + 3 + avatar is a small service that fetches your pretty Bluesky avatar and caches it on Cloudflare. 4 + It uses a shared secret `AVATAR_SHARED_SECRET` to ensure requests only originate from the trusted appview. 5 + 6 + It's deployed using `wrangler` like so: 7 + 8 + ``` 9 + npx wrangler deploy 10 + npx wrangler secrets put AVATAR_SHARED_SECRET 11 + ```
+88
avatar/src/index.js
··· 1 + export default { 2 + async fetch(request, env) { 3 + const url = new URL(request.url); 4 + const { pathname } = url; 5 + 6 + if (!pathname || pathname === '/') { 7 + return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare. 8 + You can't use this directly unforunately since all requests are signed and may only originate from the appview.`); 9 + } 10 + 11 + const cache = caches.default; 12 + 13 + let cacheKey = request.url; 14 + let response = await cache.match(cacheKey); 15 + if (response) { 16 + return response; 17 + } 18 + 19 + const pathParts = pathname.slice(1).split('/'); 20 + if (pathParts.length < 2) { 21 + return new Response('Bad URL', { status: 400 }); 22 + } 23 + 24 + const [signatureHex, actor] = pathParts; 25 + 26 + const actorBytes = new TextEncoder().encode(actor); 27 + 28 + const key = await crypto.subtle.importKey( 29 + 'raw', 30 + new TextEncoder().encode(env.AVATAR_SHARED_SECRET), 31 + { name: 'HMAC', hash: 'SHA-256' }, 32 + false, 33 + ['sign', 'verify'], 34 + ); 35 + 36 + const computedSigBuffer = await crypto.subtle.sign('HMAC', key, actorBytes); 37 + const computedSig = Array.from(new Uint8Array(computedSigBuffer)) 38 + .map((b) => b.toString(16).padStart(2, '0')) 39 + .join(''); 40 + 41 + console.log({ 42 + level: 'debug', 43 + message: 'avatar request for: ' + actor, 44 + computedSignature: computedSig, 45 + providedSignature: signatureHex, 46 + }); 47 + 48 + const sigBytes = Uint8Array.from(signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16))); 49 + const valid = await crypto.subtle.verify('HMAC', key, sigBytes, actorBytes); 50 + 51 + if (!valid) { 52 + return new Response('Invalid signature', { status: 403 }); 53 + } 54 + 55 + try { 56 + const profileResponse = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`, { method: 'GET' }); 57 + const profile = await profileResponse.json(); 58 + const avatar = profile.avatar; 59 + 60 + if (!avatar) { 61 + return new Response(`avatar not found for ${actor}.`, { status: 404 }); 62 + } 63 + 64 + // fetch the actual avatar image 65 + const avatarResponse = await fetch(avatar); 66 + if (!avatarResponse.ok) { 67 + return new Response(`failed to fetch avatar for ${actor}.`, { status: avatarResponse.status }); 68 + } 69 + 70 + const avatarData = await avatarResponse.arrayBuffer(); 71 + const contentType = avatarResponse.headers.get('content-type') || 'image/jpeg'; 72 + 73 + response = new Response(avatarData, { 74 + headers: { 75 + 'Content-Type': contentType, 76 + 'Cache-Control': 'public, max-age=3600', 77 + }, 78 + }); 79 + 80 + // cache it in cf using request.url as the key 81 + await cache.put(cacheKey, response.clone()); 82 + 83 + return response; 84 + } catch (error) { 85 + return new Response(`error fetching avatar: ${error.message}`, { status: 500 }); 86 + } 87 + }, 88 + };
+15
avatar/wrangler.jsonc
··· 1 + { 2 + "$schema": "node_modules/wrangler/config-schema.json", 3 + "name": "avatar", 4 + "main": "src/index.js", 5 + "compatibility_date": "2025-05-03", 6 + "observability": { 7 + "enabled": true, 8 + }, 9 + "routes": [ 10 + { 11 + "pattern": "avatar.tangled.sh", 12 + "custom_domain": true, 13 + }, 14 + ], 15 + }
+174
camo/.gitignore
··· 1 + # Logs 2 + 3 + ./test.sh 4 + 5 + logs 6 + _.log 7 + npm-debug.log_ 8 + yarn-debug.log* 9 + yarn-error.log* 10 + lerna-debug.log* 11 + .pnpm-debug.log* 12 + 13 + # Diagnostic reports (https://nodejs.org/api/report.html) 14 + 15 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 + 17 + # Runtime data 18 + 19 + pids 20 + _.pid 21 + _.seed 22 + \*.pid.lock 23 + 24 + # Directory for instrumented libs generated by jscoverage/JSCover 25 + 26 + lib-cov 27 + 28 + # Coverage directory used by tools like istanbul 29 + 30 + coverage 31 + \*.lcov 32 + 33 + # nyc test coverage 34 + 35 + .nyc_output 36 + 37 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 + 39 + .grunt 40 + 41 + # Bower dependency directory (https://bower.io/) 42 + 43 + bower_components 44 + 45 + # node-waf configuration 46 + 47 + .lock-wscript 48 + 49 + # Compiled binary addons (https://nodejs.org/api/addons.html) 50 + 51 + build/Release 52 + 53 + # Dependency directories 54 + 55 + node_modules/ 56 + jspm_packages/ 57 + 58 + # Snowpack dependency directory (https://snowpack.dev/) 59 + 60 + web_modules/ 61 + 62 + # TypeScript cache 63 + 64 + \*.tsbuildinfo 65 + 66 + # Optional npm cache directory 67 + 68 + .npm 69 + 70 + # Optional eslint cache 71 + 72 + .eslintcache 73 + 74 + # Optional stylelint cache 75 + 76 + .stylelintcache 77 + 78 + # Microbundle cache 79 + 80 + .rpt2_cache/ 81 + .rts2_cache_cjs/ 82 + .rts2_cache_es/ 83 + .rts2_cache_umd/ 84 + 85 + # Optional REPL history 86 + 87 + .node_repl_history 88 + 89 + # Output of 'npm pack' 90 + 91 + \*.tgz 92 + 93 + # Yarn Integrity file 94 + 95 + .yarn-integrity 96 + 97 + # dotenv environment variable files 98 + 99 + .env 100 + .env.development.local 101 + .env.test.local 102 + .env.production.local 103 + .env.local 104 + 105 + # parcel-bundler cache (https://parceljs.org/) 106 + 107 + .cache 108 + .parcel-cache 109 + 110 + # Next.js build output 111 + 112 + .next 113 + out 114 + 115 + # Nuxt.js build / generate output 116 + 117 + .nuxt 118 + dist 119 + 120 + # Gatsby files 121 + 122 + .cache/ 123 + 124 + # Comment in the public line in if your project uses Gatsby and not Next.js 125 + 126 + # https://nextjs.org/blog/next-9-1#public-directory-support 127 + 128 + # public 129 + 130 + # vuepress build output 131 + 132 + .vuepress/dist 133 + 134 + # vuepress v2.x temp and cache directory 135 + 136 + .temp 137 + .cache 138 + 139 + # Docusaurus cache and generated files 140 + 141 + .docusaurus 142 + 143 + # Serverless directories 144 + 145 + .serverless/ 146 + 147 + # FuseBox cache 148 + 149 + .fusebox/ 150 + 151 + # DynamoDB Local files 152 + 153 + .dynamodb/ 154 + 155 + # TernJS port file 156 + 157 + .tern-port 158 + 159 + # Stores VSCode versions used for testing VSCode extensions 160 + 161 + .vscode-test 162 + 163 + # yarn v2 164 + 165 + .yarn/cache 166 + .yarn/unplugged 167 + .yarn/build-state.yml 168 + .yarn/install-state.gz 169 + .pnp.\* 170 + 171 + # wrangler project 172 + 173 + .dev.vars 174 + .wrangler/
+3024
camo/package-lock.json
··· 1 + { 2 + "name": "camo", 3 + "version": "0.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "camo", 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 + } 3024 + }
+16
camo/package.json
··· 1 + { 2 + "name": "camo", 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 + } 16 + }
+17
camo/readme.md
··· 1 + # camo 2 + 3 + Camo is Tangled's "camouflage" service much like that of [GitHub's](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-anonymized-urls). 4 + 5 + Camo uses a shared secret `CAMO_SHARED_SECRET` to verify HMAC signatures. URLs are of the form: 6 + 7 + ``` 8 + https://camo.tangled.sh/<signature>/<hex-encoded-origin-url> 9 + ``` 10 + 11 + It's pretty barebones for the moment and doesn't support a whole lot of what the 12 + big G's does. Ours is a Cloudflare Worker, deployed using `wrangler` like so: 13 + 14 + ``` 15 + npx wrangler deploy 16 + npx wrangler secrets put CAMO_SHARED_SECRET 17 + ```
+101
camo/src/index.js
··· 1 + export default { 2 + async fetch(request, env) { 3 + const url = new URL(request.url); 4 + 5 + if (url.pathname === "/" || url.pathname === "") { 6 + return new Response( 7 + "This is Tangled's Camo service. It proxies images served from knots via Cloudflare.", 8 + ); 9 + } 10 + 11 + const cache = caches.default; 12 + 13 + const pathParts = url.pathname.slice(1).split("/"); 14 + if (pathParts.length < 2) { 15 + return new Response("Bad URL", { status: 400 }); 16 + } 17 + 18 + const [signatureHex, ...hexUrlParts] = pathParts; 19 + const hexUrl = hexUrlParts.join(""); 20 + const urlBytes = Uint8Array.from( 21 + hexUrl.match(/.{2}/g).map((b) => parseInt(b, 16)), 22 + ); 23 + const targetUrl = new TextDecoder().decode(urlBytes); 24 + 25 + // check if we have an entry in the cache with the target url 26 + let cacheKey = new Request(targetUrl); 27 + let response = await cache.match(cacheKey); 28 + if (response) { 29 + return response; 30 + } 31 + 32 + // else compute the signature 33 + const key = await crypto.subtle.importKey( 34 + "raw", 35 + new TextEncoder().encode(env.CAMO_SHARED_SECRET), 36 + { name: "HMAC", hash: "SHA-256" }, 37 + false, 38 + ["sign", "verify"], 39 + ); 40 + 41 + const computedSigBuffer = await crypto.subtle.sign("HMAC", key, urlBytes); 42 + const computedSig = Array.from(new Uint8Array(computedSigBuffer)) 43 + .map((b) => b.toString(16).padStart(2, "0")) 44 + .join(""); 45 + 46 + console.log({ 47 + level: "debug", 48 + message: "camo target: " + targetUrl, 49 + computedSignature: computedSig, 50 + providedSignature: signatureHex, 51 + targetUrl: targetUrl, 52 + }); 53 + 54 + const sigBytes = Uint8Array.from( 55 + signatureHex.match(/.{2}/g).map((b) => parseInt(b, 16)), 56 + ); 57 + const valid = await crypto.subtle.verify("HMAC", key, sigBytes, urlBytes); 58 + 59 + if (!valid) { 60 + return new Response("Invalid signature", { status: 403 }); 61 + } 62 + 63 + let parsedUrl; 64 + try { 65 + parsedUrl = new URL(targetUrl); 66 + if (!["https:", "http:"].includes(parsedUrl.protocol)) { 67 + return new Response("Only HTTP(S) allowed", { status: 400 }); 68 + } 69 + } catch { 70 + return new Response("Malformed URL", { status: 400 }); 71 + } 72 + 73 + // fetch from the parsed URL 74 + const res = await fetch(parsedUrl.toString(), { 75 + headers: { "User-Agent": "Tangled Camo v0.1.0" }, 76 + }); 77 + 78 + const allowedMimeTypes = require("./mimetypes.json"); 79 + 80 + const contentType = 81 + res.headers.get("Content-Type") || "application/octet-stream"; 82 + 83 + if (!allowedMimeTypes.includes(contentType.split(";")[0].trim())) { 84 + return new Response("Unsupported media type", { status: 415 }); 85 + } 86 + 87 + const headers = new Headers(); 88 + headers.set("Content-Type", contentType); 89 + headers.set("Cache-Control", "public, max-age=86400, immutable"); 90 + 91 + // serve and cache it with cf 92 + response = new Response(await res.arrayBuffer(), { 93 + status: res.status, 94 + headers, 95 + }); 96 + 97 + await cache.put(cacheKey, response.clone()); 98 + 99 + return response; 100 + }, 101 + };
+45
camo/src/mimetypes.json
··· 1 + [ 2 + "image/bmp", 3 + "image/cgm", 4 + "image/g3fax", 5 + "image/gif", 6 + "image/ief", 7 + "image/jp2", 8 + "image/jpeg", 9 + "image/jpg", 10 + "image/pict", 11 + "image/png", 12 + "image/prs.btif", 13 + "image/svg+xml", 14 + "image/tiff", 15 + "image/vnd.adobe.photoshop", 16 + "image/vnd.djvu", 17 + "image/vnd.dwg", 18 + "image/vnd.dxf", 19 + "image/vnd.fastbidsheet", 20 + "image/vnd.fpx", 21 + "image/vnd.fst", 22 + "image/vnd.fujixerox.edmics-mmr", 23 + "image/vnd.fujixerox.edmics-rlc", 24 + "image/vnd.microsoft.icon", 25 + "image/vnd.ms-modi", 26 + "image/vnd.net-fpx", 27 + "image/vnd.wap.wbmp", 28 + "image/vnd.xiff", 29 + "image/webp", 30 + "image/x-cmu-raster", 31 + "image/x-cmx", 32 + "image/x-icon", 33 + "image/x-macpaint", 34 + "image/x-pcx", 35 + "image/x-pict", 36 + "image/x-portable-anymap", 37 + "image/x-portable-bitmap", 38 + "image/x-portable-graymap", 39 + "image/x-portable-pixmap", 40 + "image/x-quicktime", 41 + "image/x-rgb", 42 + "image/x-xbitmap", 43 + "image/x-xpixmap", 44 + "image/x-xwindowdump" 45 + ]
+20
camo/wrangler.jsonc
··· 1 + /** 2 + * For more details on how to configure Wrangler, refer to: 3 + * https://developers.cloudflare.com/workers/wrangler/configuration/ 4 + */ 5 + { 6 + "$schema": "node_modules/wrangler/config-schema.json", 7 + "name": "camo", 8 + "main": "src/index.js", 9 + "compatibility_date": "2025-04-30", 10 + "observability": { 11 + "enabled": true, 12 + }, 13 + 14 + "routes": [ 15 + { 16 + "pattern": "camo.tangled.sh", 17 + "custom_domain": true, 18 + }, 19 + ], 20 + }
+2 -2
cmd/appview/main.go
··· 26 26 log.Fatal(err) 27 27 } 28 28 29 - log.Println("starting server on", c.ListenAddr) 30 - log.Println(http.ListenAndServe(c.ListenAddr, state.Router())) 29 + log.Println("starting server on", c.Core.ListenAddr) 30 + log.Println(http.ListenAndServe(c.Core.ListenAddr, state.Router())) 31 31 }
+15 -13
cmd/gen.go
··· 2 2 3 3 import ( 4 4 cbg "github.com/whyrusleeping/cbor-gen" 5 - shtangled "tangled.sh/tangled.sh/core/api/tangled" 5 + "tangled.sh/tangled.sh/core/api/tangled" 6 6 ) 7 7 8 8 func main() { ··· 14 14 if err := genCfg.WriteMapEncodersToFile( 15 15 "api/tangled/cbor_gen.go", 16 16 "tangled", 17 - shtangled.FeedStar{}, 18 - shtangled.GraphFollow{}, 19 - shtangled.KnotMember{}, 20 - shtangled.PublicKey{}, 21 - shtangled.RepoIssueComment{}, 22 - shtangled.RepoIssueState{}, 23 - shtangled.RepoIssue{}, 24 - shtangled.Repo{}, 25 - shtangled.RepoPull{}, 26 - shtangled.RepoPull_Source{}, 27 - shtangled.RepoPullStatus{}, 28 - shtangled.RepoPullComment{}, 17 + tangled.FeedStar{}, 18 + tangled.GraphFollow{}, 19 + tangled.KnotMember{}, 20 + tangled.PublicKey{}, 21 + tangled.RepoIssueComment{}, 22 + tangled.RepoIssueState{}, 23 + tangled.RepoIssue{}, 24 + tangled.Repo{}, 25 + tangled.RepoPull{}, 26 + tangled.RepoPull_Source{}, 27 + tangled.RepoPullStatus{}, 28 + tangled.RepoPullComment{}, 29 + tangled.RepoArtifact{}, 30 + tangled.ActorProfile{}, 29 31 ); err != nil { 30 32 panic(err) 31 33 }
+39
cmd/genjwks/main.go
··· 1 + // adapted from https://github.com/haileyok/atproto-oauth-golang 2 + 3 + package main 4 + 5 + import ( 6 + "crypto/ecdsa" 7 + "crypto/elliptic" 8 + "crypto/rand" 9 + "encoding/json" 10 + "fmt" 11 + "time" 12 + 13 + "github.com/lestrrat-go/jwx/v2/jwk" 14 + ) 15 + 16 + func main() { 17 + privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 18 + if err != nil { 19 + panic(err) 20 + } 21 + 22 + key, err := jwk.FromRaw(privKey) 23 + if err != nil { 24 + panic(err) 25 + } 26 + 27 + kid := fmt.Sprintf("%d", time.Now().Unix()) 28 + 29 + if err := key.Set(jwk.KeyIDKey, kid); err != nil { 30 + panic(err) 31 + } 32 + 33 + b, err := json.Marshal(key) 34 + if err != nil { 35 + panic(err) 36 + } 37 + 38 + fmt.Println(string(b)) 39 + }
+2 -4
docker/Dockerfile
··· 42 42 COPY docker/rootfs/ . 43 43 44 44 RUN chown root:root /usr/local/libexec/tangled-keyfetch && \ 45 - chmod 755 /usr/local/libexec/tangled-keyfetch && \ 46 - chown git:git /home/git/repoguard && \ 47 - chown git:git /app && chown git:git /home/git/repositories 45 + chmod 755 /usr/local/libexec/tangled-keyfetch 48 46 49 47 EXPOSE 22 50 48 EXPOSE 5555 51 49 52 - ENTRYPOINT ["/init"] 50 + ENTRYPOINT ["/bin/sh", "-c", "chown git:git /home/git/repoguard && chown git:git /app && chown git:git /home/git/repositories && /init"]
+17 -1
docker/docker-compose.yml
··· 13 13 - "./repositories:/home/git/repositories" 14 14 - "./server:/app" 15 15 ports: 16 - - "5555:5555" 17 16 - "2222:22" 17 + frontend: 18 + image: caddy:2-alpine 19 + command: > 20 + caddy 21 + reverse-proxy 22 + --from ${KNOT_SERVER_HOSTNAME} 23 + --to knot:5555 24 + depends_on: 25 + - knot 26 + ports: 27 + - "443:443" 28 + - "443:443/udp" 29 + volumes: 30 + - caddy_data:/data 31 + restart: always 32 + volumes: 33 + caddy_data:
+9 -7
docs/contributing.md
··· 11 11 ### message format 12 12 13 13 ``` 14 - <service/top-level directory>: <package/path>: <short summary of change> 14 + <service/top-level directory>: <affected package/directory>: <short summary of change> 15 15 16 16 17 - Optional longer description, if needed. Explain what the change does and 18 - why, especially if not obvious. Reference relevant issues or PRs when 19 - applicable. These can be links for now since we don't auto-link 20 - issues/PRs yet. 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 21 ``` 22 22 23 23 Here are some examples: ··· 35 35 36 36 ### general notes 37 37 38 - - PRs get merged as a single commit, so keep PRs small and focused. Use 39 - the above guidelines for the PR title and description. 38 + - PRs get merged "as-is" (fast-forward) -- like applying a patch-series 39 + using `git am`. At present, there is no squashing -- so please author 40 + your commits as they would appear on `master`, following the above 41 + guidelines. 40 42 - Use the imperative mood in the summary line (e.g., "fix bug" not 41 43 "fixed bug" or "fixes bug"). 42 44 - Try to keep the summary line under 72 characters, but we aren't too
+72
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 + ## running a knotserver 36 + 37 + An end-to-end knotserver setup requires setting up a machine 38 + with `sshd`, `repoguard`, `keyfetch`, a git user, which is 39 + quite cumbersome and so the nix flake provides a 40 + `nixosConfiguration` to do so. 41 + 42 + To begin, head to `http://localhost:3000` in the browser and 43 + generate a knotserver secret. Replace the existing secret in 44 + `flake.nix` with the newly generated secret. 45 + 46 + You can now start a lightweight NixOS VM using 47 + `nixos-shell` like so: 48 + 49 + ```bash 50 + QEMU_NET_OPTS="hostfwd=tcp::6000-:6000,hostfwd=tcp::2222-:22" nixos-shell --flake .#knotVM 51 + 52 + # hit Ctrl-a + c + q to exit the VM 53 + ``` 54 + 55 + This starts a knotserver on port 6000 with `ssh` exposed on 56 + port 2222. You can push repositories to this VM with this 57 + ssh config block on your main machine: 58 + 59 + ```bash 60 + Host nixos-shell 61 + Hostname localhost 62 + Port 2222 63 + User git 64 + IdentityFile ~/.ssh/my_tangled_key 65 + ``` 66 + 67 + Set up a remote called `local-dev` on a git repo: 68 + 69 + ```bash 70 + git remote add local-dev git@nixos-shell:user/repo 71 + git push local-dev main 72 + ```
+82
docs/knot-hosting.md
··· 106 106 107 107 You should now have a running knot server! You can finalize your registration by hitting the 108 108 `initialize` button on the [/knots](/knots) page. 109 + 110 + ### custom paths 111 + 112 + (This section applies to manual setup only. Docker users should edit the mounts 113 + in `docker-compose.yml` instead.) 114 + 115 + Right now, the database and repositories of your knot lives in `/home/git`. You 116 + can move these paths if you'd like to store them in another folder. Be careful 117 + when adjusting these paths: 118 + 119 + * Stop your knot when moving data (e.g. `systemctl stop knotserver`) to prevent 120 + any possible side effects. Remember to restart it once you're done. 121 + * Make backups before moving in case something goes wrong. 122 + * Make sure the `git` user can read and write from the new paths. 123 + 124 + #### database 125 + 126 + As an example, let's say the current database is at `/home/git/knotserver.db`, 127 + and we want to move it to `/home/git/database/knotserver.db`. 128 + 129 + Copy the current database to the new location. Make sure to copy the `.db-shm` 130 + and `.db-wal` files if they exist. 131 + 132 + ``` 133 + mkdir /home/git/database 134 + cp /home/git/knotserver.db* /home/git/database 135 + ``` 136 + 137 + In the environment (e.g. `/home/git/.knot.env`), set `KNOT_SERVER_DB_PATH` to 138 + the new file path (_not_ the directory): 139 + 140 + ``` 141 + KNOT_SERVER_DB_PATH=/home/git/database/knotserver.db 142 + ``` 143 + 144 + #### repositories 145 + 146 + As an example, let's say the repositories are currently in `/home/git`, and we 147 + want to move them into `/home/git/repositories`. 148 + 149 + Create the new folder, then move the existing repositories (if there are any): 150 + 151 + ``` 152 + mkdir /home/git/repositories 153 + # move all DIDs into the new folder; these will vary for you! 154 + mv /home/git/did:plc:wshs7t2adsemcrrd4snkeqli /home/git/repositories 155 + ``` 156 + 157 + In the environment (e.g. `/home/git/.knot.env`), update `KNOT_REPO_SCAN_PATH` 158 + to the new directory: 159 + 160 + ``` 161 + KNOT_REPO_SCAN_PATH=/home/git/repositories 162 + ``` 163 + 164 + In your SSH config (e.g. `/etc/ssh/sshd_config.d/authorized_keys_command.conf`), 165 + update the `AuthorizedKeysCommand` line to use the new folder. For example: 166 + 167 + ``` 168 + Match User git 169 + AuthorizedKeysCommand /usr/local/libexec/tangled-keyfetch -git-dir /home/git/repositories 170 + AuthorizedKeysCommandUser nobody 171 + ``` 172 + 173 + Make sure to restart your SSH server! 174 + 175 + #### git 176 + 177 + The keyfetch executable takes multiple arguments to change certain paths. You 178 + can view a full list by running `/usr/local/libexec/tangled-keyfetch -h`. 179 + 180 + As an example, if you wanted to change the path to the repoguard executable, 181 + you would edit your SSH config (e.g. `/etc/ssh/sshd_config.d/authorized_keys_command.conf`) 182 + and update the `AuthorizedKeysCommand` line: 183 + 184 + ``` 185 + Match User git 186 + AuthorizedKeysCommand /usr/local/libexec/tangled-keyfetch -repoguard-path /path/to/repoguard 187 + AuthorizedKeysCommandUser nobody 188 + ``` 189 + 190 + Make sure to restart your SSH server!
+7 -7
flake.lock
··· 48 48 "indigo": { 49 49 "flake": false, 50 50 "locked": { 51 - "lastModified": 1738491661, 52 - "narHash": "sha256-+njDigkvjH4XmXZMog5Mp0K4x9mamHX6gSGJCZB9mE4=", 51 + "lastModified": 1745333930, 52 + "narHash": "sha256-83fIHqDE+dfnZ88HaNuwfKFO+R0RKAM1WxMfNh/Matk=", 53 53 "owner": "oppiliappan", 54 54 "repo": "indigo", 55 - "rev": "feb802f02a462ac0a6392ffc3e40b0529f0cdf71", 55 + "rev": "e4e59280737b8676611fc077a228d47b3e8e9491", 56 56 "type": "github" 57 57 }, 58 58 "original": { ··· 89 89 }, 90 90 "nixpkgs": { 91 91 "locked": { 92 - "lastModified": 1743813633, 93 - "narHash": "sha256-BgkBz4NpV6Kg8XF7cmHDHRVGZYnKbvG0Y4p+jElwxaM=", 92 + "lastModified": 1746904237, 93 + "narHash": "sha256-3e+AVBczosP5dCLQmMoMEogM57gmZ2qrVSrmq9aResQ=", 94 94 "owner": "nixos", 95 95 "repo": "nixpkgs", 96 - "rev": "7819a0d29d1dd2bc331bec4b327f0776359b1fa6", 96 + "rev": "d89fc19e405cb2d55ce7cc114356846a0ee5e956", 97 97 "type": "github" 98 98 }, 99 99 "original": { 100 100 "owner": "nixos", 101 - "ref": "nixos-24.11", 101 + "ref": "nixos-unstable", 102 102 "repo": "nixpkgs", 103 103 "type": "github" 104 104 }
+24 -9
flake.nix
··· 2 2 description = "atproto github"; 3 3 4 4 inputs = { 5 - nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11"; 5 + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 6 indigo = { 7 7 url = "github:oppiliappan/indigo"; 8 8 flake = false; ··· 49 49 inherit (gitignore.lib) gitignoreSource; 50 50 in { 51 51 overlays.default = final: prev: let 52 - goModHash = "sha256-EilWxfqrcKDaSR5zA3ZuDSCq7V+/IfWpKPu8HWhpndA="; 52 + goModHash = "sha256-zcfTNo7QsiihzLa4qHEX8uGGtbcmBn8TlSm0YHBRNw8="; 53 53 buildCmdPackage = name: 54 54 final.buildGoModule { 55 55 pname = name; ··· 57 57 src = gitignoreSource ./.; 58 58 subPackages = ["cmd/${name}"]; 59 59 vendorHash = goModHash; 60 - CGO_ENABLED = 0; 60 + env.CGO_ENABLED = 0; 61 61 }; 62 62 in { 63 63 indigo-lexgen = final.buildGoModule { ··· 88 88 doCheck = false; 89 89 subPackages = ["cmd/appview"]; 90 90 vendorHash = goModHash; 91 - CGO_ENABLED = 1; 91 + env.CGO_ENABLED = 1; 92 92 stdenv = pkgsStatic.stdenv; 93 93 }; 94 94 ··· 111 111 112 112 runHook postInstall 113 113 ''; 114 - CGO_ENABLED = 1; 114 + env.CGO_ENABLED = 1; 115 115 }; 116 116 knotserver-unwrapped = final.pkgsStatic.buildGoModule { 117 117 pname = "knotserver"; ··· 119 119 src = gitignoreSource ./.; 120 120 subPackages = ["cmd/knotserver"]; 121 121 vendorHash = goModHash; 122 - CGO_ENABLED = 1; 122 + env.CGO_ENABLED = 1; 123 123 }; 124 124 repoguard = buildCmdPackage "repoguard"; 125 125 keyfetch = buildCmdPackage "keyfetch"; 126 + genjwks = buildCmdPackage "genjwks"; 126 127 }; 127 128 packages = forAllSystems (system: { 128 129 inherit ··· 133 134 knotserver-unwrapped 134 135 repoguard 135 136 keyfetch 137 + genjwks 136 138 ; 137 139 }); 138 140 defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); ··· 162 164 cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 163 165 cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 164 166 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 167 + export TANGLED_OAUTH_JWKS="$(${pkgs.genjwks}/bin/genjwks)" 165 168 ''; 169 + env.CGO_ENABLED = 1; 166 170 }; 167 171 }); 168 172 apps = forAllSystems (system: let ··· 171 175 pkgs.writeShellScriptBin "run" 172 176 '' 173 177 ${pkgs.air}/bin/air -c /dev/null \ 174 - -build.cmd "${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o ./appview/pages/static/tw.css && ${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ 178 + -build.cmd "${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ 175 179 -build.bin "./out/${name}.out" \ 176 - -build.include_ext "go,html,css" 180 + -build.stop_on_error "true" \ 181 + -build.include_ext "go" 182 + ''; 183 + tailwind-watcher = 184 + pkgs.writeShellScriptBin "run" 185 + '' 186 + ${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css 177 187 ''; 178 188 in { 179 189 watch-appview = { ··· 183 193 watch-knotserver = { 184 194 type = "app"; 185 195 program = ''${air-watcher "knotserver"}/bin/run''; 196 + }; 197 + watch-tailwind = { 198 + type = "app"; 199 + program = ''${tailwind-watcher}/bin/run''; 186 200 }; 187 201 }); 188 202 ··· 412 426 ... 413 427 }: { 414 428 virtualisation.memorySize = 2048; 429 + virtualisation.diskSize = 10 * 1024; 415 430 virtualisation.cores = 2; 416 431 services.getty.autologinUser = "root"; 417 432 environment.systemPackages = with pkgs; [curl vim git]; ··· 420 435 g = config.services.tangled-knotserver.gitUser; 421 436 in [ 422 437 "d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first 423 - "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=5b42390da4c6659f34c9a545adebd8af82c4a19960d735f651e3d582623ba9f2" 438 + "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=679f15000084699abc6a20d3ef449efa3656583f38e456a08f0638250688ff2e" 424 439 ]; 425 440 services.tangled-knotserver = { 426 441 enable = true;
+19 -13
go.mod
··· 1 1 module tangled.sh/tangled.sh/core 2 2 3 - go 1.23.0 3 + go 1.24.0 4 4 5 - toolchain go1.23.6 5 + toolchain go1.24.3 6 6 7 7 require ( 8 8 github.com/Blank-Xu/sql-adapter v1.1.1 9 9 github.com/alecthomas/chroma/v2 v2.15.0 10 10 github.com/bluekeyes/go-gitdiff v0.8.1 11 - github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20 11 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 12 12 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 13 13 github.com/casbin/casbin/v2 v2.103.0 14 14 github.com/cyphar/filepath-securejoin v0.4.1 ··· 19 19 github.com/go-git/go-git/v5 v5.14.0 20 20 github.com/google/uuid v1.6.0 21 21 github.com/gorilla/sessions v1.4.0 22 - github.com/ipfs/go-cid v0.4.1 22 + github.com/haileyok/atproto-oauth-golang v0.0.2 23 + github.com/ipfs/go-cid v0.5.0 24 + github.com/lestrrat-go/jwx/v2 v2.0.12 23 25 github.com/mattn/go-sqlite3 v1.14.24 24 26 github.com/microcosm-cc/bluemonday v1.0.27 25 27 github.com/resend/resend-go/v2 v2.15.0 ··· 41 43 github.com/casbin/govaluate v1.3.0 // indirect 42 44 github.com/cespare/xxhash/v2 v2.3.0 // indirect 43 45 github.com/cloudflare/circl v1.6.0 // indirect 44 - github.com/davecgh/go-spew v1.1.1 // indirect 46 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 45 47 github.com/dlclark/regexp2 v1.11.5 // indirect 46 48 github.com/emirpasic/gods v1.18.1 // indirect 47 49 github.com/felixge/httpsnoop v1.0.4 // indirect 48 50 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 49 51 github.com/go-git/go-billy/v5 v5.6.2 // indirect 50 - github.com/go-logr/logr v1.4.1 // indirect 52 + github.com/go-logr/logr v1.4.2 // indirect 51 53 github.com/go-logr/stdr v1.2.2 // indirect 52 54 github.com/goccy/go-json v0.10.2 // indirect 53 55 github.com/gogo/protobuf v1.3.2 // indirect 56 + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 54 57 github.com/gorilla/css v1.0.1 // indirect 55 58 github.com/gorilla/securecookie v1.1.2 // indirect 56 59 github.com/gorilla/websocket v1.5.1 // indirect ··· 75 78 github.com/kevinburke/ssh_config v1.2.0 // indirect 76 79 github.com/klauspost/compress v1.17.9 // indirect 77 80 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 81 + github.com/lestrrat-go/blackmagic v1.0.2 // indirect 82 + github.com/lestrrat-go/httpcc v1.0.1 // indirect 83 + github.com/lestrrat-go/httprc v1.0.4 // indirect 84 + github.com/lestrrat-go/iter v1.0.2 // indirect 85 + github.com/lestrrat-go/option v1.0.1 // indirect 78 86 github.com/mattn/go-isatty v0.0.20 // indirect 79 87 github.com/minio/sha256-simd v1.0.1 // indirect 80 88 github.com/mr-tron/base58 v1.2.0 // indirect ··· 86 94 github.com/opentracing/opentracing-go v1.2.0 // indirect 87 95 github.com/pjbgf/sha1cd v0.3.2 // indirect 88 96 github.com/pkg/errors v0.9.1 // indirect 89 - github.com/pmezard/go-difflib v1.0.0 // indirect 90 97 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect 91 98 github.com/prometheus/client_golang v1.19.1 // indirect 92 99 github.com/prometheus/client_model v0.6.1 // indirect 93 100 github.com/prometheus/common v0.54.0 // indirect 94 101 github.com/prometheus/procfs v0.15.1 // indirect 102 + github.com/segmentio/asm v1.2.0 // indirect 95 103 github.com/sergi/go-diff v1.3.1 // indirect 96 104 github.com/skeema/knownhosts v1.3.1 // indirect 97 105 github.com/spaolacci/murmur3 v1.1.0 // indirect 98 - github.com/stretchr/testify v1.10.0 // indirect 99 106 github.com/xanzy/ssh-agent v0.3.3 // indirect 100 107 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 101 108 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 102 109 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect 103 - go.opentelemetry.io/otel v1.21.0 // indirect 104 - go.opentelemetry.io/otel/metric v1.21.0 // indirect 105 - go.opentelemetry.io/otel/trace v1.21.0 // indirect 110 + go.opentelemetry.io/otel v1.29.0 // indirect 111 + go.opentelemetry.io/otel/metric v1.29.0 // indirect 112 + go.opentelemetry.io/otel/trace v1.29.0 // indirect 106 113 go.uber.org/atomic v1.11.0 // indirect 107 114 go.uber.org/multierr v1.11.0 // indirect 108 115 go.uber.org/zap v1.26.0 // indirect 109 116 golang.org/x/crypto v0.37.0 // indirect 110 117 golang.org/x/net v0.39.0 // indirect 111 118 golang.org/x/sys v0.32.0 // indirect 112 - golang.org/x/time v0.5.0 // indirect 119 + golang.org/x/time v0.8.0 // indirect 113 120 google.golang.org/protobuf v1.34.2 // indirect 114 121 gopkg.in/warnings.v0 v0.1.2 // indirect 115 - gopkg.in/yaml.v3 v3.0.1 // indirect 116 122 lukechampine.com/blake3 v1.2.1 // indirect 117 123 ) 118 124
+63 -18
go.sum
··· 26 26 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 27 27 github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= 28 28 github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE= 29 - github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20 h1:yHusfYYi8odoCcsI6AurU+dRWb7itHAQNwt3/Rl9Vfs= 30 - github.com/bluesky-social/indigo v0.0.0-20250123072624-9e3b84fdbb20/go.mod h1:Qp4YqWf+AQ3TwQCxV5Ls8O2tXE55zVTGVs3zTmn7BOg= 29 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk= 30 + github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA= 31 31 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1 h1:CFvRtYNSnWRAi/98M3O466t9dYuwtesNbu6FVPymRrA= 32 32 github.com/bluesky-social/jetstream v0.0.0-20241210005130-ea96859b93d1/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4= 33 33 github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= ··· 52 52 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 53 53 github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 54 54 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55 - github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 56 55 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 57 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 58 + github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= 59 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 60 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 57 61 github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE= 58 62 github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU= 59 63 github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= ··· 82 86 github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= 83 87 github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= 84 88 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 85 - github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 86 - github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 89 + github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 90 + github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 87 91 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 88 92 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 89 93 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= ··· 91 95 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 92 96 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 93 97 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 98 + github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 99 + github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 94 100 github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= 95 101 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 96 102 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= ··· 111 117 github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 112 118 github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= 113 119 github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= 120 + github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8= 121 + github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8= 114 122 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 115 123 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 116 124 github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= ··· 130 138 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 131 139 github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 132 140 github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 133 - github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 134 - github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 141 + github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= 142 + github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= 135 143 github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 136 144 github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 137 145 github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= ··· 159 167 github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 160 168 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 161 169 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 170 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 171 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 162 172 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 163 173 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 164 174 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= ··· 177 187 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 178 188 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 179 189 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 190 + github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 191 + github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= 192 + github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= 193 + github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= 194 + github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= 195 + github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8= 196 + github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= 197 + github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= 198 + github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= 199 + github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA= 200 + github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ= 201 + github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 202 + github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= 203 + github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= 180 204 github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= 181 205 github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= 182 206 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= ··· 212 236 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 213 237 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 214 238 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 215 - github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 216 239 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 240 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 241 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 217 242 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0= 218 243 github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw= 219 244 github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= ··· 227 252 github.com/resend/resend-go/v2 v2.15.0 h1:B6oMEPf8IEQwn2Ovx/9yymkESLDSeNfLFaNMw+mzHhE= 228 253 github.com/resend/resend-go/v2 v2.15.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ= 229 254 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 230 - github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 231 - github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 255 + github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 256 + github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 232 257 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 258 + github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= 259 + github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= 233 260 github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= 234 261 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 235 262 github.com/sethvargo/go-envconfig v1.1.0 h1:cWZiJxeTm7AlCvzGXrEXaSTCNgip5oJepekh/BOQuog= ··· 246 273 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 247 274 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 248 275 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 276 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 277 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 249 278 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 250 279 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 251 280 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 281 + github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 252 282 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 283 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 284 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 285 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 253 286 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 254 287 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 255 288 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= ··· 270 303 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I= 271 304 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= 272 305 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= 273 - go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= 274 - go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= 275 - go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= 276 - go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= 277 - go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= 278 - go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= 306 + go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 307 + go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 308 + go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 309 + go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 310 + go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 311 + go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 279 312 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 280 313 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 281 314 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= ··· 303 336 golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 304 337 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 305 338 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 339 + golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= 306 340 golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 307 341 golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 308 342 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= ··· 314 348 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 315 349 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 316 350 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 351 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 317 352 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 318 353 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 319 354 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= ··· 327 362 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 328 363 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 329 364 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 365 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 330 366 golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 331 367 golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 332 368 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= ··· 334 370 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 335 371 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 336 372 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 373 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 337 374 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 338 375 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 376 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 348 385 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 349 386 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 350 387 golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 388 + golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 351 389 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 352 390 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 353 391 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= ··· 357 395 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 358 396 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 359 397 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 398 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 399 + golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 360 400 golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 361 401 golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 362 402 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= ··· 364 404 golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 365 405 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 366 406 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 407 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 408 + golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= 367 409 golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 368 410 golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 369 411 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= ··· 372 414 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 373 415 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 374 416 golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 417 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 418 + golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 375 419 golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 376 420 golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 377 - golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 378 - golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 421 + golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 422 + golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 379 423 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 380 424 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 381 425 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= ··· 389 433 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 390 434 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 391 435 golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 436 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 392 437 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 393 438 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 394 439 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+681 -153
input.css
··· 13 13 @font-face { 14 14 font-family: "InterVariable"; 15 15 src: url("/static/fonts/InterVariable-Italic.woff2") format("woff2"); 16 - font-weight: normal; 16 + font-weight: 400; 17 17 font-style: italic; 18 + font-display: swap; 19 + } 20 + 21 + @font-face { 22 + font-family: "InterVariable"; 23 + src: url("/static/fonts/InterVariable.woff2") format("woff2"); 24 + font-weight: 600; 25 + font-style: normal; 18 26 font-display: swap; 19 27 } 20 28 ··· 32 40 33 41 @layer base { 34 42 html { 35 - font-size: 15px; 43 + font-size: 14px; 36 44 } 37 45 @supports (font-variation-settings: normal) { 38 46 html { ··· 96 104 } 97 105 } 98 106 99 - /* Background */ .bg { color: #4c4f69; background-color: #eff1f5; } 100 - /* PreWrapper */ .chroma { color: #4c4f69; background-color: #eff1f5; } 101 - /* Error */ .chroma .err { color: #d20f39 } 102 - /* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } 103 - /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 104 - /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } 105 - /* LineHighlight */ .chroma .hl { background-color: #bcc0cc } 106 - /* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 } 107 - /* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8c8fa1 } 108 - /* Line */ .chroma .line { display: flex; } 109 - /* Keyword */ .chroma .k { color: #8839ef } 110 - /* KeywordConstant */ .chroma .kc { color: #fe640b } 111 - /* KeywordDeclaration */ .chroma .kd { color: #d20f39 } 112 - /* KeywordNamespace */ .chroma .kn { color: #179299 } 113 - /* KeywordPseudo */ .chroma .kp { color: #8839ef } 114 - /* KeywordReserved */ .chroma .kr { color: #8839ef } 115 - /* KeywordType */ .chroma .kt { color: #d20f39 } 116 - /* NameAttribute */ .chroma .na { color: #1e66f5 } 117 - /* NameBuiltin */ .chroma .nb { color: #04a5e5 } 118 - /* NameBuiltinPseudo */ .chroma .bp { color: #04a5e5 } 119 - /* NameClass */ .chroma .nc { color: #df8e1d } 120 - /* NameConstant */ .chroma .no { color: #df8e1d } 121 - /* NameDecorator */ .chroma .nd { color: #1e66f5; font-weight: bold } 122 - /* NameEntity */ .chroma .ni { color: #179299 } 123 - /* NameException */ .chroma .ne { color: #fe640b } 124 - /* NameFunction */ .chroma .nf { color: #1e66f5 } 125 - /* NameFunctionMagic */ .chroma .fm { color: #1e66f5 } 126 - /* NameLabel */ .chroma .nl { color: #04a5e5 } 127 - /* NameNamespace */ .chroma .nn { color: #fe640b } 128 - /* NameProperty */ .chroma .py { color: #fe640b } 129 - /* NameTag */ .chroma .nt { color: #8839ef } 130 - /* NameVariable */ .chroma .nv { color: #dc8a78 } 131 - /* NameVariableClass */ .chroma .vc { color: #dc8a78 } 132 - /* NameVariableGlobal */ .chroma .vg { color: #dc8a78 } 133 - /* NameVariableInstance */ .chroma .vi { color: #dc8a78 } 134 - /* NameVariableMagic */ .chroma .vm { color: #dc8a78 } 135 - /* LiteralString */ .chroma .s { color: #40a02b } 136 - /* LiteralStringAffix */ .chroma .sa { color: #d20f39 } 137 - /* LiteralStringBacktick */ .chroma .sb { color: #40a02b } 138 - /* LiteralStringChar */ .chroma .sc { color: #40a02b } 139 - /* LiteralStringDelimiter */ .chroma .dl { color: #1e66f5 } 140 - /* LiteralStringDoc */ .chroma .sd { color: #9ca0b0 } 141 - /* LiteralStringDouble */ .chroma .s2 { color: #40a02b } 142 - /* LiteralStringEscape */ .chroma .se { color: #1e66f5 } 143 - /* LiteralStringHeredoc */ .chroma .sh { color: #9ca0b0 } 144 - /* LiteralStringInterpol */ .chroma .si { color: #40a02b } 145 - /* LiteralStringOther */ .chroma .sx { color: #40a02b } 146 - /* LiteralStringRegex */ .chroma .sr { color: #179299 } 147 - /* LiteralStringSingle */ .chroma .s1 { color: #40a02b } 148 - /* LiteralStringSymbol */ .chroma .ss { color: #40a02b } 149 - /* LiteralNumber */ .chroma .m { color: #fe640b } 150 - /* LiteralNumberBin */ .chroma .mb { color: #fe640b } 151 - /* LiteralNumberFloat */ .chroma .mf { color: #fe640b } 152 - /* LiteralNumberHex */ .chroma .mh { color: #fe640b } 153 - /* LiteralNumberInteger */ .chroma .mi { color: #fe640b } 154 - /* LiteralNumberIntegerLong */ .chroma .il { color: #fe640b } 155 - /* LiteralNumberOct */ .chroma .mo { color: #fe640b } 156 - /* Operator */ .chroma .o { color: #04a5e5; font-weight: bold } 157 - /* OperatorWord */ .chroma .ow { color: #04a5e5; font-weight: bold } 158 - /* Comment */ .chroma .c { color: #9ca0b0; font-style: italic } 159 - /* CommentHashbang */ .chroma .ch { color: #9ca0b0; font-style: italic } 160 - /* CommentMultiline */ .chroma .cm { color: #9ca0b0; font-style: italic } 161 - /* CommentSingle */ .chroma .c1 { color: #9ca0b0; font-style: italic } 162 - /* CommentSpecial */ .chroma .cs { color: #9ca0b0; font-style: italic } 163 - /* CommentPreproc */ .chroma .cp { color: #9ca0b0; font-style: italic } 164 - /* CommentPreprocFile */ .chroma .cpf { color: #9ca0b0; font-weight: bold; font-style: italic } 165 - /* GenericDeleted */ .chroma .gd { color: #d20f39; background-color: #ccd0da } 166 - /* GenericEmph */ .chroma .ge { font-style: italic } 167 - /* GenericError */ .chroma .gr { color: #d20f39 } 168 - /* GenericHeading */ .chroma .gh { color: #fe640b; font-weight: bold } 169 - /* GenericInserted */ .chroma .gi { color: #40a02b; background-color: #ccd0da } 170 - /* GenericStrong */ .chroma .gs { font-weight: bold } 171 - /* GenericSubheading */ .chroma .gu { color: #fe640b; font-weight: bold } 172 - /* GenericTraceback */ .chroma .gt { color: #d20f39 } 173 - /* GenericUnderline */ .chroma .gl { text-decoration: underline } 107 + /* Background */ 108 + .bg { 109 + color: #4c4f69; 110 + background-color: #eff1f5; 111 + } 112 + /* PreWrapper */ 113 + .chroma { 114 + color: #4c4f69; 115 + background-color: #eff1f5; 116 + } 117 + /* Error */ 118 + .chroma .err { 119 + color: #d20f39; 120 + } 121 + /* LineLink */ 122 + .chroma .lnlinks { 123 + outline: none; 124 + text-decoration: none; 125 + color: inherit; 126 + } 127 + /* LineTableTD */ 128 + .chroma .lntd { 129 + vertical-align: top; 130 + padding: 0; 131 + margin: 0; 132 + border: 0; 133 + } 134 + /* LineTable */ 135 + .chroma .lntable { 136 + border-spacing: 0; 137 + padding: 0; 138 + margin: 0; 139 + border: 0; 140 + } 141 + /* LineHighlight */ 142 + .chroma .hl { 143 + background-color: #bcc0cc; 144 + } 145 + /* LineNumbersTable */ 146 + .chroma .lnt { 147 + white-space: pre; 148 + -webkit-user-select: none; 149 + user-select: none; 150 + margin-right: 0.4em; 151 + padding: 0 0.4em 0 0.4em; 152 + color: #8c8fa1; 153 + } 154 + /* LineNumbers */ 155 + .chroma .ln { 156 + white-space: pre; 157 + -webkit-user-select: none; 158 + user-select: none; 159 + margin-right: 0.4em; 160 + padding: 0 0.4em 0 0.4em; 161 + color: #8c8fa1; 162 + } 163 + /* Line */ 164 + .chroma .line { 165 + display: flex; 166 + } 167 + /* Keyword */ 168 + .chroma .k { 169 + color: #8839ef; 170 + } 171 + /* KeywordConstant */ 172 + .chroma .kc { 173 + color: #fe640b; 174 + } 175 + /* KeywordDeclaration */ 176 + .chroma .kd { 177 + color: #d20f39; 178 + } 179 + /* KeywordNamespace */ 180 + .chroma .kn { 181 + color: #179299; 182 + } 183 + /* KeywordPseudo */ 184 + .chroma .kp { 185 + color: #8839ef; 186 + } 187 + /* KeywordReserved */ 188 + .chroma .kr { 189 + color: #8839ef; 190 + } 191 + /* KeywordType */ 192 + .chroma .kt { 193 + color: #d20f39; 194 + } 195 + /* NameAttribute */ 196 + .chroma .na { 197 + color: #1e66f5; 198 + } 199 + /* NameBuiltin */ 200 + .chroma .nb { 201 + color: #04a5e5; 202 + } 203 + /* NameBuiltinPseudo */ 204 + .chroma .bp { 205 + color: #04a5e5; 206 + } 207 + /* NameClass */ 208 + .chroma .nc { 209 + color: #df8e1d; 210 + } 211 + /* NameConstant */ 212 + .chroma .no { 213 + color: #df8e1d; 214 + } 215 + /* NameDecorator */ 216 + .chroma .nd { 217 + color: #1e66f5; 218 + font-weight: bold; 219 + } 220 + /* NameEntity */ 221 + .chroma .ni { 222 + color: #179299; 223 + } 224 + /* NameException */ 225 + .chroma .ne { 226 + color: #fe640b; 227 + } 228 + /* NameFunction */ 229 + .chroma .nf { 230 + color: #1e66f5; 231 + } 232 + /* NameFunctionMagic */ 233 + .chroma .fm { 234 + color: #1e66f5; 235 + } 236 + /* NameLabel */ 237 + .chroma .nl { 238 + color: #04a5e5; 239 + } 240 + /* NameNamespace */ 241 + .chroma .nn { 242 + color: #fe640b; 243 + } 244 + /* NameProperty */ 245 + .chroma .py { 246 + color: #fe640b; 247 + } 248 + /* NameTag */ 249 + .chroma .nt { 250 + color: #8839ef; 251 + } 252 + /* NameVariable */ 253 + .chroma .nv { 254 + color: #dc8a78; 255 + } 256 + /* NameVariableClass */ 257 + .chroma .vc { 258 + color: #dc8a78; 259 + } 260 + /* NameVariableGlobal */ 261 + .chroma .vg { 262 + color: #dc8a78; 263 + } 264 + /* NameVariableInstance */ 265 + .chroma .vi { 266 + color: #dc8a78; 267 + } 268 + /* NameVariableMagic */ 269 + .chroma .vm { 270 + color: #dc8a78; 271 + } 272 + /* LiteralString */ 273 + .chroma .s { 274 + color: #40a02b; 275 + } 276 + /* LiteralStringAffix */ 277 + .chroma .sa { 278 + color: #d20f39; 279 + } 280 + /* LiteralStringBacktick */ 281 + .chroma .sb { 282 + color: #40a02b; 283 + } 284 + /* LiteralStringChar */ 285 + .chroma .sc { 286 + color: #40a02b; 287 + } 288 + /* LiteralStringDelimiter */ 289 + .chroma .dl { 290 + color: #1e66f5; 291 + } 292 + /* LiteralStringDoc */ 293 + .chroma .sd { 294 + color: #9ca0b0; 295 + } 296 + /* LiteralStringDouble */ 297 + .chroma .s2 { 298 + color: #40a02b; 299 + } 300 + /* LiteralStringEscape */ 301 + .chroma .se { 302 + color: #1e66f5; 303 + } 304 + /* LiteralStringHeredoc */ 305 + .chroma .sh { 306 + color: #9ca0b0; 307 + } 308 + /* LiteralStringInterpol */ 309 + .chroma .si { 310 + color: #40a02b; 311 + } 312 + /* LiteralStringOther */ 313 + .chroma .sx { 314 + color: #40a02b; 315 + } 316 + /* LiteralStringRegex */ 317 + .chroma .sr { 318 + color: #179299; 319 + } 320 + /* LiteralStringSingle */ 321 + .chroma .s1 { 322 + color: #40a02b; 323 + } 324 + /* LiteralStringSymbol */ 325 + .chroma .ss { 326 + color: #40a02b; 327 + } 328 + /* LiteralNumber */ 329 + .chroma .m { 330 + color: #fe640b; 331 + } 332 + /* LiteralNumberBin */ 333 + .chroma .mb { 334 + color: #fe640b; 335 + } 336 + /* LiteralNumberFloat */ 337 + .chroma .mf { 338 + color: #fe640b; 339 + } 340 + /* LiteralNumberHex */ 341 + .chroma .mh { 342 + color: #fe640b; 343 + } 344 + /* LiteralNumberInteger */ 345 + .chroma .mi { 346 + color: #fe640b; 347 + } 348 + /* LiteralNumberIntegerLong */ 349 + .chroma .il { 350 + color: #fe640b; 351 + } 352 + /* LiteralNumberOct */ 353 + .chroma .mo { 354 + color: #fe640b; 355 + } 356 + /* Operator */ 357 + .chroma .o { 358 + color: #04a5e5; 359 + font-weight: bold; 360 + } 361 + /* OperatorWord */ 362 + .chroma .ow { 363 + color: #04a5e5; 364 + font-weight: bold; 365 + } 366 + /* Comment */ 367 + .chroma .c { 368 + color: #9ca0b0; 369 + font-style: italic; 370 + } 371 + /* CommentHashbang */ 372 + .chroma .ch { 373 + color: #9ca0b0; 374 + font-style: italic; 375 + } 376 + /* CommentMultiline */ 377 + .chroma .cm { 378 + color: #9ca0b0; 379 + font-style: italic; 380 + } 381 + /* CommentSingle */ 382 + .chroma .c1 { 383 + color: #9ca0b0; 384 + font-style: italic; 385 + } 386 + /* CommentSpecial */ 387 + .chroma .cs { 388 + color: #9ca0b0; 389 + font-style: italic; 390 + } 391 + /* CommentPreproc */ 392 + .chroma .cp { 393 + color: #9ca0b0; 394 + font-style: italic; 395 + } 396 + /* CommentPreprocFile */ 397 + .chroma .cpf { 398 + color: #9ca0b0; 399 + font-weight: bold; 400 + font-style: italic; 401 + } 402 + /* GenericDeleted */ 403 + .chroma .gd { 404 + color: #d20f39; 405 + background-color: oklch(93.6% 0.032 17.717); 406 + } 407 + /* GenericEmph */ 408 + .chroma .ge { 409 + font-style: italic; 410 + } 411 + /* GenericError */ 412 + .chroma .gr { 413 + color: #d20f39; 414 + } 415 + /* GenericHeading */ 416 + .chroma .gh { 417 + color: #fe640b; 418 + font-weight: bold; 419 + } 420 + /* GenericInserted */ 421 + .chroma .gi { 422 + color: #40a02b; 423 + background-color: oklch(96.2% 0.044 156.743); 424 + } 425 + /* GenericStrong */ 426 + .chroma .gs { 427 + font-weight: bold; 428 + } 429 + /* GenericSubheading */ 430 + .chroma .gu { 431 + color: #fe640b; 432 + font-weight: bold; 433 + } 434 + /* GenericTraceback */ 435 + .chroma .gt { 436 + color: #d20f39; 437 + } 438 + /* GenericUnderline */ 439 + .chroma .gl { 440 + text-decoration: underline; 441 + } 174 442 175 443 @media (prefers-color-scheme: dark) { 176 - /* Background */ .bg { color: #cad3f5; background-color: #24273a; } 177 - /* PreWrapper */ .chroma { color: #cad3f5; background-color: #24273a; } 178 - /* Error */ .chroma .err { color: #ed8796 } 179 - /* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit } 180 - /* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } 181 - /* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; } 182 - /* LineHighlight */ .chroma .hl { background-color: #494d64 } 183 - /* LineNumbersTable */ .chroma .lnt { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8087a2 } 184 - /* LineNumbers */ .chroma .ln { white-space: pre; -webkit-user-select: none; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #8087a2 } 185 - /* Line */ .chroma .line { display: flex; } 186 - /* Keyword */ .chroma .k { color: #c6a0f6 } 187 - /* KeywordConstant */ .chroma .kc { color: #f5a97f } 188 - /* KeywordDeclaration */ .chroma .kd { color: #ed8796 } 189 - /* KeywordNamespace */ .chroma .kn { color: #8bd5ca } 190 - /* KeywordPseudo */ .chroma .kp { color: #c6a0f6 } 191 - /* KeywordReserved */ .chroma .kr { color: #c6a0f6 } 192 - /* KeywordType */ .chroma .kt { color: #ed8796 } 193 - /* NameAttribute */ .chroma .na { color: #8aadf4 } 194 - /* NameBuiltin */ .chroma .nb { color: #91d7e3 } 195 - /* NameBuiltinPseudo */ .chroma .bp { color: #91d7e3 } 196 - /* NameClass */ .chroma .nc { color: #eed49f } 197 - /* NameConstant */ .chroma .no { color: #eed49f } 198 - /* NameDecorator */ .chroma .nd { color: #8aadf4; font-weight: bold } 199 - /* NameEntity */ .chroma .ni { color: #8bd5ca } 200 - /* NameException */ .chroma .ne { color: #f5a97f } 201 - /* NameFunction */ .chroma .nf { color: #8aadf4 } 202 - /* NameFunctionMagic */ .chroma .fm { color: #8aadf4 } 203 - /* NameLabel */ .chroma .nl { color: #91d7e3 } 204 - /* NameNamespace */ .chroma .nn { color: #f5a97f } 205 - /* NameProperty */ .chroma .py { color: #f5a97f } 206 - /* NameTag */ .chroma .nt { color: #c6a0f6 } 207 - /* NameVariable */ .chroma .nv { color: #f4dbd6 } 208 - /* NameVariableClass */ .chroma .vc { color: #f4dbd6 } 209 - /* NameVariableGlobal */ .chroma .vg { color: #f4dbd6 } 210 - /* NameVariableInstance */ .chroma .vi { color: #f4dbd6 } 211 - /* NameVariableMagic */ .chroma .vm { color: #f4dbd6 } 212 - /* LiteralString */ .chroma .s { color: #a6da95 } 213 - /* LiteralStringAffix */ .chroma .sa { color: #ed8796 } 214 - /* LiteralStringBacktick */ .chroma .sb { color: #a6da95 } 215 - /* LiteralStringChar */ .chroma .sc { color: #a6da95 } 216 - /* LiteralStringDelimiter */ .chroma .dl { color: #8aadf4 } 217 - /* LiteralStringDoc */ .chroma .sd { color: #6e738d } 218 - /* LiteralStringDouble */ .chroma .s2 { color: #a6da95 } 219 - /* LiteralStringEscape */ .chroma .se { color: #8aadf4 } 220 - /* LiteralStringHeredoc */ .chroma .sh { color: #6e738d } 221 - /* LiteralStringInterpol */ .chroma .si { color: #a6da95 } 222 - /* LiteralStringOther */ .chroma .sx { color: #a6da95 } 223 - /* LiteralStringRegex */ .chroma .sr { color: #8bd5ca } 224 - /* LiteralStringSingle */ .chroma .s1 { color: #a6da95 } 225 - /* LiteralStringSymbol */ .chroma .ss { color: #a6da95 } 226 - /* LiteralNumber */ .chroma .m { color: #f5a97f } 227 - /* LiteralNumberBin */ .chroma .mb { color: #f5a97f } 228 - /* LiteralNumberFloat */ .chroma .mf { color: #f5a97f } 229 - /* LiteralNumberHex */ .chroma .mh { color: #f5a97f } 230 - /* LiteralNumberInteger */ .chroma .mi { color: #f5a97f } 231 - /* LiteralNumberIntegerLong */ .chroma .il { color: #f5a97f } 232 - /* LiteralNumberOct */ .chroma .mo { color: #f5a97f } 233 - /* Operator */ .chroma .o { color: #91d7e3; font-weight: bold } 234 - /* OperatorWord */ .chroma .ow { color: #91d7e3; font-weight: bold } 235 - /* Comment */ .chroma .c { color: #6e738d; font-style: italic } 236 - /* CommentHashbang */ .chroma .ch { color: #6e738d; font-style: italic } 237 - /* CommentMultiline */ .chroma .cm { color: #6e738d; font-style: italic } 238 - /* CommentSingle */ .chroma .c1 { color: #6e738d; font-style: italic } 239 - /* CommentSpecial */ .chroma .cs { color: #6e738d; font-style: italic } 240 - /* CommentPreproc */ .chroma .cp { color: #6e738d; font-style: italic } 241 - /* CommentPreprocFile */ .chroma .cpf { color: #6e738d; font-weight: bold; font-style: italic } 242 - /* GenericDeleted */ .chroma .gd { color: #ed8796; background-color: #363a4f } 243 - /* GenericEmph */ .chroma .ge { font-style: italic } 244 - /* GenericError */ .chroma .gr { color: #ed8796 } 245 - /* GenericHeading */ .chroma .gh { color: #f5a97f; font-weight: bold } 246 - /* GenericInserted */ .chroma .gi { color: #a6da95; background-color: #363a4f } 247 - /* GenericStrong */ .chroma .gs { font-weight: bold } 248 - /* GenericSubheading */ .chroma .gu { color: #f5a97f; font-weight: bold } 249 - /* GenericTraceback */ .chroma .gt { color: #ed8796 } 250 - /* GenericUnderline */ .chroma .gl { text-decoration: underline } 444 + /* Background */ 445 + .bg { 446 + color: #cad3f5; 447 + background-color: #24273a; 448 + } 449 + /* PreWrapper */ 450 + .chroma { 451 + color: #cad3f5; 452 + background-color: #24273a; 453 + } 454 + /* Error */ 455 + .chroma .err { 456 + color: #ed8796; 457 + } 458 + /* LineLink */ 459 + .chroma .lnlinks { 460 + outline: none; 461 + text-decoration: none; 462 + color: inherit; 463 + } 464 + /* LineTableTD */ 465 + .chroma .lntd { 466 + vertical-align: top; 467 + padding: 0; 468 + margin: 0; 469 + border: 0; 470 + } 471 + /* LineTable */ 472 + .chroma .lntable { 473 + border-spacing: 0; 474 + padding: 0; 475 + margin: 0; 476 + border: 0; 477 + } 478 + /* LineHighlight */ 479 + .chroma .hl { 480 + background-color: #494d64; 481 + } 482 + /* LineNumbersTable */ 483 + .chroma .lnt { 484 + white-space: pre; 485 + -webkit-user-select: none; 486 + user-select: none; 487 + margin-right: 0.4em; 488 + padding: 0 0.4em 0 0.4em; 489 + color: #8087a2; 490 + } 491 + /* LineNumbers */ 492 + .chroma .ln { 493 + white-space: pre; 494 + -webkit-user-select: none; 495 + user-select: none; 496 + margin-right: 0.4em; 497 + padding: 0 0.4em 0 0.4em; 498 + color: #8087a2; 499 + } 500 + /* Line */ 501 + .chroma .line { 502 + display: flex; 503 + } 504 + /* Keyword */ 505 + .chroma .k { 506 + color: #c6a0f6; 507 + } 508 + /* KeywordConstant */ 509 + .chroma .kc { 510 + color: #f5a97f; 511 + } 512 + /* KeywordDeclaration */ 513 + .chroma .kd { 514 + color: #ed8796; 515 + } 516 + /* KeywordNamespace */ 517 + .chroma .kn { 518 + color: #8bd5ca; 519 + } 520 + /* KeywordPseudo */ 521 + .chroma .kp { 522 + color: #c6a0f6; 523 + } 524 + /* KeywordReserved */ 525 + .chroma .kr { 526 + color: #c6a0f6; 527 + } 528 + /* KeywordType */ 529 + .chroma .kt { 530 + color: #ed8796; 531 + } 532 + /* NameAttribute */ 533 + .chroma .na { 534 + color: #8aadf4; 535 + } 536 + /* NameBuiltin */ 537 + .chroma .nb { 538 + color: #91d7e3; 539 + } 540 + /* NameBuiltinPseudo */ 541 + .chroma .bp { 542 + color: #91d7e3; 543 + } 544 + /* NameClass */ 545 + .chroma .nc { 546 + color: #eed49f; 547 + } 548 + /* NameConstant */ 549 + .chroma .no { 550 + color: #eed49f; 551 + } 552 + /* NameDecorator */ 553 + .chroma .nd { 554 + color: #8aadf4; 555 + font-weight: bold; 556 + } 557 + /* NameEntity */ 558 + .chroma .ni { 559 + color: #8bd5ca; 560 + } 561 + /* NameException */ 562 + .chroma .ne { 563 + color: #f5a97f; 564 + } 565 + /* NameFunction */ 566 + .chroma .nf { 567 + color: #8aadf4; 568 + } 569 + /* NameFunctionMagic */ 570 + .chroma .fm { 571 + color: #8aadf4; 572 + } 573 + /* NameLabel */ 574 + .chroma .nl { 575 + color: #91d7e3; 576 + } 577 + /* NameNamespace */ 578 + .chroma .nn { 579 + color: #f5a97f; 580 + } 581 + /* NameProperty */ 582 + .chroma .py { 583 + color: #f5a97f; 584 + } 585 + /* NameTag */ 586 + .chroma .nt { 587 + color: #c6a0f6; 588 + } 589 + /* NameVariable */ 590 + .chroma .nv { 591 + color: #f4dbd6; 592 + } 593 + /* NameVariableClass */ 594 + .chroma .vc { 595 + color: #f4dbd6; 596 + } 597 + /* NameVariableGlobal */ 598 + .chroma .vg { 599 + color: #f4dbd6; 600 + } 601 + /* NameVariableInstance */ 602 + .chroma .vi { 603 + color: #f4dbd6; 604 + } 605 + /* NameVariableMagic */ 606 + .chroma .vm { 607 + color: #f4dbd6; 608 + } 609 + /* LiteralString */ 610 + .chroma .s { 611 + color: #a6da95; 612 + } 613 + /* LiteralStringAffix */ 614 + .chroma .sa { 615 + color: #ed8796; 616 + } 617 + /* LiteralStringBacktick */ 618 + .chroma .sb { 619 + color: #a6da95; 620 + } 621 + /* LiteralStringChar */ 622 + .chroma .sc { 623 + color: #a6da95; 624 + } 625 + /* LiteralStringDelimiter */ 626 + .chroma .dl { 627 + color: #8aadf4; 628 + } 629 + /* LiteralStringDoc */ 630 + .chroma .sd { 631 + color: #6e738d; 632 + } 633 + /* LiteralStringDouble */ 634 + .chroma .s2 { 635 + color: #a6da95; 636 + } 637 + /* LiteralStringEscape */ 638 + .chroma .se { 639 + color: #8aadf4; 640 + } 641 + /* LiteralStringHeredoc */ 642 + .chroma .sh { 643 + color: #6e738d; 644 + } 645 + /* LiteralStringInterpol */ 646 + .chroma .si { 647 + color: #a6da95; 648 + } 649 + /* LiteralStringOther */ 650 + .chroma .sx { 651 + color: #a6da95; 652 + } 653 + /* LiteralStringRegex */ 654 + .chroma .sr { 655 + color: #8bd5ca; 656 + } 657 + /* LiteralStringSingle */ 658 + .chroma .s1 { 659 + color: #a6da95; 660 + } 661 + /* LiteralStringSymbol */ 662 + .chroma .ss { 663 + color: #a6da95; 664 + } 665 + /* LiteralNumber */ 666 + .chroma .m { 667 + color: #f5a97f; 668 + } 669 + /* LiteralNumberBin */ 670 + .chroma .mb { 671 + color: #f5a97f; 672 + } 673 + /* LiteralNumberFloat */ 674 + .chroma .mf { 675 + color: #f5a97f; 676 + } 677 + /* LiteralNumberHex */ 678 + .chroma .mh { 679 + color: #f5a97f; 680 + } 681 + /* LiteralNumberInteger */ 682 + .chroma .mi { 683 + color: #f5a97f; 684 + } 685 + /* LiteralNumberIntegerLong */ 686 + .chroma .il { 687 + color: #f5a97f; 688 + } 689 + /* LiteralNumberOct */ 690 + .chroma .mo { 691 + color: #f5a97f; 692 + } 693 + /* Operator */ 694 + .chroma .o { 695 + color: #91d7e3; 696 + font-weight: bold; 697 + } 698 + /* OperatorWord */ 699 + .chroma .ow { 700 + color: #91d7e3; 701 + font-weight: bold; 702 + } 703 + /* Comment */ 704 + .chroma .c { 705 + color: #6e738d; 706 + font-style: italic; 707 + } 708 + /* CommentHashbang */ 709 + .chroma .ch { 710 + color: #6e738d; 711 + font-style: italic; 712 + } 713 + /* CommentMultiline */ 714 + .chroma .cm { 715 + color: #6e738d; 716 + font-style: italic; 717 + } 718 + /* CommentSingle */ 719 + .chroma .c1 { 720 + color: #6e738d; 721 + font-style: italic; 722 + } 723 + /* CommentSpecial */ 724 + .chroma .cs { 725 + color: #6e738d; 726 + font-style: italic; 727 + } 728 + /* CommentPreproc */ 729 + .chroma .cp { 730 + color: #6e738d; 731 + font-style: italic; 732 + } 733 + /* CommentPreprocFile */ 734 + .chroma .cpf { 735 + color: #6e738d; 736 + font-weight: bold; 737 + font-style: italic; 738 + } 739 + /* GenericDeleted */ 740 + .chroma .gd { 741 + color: #ed8796; 742 + background-color: oklch(44.4% 0.177 26.899 / 0.5); 743 + } 744 + /* GenericEmph */ 745 + .chroma .ge { 746 + font-style: italic; 747 + } 748 + /* GenericError */ 749 + .chroma .gr { 750 + color: #ed8796; 751 + } 752 + /* GenericHeading */ 753 + .chroma .gh { 754 + color: #f5a97f; 755 + font-weight: bold; 756 + } 757 + /* GenericInserted */ 758 + .chroma .gi { 759 + color: #a6da95; 760 + background-color: oklch(44.8% 0.119 151.328 / 0.5); 761 + } 762 + /* GenericStrong */ 763 + .chroma .gs { 764 + font-weight: bold; 765 + } 766 + /* GenericSubheading */ 767 + .chroma .gu { 768 + color: #f5a97f; 769 + font-weight: bold; 770 + } 771 + /* GenericTraceback */ 772 + .chroma .gt { 773 + color: #ed8796; 774 + } 775 + /* GenericUnderline */ 776 + .chroma .gl { 777 + text-decoration: underline; 778 + } 251 779 } 252 780 253 781 .chroma .line:has(.ln:target) { 254 - @apply bg-amber-400/30 dark:bg-amber-500/20 782 + @apply bg-amber-400/30 dark:bg-amber-500/20; 255 783 }
+9 -9
knotserver/db/pubkeys.go
··· 23 23 Did: did, 24 24 } 25 25 pk.Key = record["key"] 26 - pk.Created = record["created"] 26 + pk.CreatedAt = record["createdAt"] 27 27 28 28 return d.AddPublicKey(pk) 29 29 } 30 30 31 31 func (d *DB) AddPublicKey(pk PublicKey) error { 32 - if pk.Created == "" { 33 - pk.Created = time.Now().Format(time.RFC3339) 32 + if pk.CreatedAt == "" { 33 + pk.CreatedAt = time.Now().Format(time.RFC3339) 34 34 } 35 35 36 36 query := `insert or ignore into public_keys (did, key, created) values (?, ?, ?)` 37 - _, err := d.db.Exec(query, pk.Did, pk.Key, pk.Created) 37 + _, err := d.db.Exec(query, pk.Did, pk.Key, pk.CreatedAt) 38 38 return err 39 39 } 40 40 ··· 46 46 47 47 func (pk *PublicKey) JSON() map[string]any { 48 48 return map[string]any{ 49 - "did": pk.Did, 50 - "key": pk.Key, 51 - "created": pk.Created, 49 + "did": pk.Did, 50 + "key": pk.Key, 51 + "createdAt": pk.CreatedAt, 52 52 } 53 53 } 54 54 ··· 63 63 64 64 for rows.Next() { 65 65 var publicKey PublicKey 66 - if err := rows.Scan(&publicKey.Key, &publicKey.Did, &publicKey.Created); err != nil { 66 + if err := rows.Scan(&publicKey.Key, &publicKey.Did, &publicKey.CreatedAt); err != nil { 67 67 return nil, err 68 68 } 69 69 keys = append(keys, publicKey) ··· 87 87 88 88 for rows.Next() { 89 89 var publicKey PublicKey 90 - if err := rows.Scan(&publicKey.Did, &publicKey.Key, &publicKey.Created); err != nil { 90 + if err := rows.Scan(&publicKey.Did, &publicKey.Key, &publicKey.CreatedAt); err != nil { 91 91 return nil, err 92 92 } 93 93 keys = append(keys, publicKey)
+54 -4
knotserver/git/git.go
··· 37 37 } 38 38 39 39 var ( 40 - ErrBinaryFile = fmt.Errorf("binary file") 40 + ErrBinaryFile = fmt.Errorf("binary file") 41 + ErrNotBinaryFile = fmt.Errorf("not binary file") 41 42 ) 42 43 43 44 type GitRepo struct { ··· 156 157 return commits, nil 157 158 } 158 159 160 + func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) { 161 + return g.r.CommitObject(h) 162 + } 163 + 159 164 func (g *GitRepo) LastCommit() (*object.Commit, error) { 160 165 c, err := g.r.CommitObject(g.h) 161 166 if err != nil { ··· 189 194 } 190 195 } 191 196 197 + func (g *GitRepo) RawContent(path string) ([]byte, error) { 198 + c, err := g.r.CommitObject(g.h) 199 + if err != nil { 200 + return nil, fmt.Errorf("commit object: %w", err) 201 + } 202 + 203 + tree, err := c.Tree() 204 + if err != nil { 205 + return nil, fmt.Errorf("file tree: %w", err) 206 + } 207 + 208 + file, err := tree.File(path) 209 + if err != nil { 210 + return nil, err 211 + } 212 + 213 + reader, err := file.Reader() 214 + if err != nil { 215 + return nil, fmt.Errorf("opening file reader: %w", err) 216 + } 217 + defer reader.Close() 218 + 219 + return io.ReadAll(reader) 220 + } 221 + 192 222 func (g *GitRepo) Tags() ([]*TagReference, error) { 193 223 iter, err := g.r.Tags() 194 224 if err != nil { ··· 222 252 return tags, nil 223 253 } 224 254 225 - func (g *GitRepo) Branches() ([]*plumbing.Reference, error) { 255 + func (g *GitRepo) Branches() ([]types.Branch, error) { 226 256 bi, err := g.r.Branches() 227 257 if err != nil { 228 258 return nil, fmt.Errorf("branchs: %w", err) 229 259 } 230 260 231 - branches := []*plumbing.Reference{} 261 + branches := []types.Branch{} 262 + 263 + defaultBranch, err := g.FindMainBranch() 264 + if err != nil { 265 + return nil, fmt.Errorf("getting default branch", "error", err.Error()) 266 + } 232 267 233 268 _ = bi.ForEach(func(ref *plumbing.Reference) error { 234 - branches = append(branches, ref) 269 + b := types.Branch{} 270 + b.Hash = ref.Hash().String() 271 + b.Name = ref.Name().Short() 272 + 273 + // resolve commit that this branch points to 274 + commit, _ := g.Commit(ref.Hash()) 275 + if commit != nil { 276 + b.Commit = commit 277 + } 278 + 279 + if defaultBranch != "" && defaultBranch == b.Name { 280 + b.IsDefault = true 281 + } 282 + 283 + branches = append(branches, b) 284 + 235 285 return nil 236 286 }) 237 287
+4
knotserver/handler.go
··· 102 102 r.Get("/*", h.Blob) 103 103 }) 104 104 105 + r.Route("/raw/{ref}", func(r chi.Router) { 106 + r.Get("/*", h.BlobRaw) 107 + }) 108 + 105 109 r.Get("/log/{ref}", h.Log) 106 110 r.Get("/archive/{file}", h.Archive) 107 111 r.Get("/commit/{ref}", h.Diff)
+2 -2
knotserver/jetstream.go
··· 43 43 return fmt.Errorf("failed to enforce permissions: %w", err) 44 44 } 45 45 46 - if err := h.e.AddMember(ThisServer, record.Member); err != nil { 46 + if err := h.e.AddMember(ThisServer, record.Subject); err != nil { 47 47 l.Error("failed to add member", "error", err) 48 48 return fmt.Errorf("failed to add member: %w", err) 49 49 } 50 - l.Info("added member from firehose", "member", record.Member) 50 + l.Info("added member from firehose", "member", record.Subject) 51 51 52 52 if err := h.db.AddDid(did); err != nil { 53 53 l.Error("failed to add did", "error", err)
+108 -20
knotserver/routes.go
··· 93 93 return 94 94 } 95 95 96 - bs := []types.Branch{} 97 - for _, branch := range branches { 98 - b := types.Branch{} 99 - b.Hash = branch.Hash().String() 100 - b.Name = branch.Name().Short() 101 - bs = append(bs, b) 102 - } 103 - 104 96 tags, err := gr.Tags() 105 97 if err != nil { 106 98 // Non-fatal, we *should* have at least one branch to show. ··· 160 152 Readme: readmeContent, 161 153 ReadmeFileName: readmeFile, 162 154 Files: files, 163 - Branches: bs, 155 + Branches: branches, 164 156 Tags: rtags, 165 157 TotalCommits: total, 166 158 } ··· 202 194 return 203 195 } 204 196 197 + func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { 198 + treePath := chi.URLParam(r, "*") 199 + ref := chi.URLParam(r, "ref") 200 + ref, _ = url.PathUnescape(ref) 201 + 202 + l := h.l.With("handler", "BlobRaw", "ref", ref, "treePath", treePath) 203 + 204 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 205 + gr, err := git.Open(path, ref) 206 + if err != nil { 207 + notFound(w) 208 + return 209 + } 210 + 211 + contents, err := gr.RawContent(treePath) 212 + if err != nil { 213 + writeError(w, err.Error(), http.StatusBadRequest) 214 + l.Error("file content", "error", err.Error()) 215 + return 216 + } 217 + 218 + mimeType := http.DetectContentType(contents) 219 + 220 + // exception for svg 221 + if strings.HasPrefix(mimeType, "text/xml") && filepath.Ext(treePath) == ".svg" { 222 + mimeType = "image/svg+xml" 223 + } 224 + 225 + if !strings.HasPrefix(mimeType, "image/") && !strings.HasPrefix(mimeType, "video/") { 226 + l.Error("attempted to serve non-image/video file", "mimetype", mimeType) 227 + writeError(w, "only image and video files can be accessed directly", http.StatusForbidden) 228 + return 229 + } 230 + 231 + w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours 232 + w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) 233 + w.Header().Set("Content-Type", mimeType) 234 + w.Write(contents) 235 + } 236 + 205 237 func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 206 238 treePath := chi.URLParam(r, "*") 207 239 ref := chi.URLParam(r, "ref") 208 240 ref, _ = url.PathUnescape(ref) 209 241 210 - l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath) 242 + l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 211 243 212 244 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 213 245 gr, err := git.Open(path, ref) ··· 293 325 294 326 func (h *Handle) Log(w http.ResponseWriter, r *http.Request) { 295 327 ref := chi.URLParam(r, "ref") 328 + ref, _ = url.PathUnescape(ref) 329 + 296 330 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 297 331 298 332 l := h.l.With("handler", "Log", "ref", ref, "path", path) ··· 442 476 return 443 477 } 444 478 445 - bs := []types.Branch{} 446 - for _, branch := range branches { 447 - b := types.Branch{} 448 - b.Hash = branch.Hash().String() 449 - b.Name = branch.Name().Short() 450 - bs = append(bs, b) 451 - } 452 - 453 479 resp := types.RepoBranchesResponse{ 454 - Branches: bs, 480 + Branches: branches, 455 481 } 456 482 457 483 writeJSON(w, resp) ··· 461 487 func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) { 462 488 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 463 489 branchName := chi.URLParam(r, "branch") 490 + branchName, _ = url.PathUnescape(branchName) 491 + 464 492 l := h.l.With("handler", "Branch") 465 493 466 494 gr, err := git.PlainOpen(path) ··· 471 499 472 500 ref, err := gr.Branch(branchName) 473 501 if err != nil { 474 - l.Error("getting branches", "error", err.Error()) 502 + l.Error("getting branch", "error", err.Error()) 503 + writeError(w, err.Error(), http.StatusInternalServerError) 504 + return 505 + } 506 + 507 + commit, err := gr.Commit(ref.Hash()) 508 + if err != nil { 509 + l.Error("getting commit object", "error", err.Error()) 475 510 writeError(w, err.Error(), http.StatusInternalServerError) 476 511 return 477 512 } 478 513 514 + defaultBranch, err := gr.FindMainBranch() 515 + isDefault := false 516 + if err != nil { 517 + l.Error("getting default branch", "error", err.Error()) 518 + // do not quit though 519 + } else if defaultBranch == branchName { 520 + isDefault = true 521 + } 522 + 479 523 resp := types.RepoBranchResponse{ 480 524 Branch: types.Branch{ 481 525 Reference: types.Reference{ 482 526 Name: ref.Name().Short(), 483 527 Hash: ref.Hash().String(), 484 528 }, 529 + Commit: commit, 530 + IsDefault: isDefault, 485 531 }, 486 532 } 487 533 ··· 553 599 did := data.Did 554 600 name := data.Name 555 601 defaultBranch := data.DefaultBranch 602 + 603 + if err := validateRepoName(name); err != nil { 604 + l.Error("creating repo", "error", err.Error()) 605 + writeError(w, err.Error(), http.StatusBadRequest) 606 + return 607 + } 556 608 557 609 relativeRepoPath := filepath.Join(did, name) 558 610 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) ··· 829 881 l := h.l.With("handler", "NewHiddenRef") 830 882 831 883 forkRef := chi.URLParam(r, "forkRef") 884 + forkRef, _ = url.PathUnescape(forkRef) 885 + 832 886 remoteRef := chi.URLParam(r, "remoteRef") 887 + remoteRef, _ = url.PathUnescape(remoteRef) 888 + 833 889 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 834 890 gr, err := git.PlainOpen(path) 835 891 if err != nil { ··· 1028 1084 func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1029 1085 w.Write([]byte("ok")) 1030 1086 } 1087 + 1088 + func validateRepoName(name string) error { 1089 + // check for path traversal attempts 1090 + if name == "." || name == ".." || 1091 + strings.Contains(name, "/") || strings.Contains(name, "\\") { 1092 + return fmt.Errorf("Repository name contains invalid path characters") 1093 + } 1094 + 1095 + // check for sequences that could be used for traversal when normalized 1096 + if strings.Contains(name, "./") || strings.Contains(name, "../") || 1097 + strings.HasPrefix(name, ".") || strings.HasSuffix(name, ".") { 1098 + return fmt.Errorf("Repository name contains invalid path sequence") 1099 + } 1100 + 1101 + // then continue with character validation 1102 + for _, char := range name { 1103 + if !((char >= 'a' && char <= 'z') || 1104 + (char >= 'A' && char <= 'Z') || 1105 + (char >= '0' && char <= '9') || 1106 + char == '-' || char == '_' || char == '.') { 1107 + return fmt.Errorf("Repository name can only contain alphanumeric characters, periods, hyphens, and underscores") 1108 + } 1109 + } 1110 + 1111 + // additional check to prevent multiple sequential dots 1112 + if strings.Contains(name, "..") { 1113 + return fmt.Errorf("Repository name cannot contain sequential dots") 1114 + } 1115 + 1116 + // if all checks pass 1117 + return nil 1118 + }
+72
lexicons/actor/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.actor.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A declaration of a Tangled account profile.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "bluesky" 13 + ], 14 + "properties": { 15 + "description": { 16 + "type": "string", 17 + "description": "Free-form profile description text.", 18 + "maxGraphemes": 256, 19 + "maxLength": 2560 20 + }, 21 + "links": { 22 + "type": "array", 23 + "minLength": 0, 24 + "maxLength": 5, 25 + "items": { 26 + "type": "string", 27 + "description": "Any URI, intended for social profiles or websites, can be used to link DIDs/AT-URIs too.", 28 + "format": "uri" 29 + } 30 + }, 31 + "stats": { 32 + "type": "array", 33 + "minLength": 0, 34 + "maxLength": 2, 35 + "items": { 36 + "type": "string", 37 + "description": "Vanity stats.", 38 + "enum": [ 39 + "merged-pull-request-count", 40 + "closed-pull-request-count", 41 + "open-pull-request-count", 42 + "open-issue-count", 43 + "closed-issue-count", 44 + "repository-count" 45 + ] 46 + } 47 + }, 48 + "bluesky": { 49 + "type": "boolean", 50 + "description": "Include link to this account on Bluesky." 51 + }, 52 + "location": { 53 + "type": "string", 54 + "description": "Free-form location text.", 55 + "maxGraphemes": 40, 56 + "maxLength": 400 57 + }, 58 + "pinnedRepositories": { 59 + "type": "array", 60 + "description": "Any ATURI, it is up to appviews to validate these fields.", 61 + "minLength": 0, 62 + "maxLength": 6, 63 + "items": { 64 + "type": "string", 65 + "format": "at-uri" 66 + } 67 + } 68 + } 69 + } 70 + } 71 + } 72 + }
+52
lexicons/artifact.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.repo.artifact", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "name", 14 + "repo", 15 + "tag", 16 + "createdAt", 17 + "artifact" 18 + ], 19 + "properties": { 20 + "name": { 21 + "type": "string", 22 + "description": "name of the artifact" 23 + }, 24 + "repo": { 25 + "type": "string", 26 + "format": "at-uri", 27 + "description": "repo that this artifact is being uploaded to" 28 + }, 29 + "tag": { 30 + "type": "bytes", 31 + "description": "hash of the tag object that this artifact is attached to (only annotated tags are supported)", 32 + "minLength": 20, 33 + "maxLength": 20 34 + }, 35 + "createdAt": { 36 + "type": "string", 37 + "format": "datetime", 38 + "description": "time of creation of this artifact" 39 + }, 40 + "artifact": { 41 + "type": "blob", 42 + "description": "the artifact", 43 + "accept": [ 44 + "*/*" 45 + ], 46 + "maxSize": 52428800 47 + } 48 + } 49 + } 50 + } 51 + } 52 + }
+29
lexicons/feed/star.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.feed.star", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "subject", 14 + "createdAt" 15 + ], 16 + "properties": { 17 + "subject": { 18 + "type": "string", 19 + "format": "at-uri" 20 + }, 21 + "createdAt": { 22 + "type": "string", 23 + "format": "datetime" 24 + } 25 + } 26 + } 27 + } 28 + } 29 + }
-30
lexicons/follow.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.graph.follow", 4 - "needsCbor": true, 5 - "needsType": true, 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "createdAt", 14 - "subject" 15 - ], 16 - "properties": { 17 - "createdAt": { 18 - "type": "string", 19 - "format": "datetime" 20 - }, 21 - "subject": { 22 - "type": "string", 23 - "format": "did" 24 - } 25 - } 26 - } 27 - } 28 - } 29 - } 30 -
+29
lexicons/graph/follow.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.graph.follow", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "subject", 14 + "createdAt" 15 + ], 16 + "properties": { 17 + "subject": { 18 + "type": "string", 19 + "format": "did" 20 + }, 21 + "createdAt": { 22 + "type": "string", 23 + "format": "datetime" 24 + } 25 + } 26 + } 27 + } 28 + } 29 + }
+5 -1
lexicons/issue/comment.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["issue"], 12 + "required": [ 13 + "issue", 14 + "body", 15 + "createdAt" 16 + ], 13 17 "properties": { 14 18 "issue": { 15 19 "type": "string",
+7 -1
lexicons/issue/issue.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["repo", "issueId", "owner", "title"], 12 + "required": [ 13 + "repo", 14 + "issueId", 15 + "owner", 16 + "title", 17 + "createdAt" 18 + ], 13 19 "properties": { 14 20 "repo": { 15 21 "type": "string",
+4 -1
lexicons/issue/state.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["issue"], 12 + "required": [ 13 + "issue", 14 + "state" 15 + ], 13 16 "properties": { 14 17 "issue": { 15 18 "type": "string",
+34
lexicons/knot/member.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.knot.member", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "subject", 14 + "domain", 15 + "createdAt" 16 + ], 17 + "properties": { 18 + "subject": { 19 + "type": "string", 20 + "format": "did" 21 + }, 22 + "domain": { 23 + "type": "string", 24 + "description": "domain that this member now belongs to" 25 + }, 26 + "createdAt": { 27 + "type": "string", 28 + "format": "datetime" 29 + } 30 + } 31 + } 32 + } 33 + } 34 + }
-33
lexicons/member.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.knot.member", 4 - "needsCbor": true, 5 - "needsType": true, 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "member", 14 - "domain" 15 - ], 16 - "properties": { 17 - "member": { 18 - "type": "string", 19 - "format": "did" 20 - }, 21 - "domain": { 22 - "type": "string", 23 - "description": "domain that this member now belongs to" 24 - }, 25 - "addedAt": { 26 - "type": "string", 27 - "format": "datetime" 28 - } 29 - } 30 - } 31 - } 32 - } 33 - }
+2 -4
lexicons/publicKey.json
··· 12 12 "required": [ 13 13 "key", 14 14 "name", 15 - "created" 15 + "createdAt" 16 16 ], 17 17 "properties": { 18 18 "key": { 19 19 "type": "string", 20 20 "maxLength": 4096, 21 - "maxGraphemes": 4096, 22 21 "description": "public key contents" 23 22 }, 24 23 "name": { 25 24 "type": "string", 26 - "format": "string", 27 25 "description": "human-readable name for this key" 28 26 }, 29 - "created": { 27 + "createdAt": { 30 28 "type": "string", 31 29 "format": "datetime", 32 30 "description": "key upload timestamp"
+5 -1
lexicons/pulls/comment.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["pull"], 12 + "required": [ 13 + "pull", 14 + "body", 15 + "createdAt" 16 + ], 13 17 "properties": { 14 18 "pull": { 15 19 "type": "string",
+9 -6
lexicons/pulls/pull.json
··· 14 14 "targetBranch", 15 15 "pullId", 16 16 "title", 17 - "patch" 17 + "patch", 18 + "createdAt" 18 19 ], 19 20 "properties": { 20 21 "targetRepo": { ··· 33 34 "body": { 34 35 "type": "string" 35 36 }, 36 - "createdAt": { 37 - "type": "string", 38 - "format": "datetime" 39 - }, 40 37 "patch": { 41 38 "type": "string" 42 39 }, 43 40 "source": { 44 41 "type": "ref", 45 42 "ref": "#source" 43 + }, 44 + "createdAt": { 45 + "type": "string", 46 + "format": "datetime" 46 47 } 47 48 } 48 49 } 49 50 }, 50 51 "source": { 51 52 "type": "object", 52 - "required": ["branch"], 53 + "required": [ 54 + "branch" 55 + ], 53 56 "properties": { 54 57 "branch": { 55 58 "type": "string"
+4 -1
lexicons/pulls/state.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["pull"], 12 + "required": [ 13 + "pull", 14 + "status" 15 + ], 13 16 "properties": { 14 17 "pull": { 15 18 "type": "string",
+12 -7
lexicons/repo.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["name", "knot", "owner"], 12 + "required": [ 13 + "name", 14 + "knot", 15 + "owner", 16 + "createdAt" 17 + ], 13 18 "properties": { 14 19 "name": { 15 20 "type": "string", ··· 23 28 "type": "string", 24 29 "description": "knot where the repo was created" 25 30 }, 26 - "addedAt": { 27 - "type": "string", 28 - "format": "datetime" 29 - }, 30 31 "description": { 31 32 "type": "string", 32 33 "format": "datetime", 33 - "minLength": 1, 34 - "maxLength": 140 34 + "minGraphemes": 1, 35 + "maxGraphemes": 140 35 36 }, 36 37 "source": { 37 38 "type": "string", 38 39 "format": "uri", 39 40 "description": "source of the repo" 41 + }, 42 + "createdAt": { 43 + "type": "string", 44 + "format": "datetime" 40 45 } 41 46 } 42 47 }
-31
lexicons/star.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "sh.tangled.feed.star", 4 - "needsCbor": true, 5 - "needsType": true, 6 - "defs": { 7 - "main": { 8 - "type": "record", 9 - "key": "tid", 10 - "record": { 11 - "type": "object", 12 - "required": [ 13 - "createdAt", 14 - "subject" 15 - ], 16 - "properties": { 17 - "createdAt": { 18 - "type": "string", 19 - "format": "datetime" 20 - }, 21 - "subject": { 22 - "type": "string", 23 - "format": "at-uri" 24 - } 25 - } 26 - } 27 - } 28 - } 29 - } 30 - 31 -
+5 -1
patchutil/combinediff.go
··· 122 122 fmt.Println(err) 123 123 } 124 124 125 - result = append(result, combined) 125 + // combined can be nil commit 2 reverted all changes from commit 1 126 + if combined != nil { 127 + result = append(result, combined) 128 + } 129 + 126 130 } else { 127 131 // only in patch1; add as-is 128 132 result = append(result, f1)
+8
patchutil/interdiff.go
··· 11 11 Files []*InterdiffFile 12 12 } 13 13 14 + func (i *InterdiffResult) AffectedFiles() []string { 15 + files := make([]string, len(i.Files)) 16 + for _, f := range i.Files { 17 + files = append(files, f.Name) 18 + } 19 + return files 20 + } 21 + 14 22 func (i *InterdiffResult) String() string { 15 23 var b strings.Builder 16 24 for _, f := range i.Files {
+5
scripts/generate-jwks.sh
··· 1 + #! /usr/bin/env bash 2 + 3 + set -e 4 + 5 + go run ./cmd/genjwks/
+14
types/diff.go
··· 59 59 Patch string `json:"patch"` 60 60 Diff []*gitdiff.File `json:"diff"` 61 61 } 62 + 63 + func (d *NiceDiff) ChangedFiles() []string { 64 + files := make([]string, len(d.Diff)) 65 + 66 + for i, f := range d.Diff { 67 + if f.IsDelete { 68 + files[i] = f.Name.Old 69 + } else { 70 + files[i] = f.Name.New 71 + } 72 + } 73 + 74 + return files 75 + }
+2
types/repo.go
··· 61 61 62 62 type Branch struct { 63 63 Reference `json:"reference"` 64 + Commit *object.Commit `json:"commit,omitempty"` 65 + IsDefault bool `json:"is_deafult,omitempty"` 64 66 } 65 67 66 68 type RepoTagsResponse struct {