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

Compare changes

Choose any two refs to compare.

Changed files
+13307 -2251
.air
api
appview
avatar
camo
cmd
appview
genjwks
docker
docs
knotserver
lexicons
patchutil
scripts
+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"]
+6
.gitignore
··· 7 7 result 8 8 !.gitkeep 9 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 + }
+776
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" ··· 3098 3099 3099 3100 return nil 3100 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 + { 3788 + b, err := cr.ReadByte() 3789 + if err != nil { 3790 + return err 3791 + } 3792 + if b != cbg.CborNull[0] { 3793 + if err := cr.UnreadByte(); err != nil { 3794 + return err 3795 + } 3796 + 3797 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3798 + if err != nil { 3799 + return err 3800 + } 3801 + 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 + 3864 + } 3865 + } 3866 + 3867 + default: 3868 + // Field doesn't exist on this type, so ignore it 3869 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 3870 + return err 3871 + } 3872 + } 3873 + } 3874 + 3875 + return nil 3876 + }
+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 + }
-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(
+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
+2 -12
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 "tangled.sh/tangled.sh/core/api/tangled" 13 + "tangled.sh/tangled.sh/core/api/tangled" 14 14 "tangled.sh/tangled.sh/core/patchutil" 15 15 "tangled.sh/tangled.sh/core/types" 16 16 ) ··· 235 235 } 236 236 237 237 func NewPull(tx *sql.Tx, pull *Pull) error { 238 - defer tx.Rollback() 239 - 240 238 _, err := tx.Exec(` 241 239 insert or ignore into repo_pull_seqs (repo_at, next_pull_id) 242 240 values (?, 1) ··· 291 289 insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 292 290 values (?, ?, ?, ?, ?) 293 291 `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 294 - if err != nil { 295 - return err 296 - } 297 - 298 - if err := tx.Commit(); err != nil { 299 - return err 300 - } 301 - 302 - return nil 292 + return err 303 293 } 304 294 305 295 func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
+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(
+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 + }
+5 -58
appview/middleware/middleware.go
··· 5 5 "log" 6 6 "net/http" 7 7 "strconv" 8 - "time" 9 8 10 - comatproto "github.com/bluesky-social/indigo/api/atproto" 11 - "github.com/bluesky-social/indigo/xrpc" 12 - "tangled.sh/tangled.sh/core/appview" 13 - "tangled.sh/tangled.sh/core/appview/auth" 9 + "tangled.sh/tangled.sh/core/appview/oauth" 14 10 "tangled.sh/tangled.sh/core/appview/pagination" 15 11 ) 16 12 17 13 type Middleware func(http.Handler) http.Handler 18 14 19 - func AuthMiddleware(a *auth.Auth) Middleware { 15 + func AuthMiddleware(a *oauth.OAuth) Middleware { 20 16 return func(next http.Handler) http.Handler { 21 17 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 18 redirectFunc := func(w http.ResponseWriter, r *http.Request) { ··· 29 25 } 30 26 } 31 27 32 - session, err := a.GetSession(r) 33 - if session.IsNew || err != nil { 28 + _, auth, err := a.GetSession(r) 29 + if err != nil { 34 30 log.Printf("not logged in, redirecting") 35 31 redirectFunc(w, r) 36 32 return 37 33 } 38 34 39 - authorized, ok := session.Values[appview.SessionAuthenticated].(bool) 40 - if !ok || !authorized { 35 + if !auth { 41 36 log.Printf("not logged in, redirecting") 42 37 redirectFunc(w, r) 43 38 return 44 - } 45 - 46 - // refresh if nearing expiry 47 - // TODO: dedup with /login 48 - expiryStr := session.Values[appview.SessionExpiry].(string) 49 - expiry, err := time.Parse(time.RFC3339, expiryStr) 50 - if err != nil { 51 - log.Println("invalid expiry time", err) 52 - redirectFunc(w, r) 53 - return 54 - } 55 - pdsUrl, ok1 := session.Values[appview.SessionPds].(string) 56 - did, ok2 := session.Values[appview.SessionDid].(string) 57 - refreshJwt, ok3 := session.Values[appview.SessionRefreshJwt].(string) 58 - 59 - if !ok1 || !ok2 || !ok3 { 60 - log.Println("invalid expiry time", err) 61 - redirectFunc(w, r) 62 - return 63 - } 64 - 65 - if time.Now().After(expiry) { 66 - log.Println("token expired, refreshing ...") 67 - 68 - client := xrpc.Client{ 69 - Host: pdsUrl, 70 - Auth: &xrpc.AuthInfo{ 71 - Did: did, 72 - AccessJwt: refreshJwt, 73 - RefreshJwt: refreshJwt, 74 - }, 75 - } 76 - atSession, err := comatproto.ServerRefreshSession(r.Context(), &client) 77 - if err != nil { 78 - log.Println("failed to refresh session", err) 79 - redirectFunc(w, r) 80 - return 81 - } 82 - 83 - sessionish := auth.RefreshSessionWrapper{atSession} 84 - 85 - err = a.StoreSession(r, w, &sessionish, pdsUrl) 86 - if err != nil { 87 - log.Printf("failed to store session for did: %s\n: %s", atSession.Did, err) 88 - return 89 - } 90 - 91 - log.Println("successfully refreshed token") 92 39 } 93 40 94 41 next.ServeHTTP(w, r)
+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 + }
+3 -1
appview/pages/funcmap.go
··· 13 13 "time" 14 14 15 15 "github.com/dustin/go-humanize" 16 + "github.com/microcosm-cc/bluemonday" 16 17 "tangled.sh/tangled.sh/core/appview/filetree" 17 18 "tangled.sh/tangled.sh/core/appview/pages/markup" 18 19 ) ··· 143 144 return v.Slice(start, end).Interface() 144 145 }, 145 146 "markdown": func(text string) template.HTML { 146 - return template.HTML(markup.RenderMarkdown(text)) 147 + rctx := &markup.RenderContext{RendererType: markup.RendererTypeDefault} 148 + return template.HTML(bluemonday.UGCPolicy().Sanitize(rctx.RenderMarkdown(text))) 147 149 }, 148 150 "isNil": func(t any) bool { 149 151 // returns false for other "zero" values
+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 - }
+137 -175
appview/pages/pages.go
··· 12 12 "log" 13 13 "net/http" 14 14 "os" 15 - "path" 16 15 "path/filepath" 17 - "slices" 18 16 "strings" 19 17 20 - "tangled.sh/tangled.sh/core/appview/auth" 18 + "tangled.sh/tangled.sh/core/appview" 21 19 "tangled.sh/tangled.sh/core/appview/db" 20 + "tangled.sh/tangled.sh/core/appview/oauth" 22 21 "tangled.sh/tangled.sh/core/appview/pages/markup" 22 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 23 23 "tangled.sh/tangled.sh/core/appview/pagination" 24 - "tangled.sh/tangled.sh/core/appview/state/userutil" 25 24 "tangled.sh/tangled.sh/core/patchutil" 26 25 "tangled.sh/tangled.sh/core/types" 27 26 ··· 30 29 "github.com/alecthomas/chroma/v2/lexers" 31 30 "github.com/alecthomas/chroma/v2/styles" 32 31 "github.com/bluesky-social/indigo/atproto/syntax" 32 + "github.com/go-git/go-git/v5/plumbing" 33 33 "github.com/go-git/go-git/v5/plumbing/object" 34 34 "github.com/microcosm-cc/bluemonday" 35 35 ) ··· 42 42 dev bool 43 43 embedFS embed.FS 44 44 templateDir string // Path to templates on disk for dev mode 45 + rctx *markup.RenderContext 45 46 } 46 47 47 - func NewPages(dev bool) *Pages { 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 + } 55 + 48 56 p := &Pages{ 49 57 t: make(map[string]*template.Template), 50 - dev: dev, 58 + dev: config.Core.Dev, 51 59 embedFS: Files, 60 + rctx: rctx, 52 61 templateDir: "appview/pages", 53 62 } 54 63 ··· 241 250 } 242 251 243 252 type TimelineParams struct { 244 - LoggedInUser *auth.User 253 + LoggedInUser *oauth.User 245 254 Timeline []db.TimelineEvent 246 255 DidHandleMap map[string]string 247 256 } ··· 251 260 } 252 261 253 262 type SettingsParams struct { 254 - LoggedInUser *auth.User 263 + LoggedInUser *oauth.User 255 264 PubKeys []db.PublicKey 256 265 Emails []db.Email 257 266 } ··· 261 270 } 262 271 263 272 type KnotsParams struct { 264 - LoggedInUser *auth.User 273 + LoggedInUser *oauth.User 265 274 Registrations []db.Registration 266 275 } 267 276 ··· 270 279 } 271 280 272 281 type KnotParams struct { 273 - LoggedInUser *auth.User 282 + LoggedInUser *oauth.User 274 283 DidHandleMap map[string]string 275 284 Registration *db.Registration 276 285 Members []string ··· 282 291 } 283 292 284 293 type NewRepoParams struct { 285 - LoggedInUser *auth.User 294 + LoggedInUser *oauth.User 286 295 Knots []string 287 296 } 288 297 ··· 291 300 } 292 301 293 302 type ForkRepoParams struct { 294 - LoggedInUser *auth.User 303 + LoggedInUser *oauth.User 295 304 Knots []string 296 - RepoInfo RepoInfo 305 + RepoInfo repoinfo.RepoInfo 297 306 } 298 307 299 308 func (p *Pages) ForkRepo(w io.Writer, params ForkRepoParams) error { ··· 301 310 } 302 311 303 312 type ProfilePageParams struct { 304 - LoggedInUser *auth.User 305 - UserDid string 306 - UserHandle string 313 + LoggedInUser *oauth.User 307 314 Repos []db.Repo 308 315 CollaboratingRepos []db.Repo 309 - ProfileStats ProfileStats 310 - FollowStatus db.FollowStatus 311 - AvatarUri string 312 316 ProfileTimeline *db.ProfileTimeline 317 + Card ProfileCard 313 318 314 319 DidHandleMap map[string]string 315 320 } 316 321 317 - type ProfileStats struct { 318 - Followers int 319 - Following int 320 - } 321 - 322 - func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 323 - return p.execute("user/profile", w, params) 324 - } 325 - 326 - type FollowFragmentParams struct { 322 + type ProfileCard struct { 327 323 UserDid string 324 + UserHandle string 328 325 FollowStatus db.FollowStatus 329 - } 330 - 331 - func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 332 - return p.executePlain("user/fragments/follow", w, params) 333 - } 334 - 335 - type RepoActionsFragmentParams struct { 336 - IsStarred bool 337 - RepoAt syntax.ATURI 338 - Stats db.RepoStats 339 - } 340 - 341 - func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error { 342 - return p.executePlain("repo/fragments/repoActions", w, params) 343 - } 326 + AvatarUri string 327 + Followers int 328 + Following int 344 329 345 - type RepoDescriptionParams struct { 346 - RepoInfo RepoInfo 330 + Profile *db.Profile 347 331 } 348 332 349 - func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 350 - return p.executePlain("repo/fragments/editRepoDescription", w, params) 333 + func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error { 334 + return p.execute("user/profile", w, params) 351 335 } 352 336 353 - func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 354 - return p.executePlain("repo/fragments/repoDescription", w, params) 355 - } 337 + type ReposPageParams struct { 338 + LoggedInUser *oauth.User 339 + Repos []db.Repo 340 + Card ProfileCard 356 341 357 - type RepoInfo struct { 358 - Name string 359 - OwnerDid string 360 - OwnerHandle string 361 - Description string 362 - Knot string 363 - RepoAt syntax.ATURI 364 - IsStarred bool 365 - Stats db.RepoStats 366 - Roles RolesInRepo 367 - Source *db.Repo 368 - SourceHandle string 369 - DisableFork bool 342 + DidHandleMap map[string]string 370 343 } 371 344 372 - type RolesInRepo struct { 373 - Roles []string 345 + func (p *Pages) ReposPage(w io.Writer, params ReposPageParams) error { 346 + return p.execute("user/repos", w, params) 374 347 } 375 348 376 - func (r RolesInRepo) SettingsAllowed() bool { 377 - return slices.Contains(r.Roles, "repo:settings") 349 + type FollowFragmentParams struct { 350 + UserDid string 351 + FollowStatus db.FollowStatus 378 352 } 379 353 380 - func (r RolesInRepo) CollaboratorInviteAllowed() bool { 381 - return slices.Contains(r.Roles, "repo:invite") 354 + func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 355 + return p.executePlain("user/fragments/follow", w, params) 382 356 } 383 357 384 - func (r RolesInRepo) RepoDeleteAllowed() bool { 385 - return slices.Contains(r.Roles, "repo:delete") 358 + type EditBioParams struct { 359 + LoggedInUser *oauth.User 360 + Profile *db.Profile 386 361 } 387 362 388 - func (r RolesInRepo) IsOwner() bool { 389 - return slices.Contains(r.Roles, "repo:owner") 363 + func (p *Pages) EditBioFragment(w io.Writer, params EditBioParams) error { 364 + return p.executePlain("user/fragments/editBio", w, params) 390 365 } 391 366 392 - func (r RolesInRepo) IsCollaborator() bool { 393 - return slices.Contains(r.Roles, "repo:collaborator") 367 + type EditPinsParams struct { 368 + LoggedInUser *oauth.User 369 + Profile *db.Profile 370 + AllRepos []PinnedRepo 371 + DidHandleMap map[string]string 394 372 } 395 373 396 - func (r RolesInRepo) IsPushAllowed() bool { 397 - return slices.Contains(r.Roles, "repo:push") 374 + type PinnedRepo struct { 375 + IsPinned bool 376 + db.Repo 398 377 } 399 378 400 - func (r RepoInfo) OwnerWithAt() string { 401 - if r.OwnerHandle != "" { 402 - return fmt.Sprintf("@%s", r.OwnerHandle) 403 - } else { 404 - return r.OwnerDid 405 - } 379 + func (p *Pages) EditPinsFragment(w io.Writer, params EditPinsParams) error { 380 + return p.executePlain("user/fragments/editPins", w, params) 406 381 } 407 382 408 - func (r RepoInfo) FullName() string { 409 - return path.Join(r.OwnerWithAt(), r.Name) 383 + type RepoActionsFragmentParams struct { 384 + IsStarred bool 385 + RepoAt syntax.ATURI 386 + Stats db.RepoStats 410 387 } 411 388 412 - func (r RepoInfo) OwnerWithoutAt() string { 413 - if strings.HasPrefix(r.OwnerWithAt(), "@") { 414 - return strings.TrimPrefix(r.OwnerWithAt(), "@") 415 - } else { 416 - return userutil.FlattenDid(r.OwnerDid) 417 - } 389 + func (p *Pages) RepoActionsFragment(w io.Writer, params RepoActionsFragmentParams) error { 390 + return p.executePlain("repo/fragments/repoActions", w, params) 418 391 } 419 392 420 - func (r RepoInfo) FullNameWithoutAt() string { 421 - return path.Join(r.OwnerWithoutAt(), r.Name) 393 + type RepoDescriptionParams struct { 394 + RepoInfo repoinfo.RepoInfo 422 395 } 423 396 424 - func (r RepoInfo) GetTabs() [][]string { 425 - tabs := [][]string{ 426 - {"overview", "/", "square-chart-gantt"}, 427 - {"issues", "/issues", "circle-dot"}, 428 - {"pulls", "/pulls", "git-pull-request"}, 429 - } 430 - 431 - if r.Roles.SettingsAllowed() { 432 - tabs = append(tabs, []string{"settings", "/settings", "cog"}) 433 - } 434 - 435 - return tabs 397 + func (p *Pages) EditRepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 398 + return p.executePlain("repo/fragments/editRepoDescription", w, params) 436 399 } 437 400 438 - // each tab on a repo could have some metadata: 439 - // 440 - // issues -> number of open issues etc. 441 - // settings -> a warning icon to setup branch protection? idk 442 - // 443 - // we gather these bits of info here, because go templates 444 - // are difficult to program in 445 - func (r RepoInfo) TabMetadata() map[string]any { 446 - meta := make(map[string]any) 447 - 448 - if r.Stats.PullCount.Open > 0 { 449 - meta["pulls"] = r.Stats.PullCount.Open 450 - } 451 - 452 - if r.Stats.IssueCount.Open > 0 { 453 - meta["issues"] = r.Stats.IssueCount.Open 454 - } 455 - 456 - // more stuff? 457 - 458 - return meta 401 + func (p *Pages) RepoDescriptionFragment(w io.Writer, params RepoDescriptionParams) error { 402 + return p.executePlain("repo/fragments/repoDescription", w, params) 459 403 } 460 404 461 405 type RepoIndexParams struct { 462 - LoggedInUser *auth.User 463 - RepoInfo RepoInfo 406 + LoggedInUser *oauth.User 407 + RepoInfo repoinfo.RepoInfo 464 408 Active string 465 409 TagMap map[string][]string 466 410 CommitsTrunc []*object.Commit ··· 477 421 if params.IsEmpty { 478 422 return p.executeRepo("repo/empty", w, params) 479 423 } 424 + 425 + p.rctx.RepoInfo = params.RepoInfo 426 + p.rctx.RendererType = markup.RendererTypeRepoMarkdown 480 427 481 428 if params.ReadmeFileName != "" { 482 429 var htmlString string 483 430 ext := filepath.Ext(params.ReadmeFileName) 484 431 switch ext { 485 432 case ".md", ".markdown", ".mdown", ".mkdn", ".mkd": 486 - htmlString = markup.RenderMarkdown(params.Readme) 433 + htmlString = p.rctx.RenderMarkdown(params.Readme) 487 434 params.Raw = false 488 435 params.HTMLReadme = template.HTML(bluemonday.UGCPolicy().Sanitize(htmlString)) 489 436 default: ··· 497 444 } 498 445 499 446 type RepoLogParams struct { 500 - LoggedInUser *auth.User 501 - RepoInfo RepoInfo 447 + LoggedInUser *oauth.User 448 + RepoInfo repoinfo.RepoInfo 449 + TagMap map[string][]string 502 450 types.RepoLogResponse 503 451 Active string 504 452 EmailToDidOrHandle map[string]string ··· 510 458 } 511 459 512 460 type RepoCommitParams struct { 513 - LoggedInUser *auth.User 514 - RepoInfo RepoInfo 461 + LoggedInUser *oauth.User 462 + RepoInfo repoinfo.RepoInfo 515 463 Active string 516 464 EmailToDidOrHandle map[string]string 517 465 ··· 524 472 } 525 473 526 474 type RepoTreeParams struct { 527 - LoggedInUser *auth.User 528 - RepoInfo RepoInfo 475 + LoggedInUser *oauth.User 476 + RepoInfo repoinfo.RepoInfo 529 477 Active string 530 478 BreadCrumbs [][]string 531 479 BaseTreeLink string ··· 560 508 } 561 509 562 510 type RepoBranchesParams struct { 563 - LoggedInUser *auth.User 564 - RepoInfo RepoInfo 511 + LoggedInUser *oauth.User 512 + RepoInfo repoinfo.RepoInfo 565 513 Active string 566 514 types.RepoBranchesResponse 567 515 } ··· 572 520 } 573 521 574 522 type RepoTagsParams struct { 575 - LoggedInUser *auth.User 576 - RepoInfo RepoInfo 523 + LoggedInUser *oauth.User 524 + RepoInfo repoinfo.RepoInfo 577 525 Active string 578 526 types.RepoTagsResponse 527 + ArtifactMap map[plumbing.Hash][]db.Artifact 528 + DanglingArtifacts []db.Artifact 579 529 } 580 530 581 531 func (p *Pages) RepoTags(w io.Writer, params RepoTagsParams) error { ··· 583 533 return p.executeRepo("repo/tags", w, params) 584 534 } 585 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 + 586 546 type RepoBlobParams struct { 587 - LoggedInUser *auth.User 588 - RepoInfo RepoInfo 547 + LoggedInUser *oauth.User 548 + RepoInfo repoinfo.RepoInfo 589 549 Active string 590 550 BreadCrumbs [][]string 591 551 ShowRendered bool ··· 600 560 if params.ShowRendered { 601 561 switch markup.GetFormat(params.Path) { 602 562 case markup.FormatMarkdown: 603 - 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))) 604 566 } 605 567 } 606 568 ··· 644 606 } 645 607 646 608 type RepoSettingsParams struct { 647 - LoggedInUser *auth.User 648 - RepoInfo RepoInfo 609 + LoggedInUser *oauth.User 610 + RepoInfo repoinfo.RepoInfo 649 611 Collaborators []Collaborator 650 612 Active string 651 613 Branches []string ··· 660 622 } 661 623 662 624 type RepoIssuesParams struct { 663 - LoggedInUser *auth.User 664 - RepoInfo RepoInfo 625 + LoggedInUser *oauth.User 626 + RepoInfo repoinfo.RepoInfo 665 627 Active string 666 628 Issues []db.Issue 667 629 DidHandleMap map[string]string ··· 675 637 } 676 638 677 639 type RepoSingleIssueParams struct { 678 - LoggedInUser *auth.User 679 - RepoInfo RepoInfo 640 + LoggedInUser *oauth.User 641 + RepoInfo repoinfo.RepoInfo 680 642 Active string 681 643 Issue db.Issue 682 644 Comments []db.Comment ··· 697 659 } 698 660 699 661 type RepoNewIssueParams struct { 700 - LoggedInUser *auth.User 701 - RepoInfo RepoInfo 662 + LoggedInUser *oauth.User 663 + RepoInfo repoinfo.RepoInfo 702 664 Active string 703 665 } 704 666 ··· 708 670 } 709 671 710 672 type EditIssueCommentParams struct { 711 - LoggedInUser *auth.User 712 - RepoInfo RepoInfo 673 + LoggedInUser *oauth.User 674 + RepoInfo repoinfo.RepoInfo 713 675 Issue *db.Issue 714 676 Comment *db.Comment 715 677 } ··· 719 681 } 720 682 721 683 type SingleIssueCommentParams struct { 722 - LoggedInUser *auth.User 684 + LoggedInUser *oauth.User 723 685 DidHandleMap map[string]string 724 - RepoInfo RepoInfo 686 + RepoInfo repoinfo.RepoInfo 725 687 Issue *db.Issue 726 688 Comment *db.Comment 727 689 } ··· 731 693 } 732 694 733 695 type RepoNewPullParams struct { 734 - LoggedInUser *auth.User 735 - RepoInfo RepoInfo 696 + LoggedInUser *oauth.User 697 + RepoInfo repoinfo.RepoInfo 736 698 Branches []types.Branch 737 699 Active string 738 700 } ··· 743 705 } 744 706 745 707 type RepoPullsParams struct { 746 - LoggedInUser *auth.User 747 - RepoInfo RepoInfo 708 + LoggedInUser *oauth.User 709 + RepoInfo repoinfo.RepoInfo 748 710 Pulls []*db.Pull 749 711 Active string 750 712 DidHandleMap map[string]string ··· 775 737 } 776 738 777 739 type RepoSinglePullParams struct { 778 - LoggedInUser *auth.User 779 - RepoInfo RepoInfo 740 + LoggedInUser *oauth.User 741 + RepoInfo repoinfo.RepoInfo 780 742 Active string 781 743 DidHandleMap map[string]string 782 744 Pull *db.Pull ··· 790 752 } 791 753 792 754 type RepoPullPatchParams struct { 793 - LoggedInUser *auth.User 755 + LoggedInUser *oauth.User 794 756 DidHandleMap map[string]string 795 - RepoInfo RepoInfo 757 + RepoInfo repoinfo.RepoInfo 796 758 Pull *db.Pull 797 759 Diff *types.NiceDiff 798 760 Round int ··· 805 767 } 806 768 807 769 type RepoPullInterdiffParams struct { 808 - LoggedInUser *auth.User 770 + LoggedInUser *oauth.User 809 771 DidHandleMap map[string]string 810 - RepoInfo RepoInfo 772 + RepoInfo repoinfo.RepoInfo 811 773 Pull *db.Pull 812 774 Round int 813 775 Interdiff *patchutil.InterdiffResult ··· 819 781 } 820 782 821 783 type PullPatchUploadParams struct { 822 - RepoInfo RepoInfo 784 + RepoInfo repoinfo.RepoInfo 823 785 } 824 786 825 787 func (p *Pages) PullPatchUploadFragment(w io.Writer, params PullPatchUploadParams) error { ··· 827 789 } 828 790 829 791 type PullCompareBranchesParams struct { 830 - RepoInfo RepoInfo 792 + RepoInfo repoinfo.RepoInfo 831 793 Branches []types.Branch 832 794 } 833 795 ··· 836 798 } 837 799 838 800 type PullCompareForkParams struct { 839 - RepoInfo RepoInfo 801 + RepoInfo repoinfo.RepoInfo 840 802 Forks []db.Repo 841 803 } 842 804 ··· 845 807 } 846 808 847 809 type PullCompareForkBranchesParams struct { 848 - RepoInfo RepoInfo 810 + RepoInfo repoinfo.RepoInfo 849 811 SourceBranches []types.Branch 850 812 TargetBranches []types.Branch 851 813 } ··· 855 817 } 856 818 857 819 type PullResubmitParams struct { 858 - LoggedInUser *auth.User 859 - RepoInfo RepoInfo 820 + LoggedInUser *oauth.User 821 + RepoInfo repoinfo.RepoInfo 860 822 Pull *db.Pull 861 823 SubmissionId int 862 824 } ··· 866 828 } 867 829 868 830 type PullActionsParams struct { 869 - LoggedInUser *auth.User 870 - RepoInfo RepoInfo 831 + LoggedInUser *oauth.User 832 + RepoInfo repoinfo.RepoInfo 871 833 Pull *db.Pull 872 834 RoundNumber int 873 835 MergeCheck types.MergeCheckResponse ··· 879 841 } 880 842 881 843 type PullNewCommentParams struct { 882 - LoggedInUser *auth.User 883 - RepoInfo RepoInfo 844 + LoggedInUser *oauth.User 845 + RepoInfo repoinfo.RepoInfo 884 846 Pull *db.Pull 885 847 RoundNumber int 886 848 }
+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 + }
+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>
+1 -1
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 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>
+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
+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>
+6 -6
appview/pages/templates/repo/fragments/filetree.html
··· 2 2 {{ if and .Name .IsDirectory }} 3 3 <details open> 4 4 <summary class="cursor-pointer list-none pt-1"> 5 - <span class="inline-flex items-center gap-2 "> 6 - {{ i "folder" "w-3 h-3 fill-current" }} 7 - <span class="text-black dark:text-white">{{ .Name }}</span> 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 8 </span> 9 9 </summary> 10 10 <div class="ml-1 pl-4 border-l border-gray-200 dark:border-gray-700"> ··· 14 14 </div> 15 15 </details> 16 16 {{ else if .Name }} 17 - <div class="flex items-center gap-2 pt-1"> 18 - {{ i "file" "w-3 h-3" }} 19 - <a href="#file-{{ .Path }}" class="text-black dark:text-white no-underline hover:underline">{{ .Name }}</a> 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 20 </div> 21 21 {{ else }} 22 22 {{ range $child := .Children }}
+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>
+8 -9
appview/pages/templates/repo/index.html
··· 103 103 class="{{ $linkstyle }}" 104 104 > 105 105 <div class="flex items-center gap-2"> 106 - {{ i "folder" "w-3 h-3 fill-current" }} 106 + {{ i "folder" "size-4 fill-current" }} 107 107 {{ .Name }} 108 108 </div> 109 109 </a> ··· 125 125 class="{{ $linkstyle }}" 126 126 > 127 127 <div class="flex items-center gap-2"> 128 - {{ i "file" "w-3 h-3" }}{{ .Name }} 128 + {{ i "file" "size-4" }}{{ .Name }} 129 129 </div> 130 130 </a> 131 131 ··· 151 151 <div id="commit-log" class="md:col-span-1 px-2 pb-4"> 152 152 <div class="flex justify-between items-center"> 153 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"> 155 - {{ i "git-commit-horizontal" "w-4 h-4" }} commits 154 + <div class="flex gap-2 items-center font-bold"> 155 + {{ i "logs" "w-4 h-4" }} commits 156 156 </div> 157 157 <span class="hidden group-hover:flex gap-2 items-center text-sm text-gray-500 dark:text-gray-400 "> 158 158 view {{ .TotalCommits }} commits {{ i "chevron-right" "w-4 h-4" }} ··· 248 248 {{ if gt (len .BranchesTrunc) 0 }} 249 249 <div id="branches" class="md:col-span-1 px-2 py-4 border-t border-gray-200 dark:border-gray-700"> 250 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"> 252 - <!-- git-branch icon is seemingly bigger than others at 4x4 --> 253 - {{ i "git-branch" "w-3 h-3" }} branches 251 + <div class="flex gap-2 items-center font-bold"> 252 + {{ i "git-branch" "w-4 h-4" }} branches 254 253 </div> 255 254 <span class="hidden group-hover:flex gap-2 items-center text-sm text-gray-500 dark:text-gray-400 "> 256 255 view {{ len .Branches }} branches {{ i "chevron-right" "w-4 h-4" }} ··· 283 282 <div id="tags" class="md:col-span-1 px-2 py-4 border-t border-gray-200 dark:border-gray-700"> 284 283 <div class="flex justify-between items-center"> 285 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"> 286 - <div class="flex gap-2 items-center"> 285 + <div class="flex gap-2 items-center font-bold"> 287 286 {{ i "tags" "w-4 h-4" }} tags 288 287 </div> 289 288 <span class="hidden group-hover:flex gap-2 items-center text-sm text-gray-500 dark:text-gray-400 "> ··· 321 320 {{ define "repoAfter" }} 322 321 {{- if .HTMLReadme }} 323 322 <section 324 - class="p-6 mt-4 rounded-br rounded-bl 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 }} 325 324 prose dark:prose-invert dark:[&_pre]:bg-gray-900 326 325 dark:[&_code]:text-gray-300 dark:[&_pre_code]:bg-gray-900 327 326 dark:[&_pre]:border dark:[&_pre]:border-gray-700
+26 -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" }}
+41 -16
appview/pages/templates/repo/log.html
··· 43 43 </td> 44 44 <td class=" py-3 align-top"> 45 45 <div> 46 - <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a> 47 - {{ if gt (len $messageParts) 1 }} 48 - <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.nextElementSibling.classList.toggle('hidden')">{{ i "ellipsis" "w-3 h-3" }}</button> 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 }} 51 + 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 }} 49 64 <p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p> 50 65 {{ end }} 51 - </div> 52 66 </td> 53 67 <td class=" py-3 align-top text-gray-500 dark:text-gray-400">{{ timeFmt $commit.Author.When }}</td> 54 68 </tr> 55 - {{ end }} 69 + {{ end }} 56 70 </tbody> 57 71 </table> 58 72 ··· 66 80 <div> 67 81 <div class="flex items-center justify-between"> 68 82 <div class="flex-1"> 69 - <a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" 70 - class="inline no-underline hover:underline dark:text-white"> 71 - {{ index $messageParts 0 }} 72 - </a> 73 - {{ if gt (len $messageParts) 1 }} 74 - <button 75 - class="py-1/2 px-1 bg-gray-200 hover:bg-gray-400 rounded dark:bg-gray-700 dark:hover:bg-gray-600" 76 - hx-on:click="this.nextElementSibling.classList.toggle('hidden')"> 77 - {{ i "ellipsis" "w-3 h-3" }} 78 - </button> 79 - {{ end }} 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> 88 + {{ if gt (len $messageParts) 1 }} 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" }} 93 + </button> 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 }} 103 + </div> 104 + 80 105 {{ if gt (len $messageParts) 1 }} 81 106 <p class="hidden mt-1 text-sm cursor-text pb-2 dark:text-gray-300"> 82 107 {{ nl2br (index $messageParts 1) }}
+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>
+9 -11
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 .Pull.IsForkBased }} 47 - {{ if .Pull.PullSource.Repo }} 48 - <a href="/{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}" class="no-underline hover:underline">{{ $owner }}/{{ .Pull.PullSource.Repo.Name }}</a> 49 - {{ else }} 50 - <span class="italic">[deleted fork]</span> 51 - {{ end }} 52 - {{ end }} 53 - 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 - {{ .Pull.PullSource.Branch }} 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>
+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 }}
+8 -4
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 }}
+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-['ยท']">
+146 -53
appview/pages/templates/repo/tags.html
··· 3 3 {{ end }} 4 4 5 5 {{ define "repoContent" }} 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-3 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"> 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> 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 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> 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 26 27 - <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 28 - <span>{{ .Tag.Tagger.Name }}</span> 27 + <span class="px-1 text-gray-500 dark:text-gray-400 select-none after:content-['ยท']"></span> 28 + <span>{{ .Tag.Tagger.Name }}</span> 29 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> 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 }} 34 33 </div> 34 + </div> 35 35 36 - <!-- Desktop layout: vertical and right-aligned --> 37 - <div class="hidden md:block text-right px-2 pb-6"> 38 - <a href="/{{ $.RepoInfo.FullName }}/tree/{{ .Name | urlquery }}" class="no-underline hover:underline flex items-center justify-end gap-2 font-bold"> 39 - {{ i "tag" "w-4 h-4" }} 40 - {{ .Name }} 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 }} 41 48 </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 justify-end 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> 49 + <span>{{ .Tag.Tagger.Name }}</span> 50 + <time>{{ timeFmt .Tag.Tagger.When }}</time> 51 + {{ end }} 53 52 </div> 54 53 </div> 54 + </div> 55 55 56 - <!-- Content column (bottom on mobile, right on md+) --> 57 - <div class="md:col-span-9 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">{{ index $messageParts 0 }}</p> 61 - {{ if gt (len $messageParts) 1 }} 62 - <p class="cursor-text pb-2 text-sm">{{ nl2br (index $messageParts 1) }}</p> 63 - {{ end }} 64 - {{ else }} 65 - <p class="italic text-gray-500 dark:text-gray-400">no message</p> 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> 66 63 {{ end }} 67 - </div> 64 + {{ block "artifacts" (list $ .) }} {{ end }} 65 + {{ else }} 66 + <p class="italic text-gray-500 dark:text-gray-400">no message</p> 67 + {{ end }} 68 68 </div> 69 - {{ end }} 70 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 }} 71 83 </section> 72 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 }} 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 >.
+81 -91
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 }} ··· 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 }}
+28 -19
appview/settings/settings.go
··· 13 13 "github.com/go-chi/chi/v5" 14 14 "tangled.sh/tangled.sh/core/api/tangled" 15 15 "tangled.sh/tangled.sh/core/appview" 16 - "tangled.sh/tangled.sh/core/appview/auth" 17 16 "tangled.sh/tangled.sh/core/appview/db" 18 17 "tangled.sh/tangled.sh/core/appview/email" 19 18 "tangled.sh/tangled.sh/core/appview/middleware" 19 + "tangled.sh/tangled.sh/core/appview/oauth" 20 20 "tangled.sh/tangled.sh/core/appview/pages" 21 21 22 22 comatproto "github.com/bluesky-social/indigo/api/atproto" ··· 27 27 28 28 type Settings struct { 29 29 Db *db.DB 30 - Auth *auth.Auth 30 + OAuth *oauth.OAuth 31 31 Pages *pages.Pages 32 32 Config *appview.Config 33 33 } ··· 35 35 func (s *Settings) Router() http.Handler { 36 36 r := chi.NewRouter() 37 37 38 - r.Use(middleware.AuthMiddleware(s.Auth)) 38 + r.Use(middleware.AuthMiddleware(s.OAuth)) 39 39 40 40 r.Get("/", s.settings) 41 41 ··· 56 56 } 57 57 58 58 func (s *Settings) settings(w http.ResponseWriter, r *http.Request) { 59 - user := s.Auth.GetUser(r) 59 + user := s.OAuth.GetUser(r) 60 60 pubKeys, err := db.GetPublicKeys(s.Db, user.Did) 61 61 if err != nil { 62 62 log.Println(err) ··· 79 79 verifyURL := s.verifyUrl(did, emailAddr, code) 80 80 81 81 return email.Email{ 82 - APIKey: s.Config.ResendApiKey, 82 + APIKey: s.Config.Resend.ApiKey, 83 83 From: "noreply@notifs.tangled.sh", 84 84 To: emailAddr, 85 85 Subject: "Verify your Tangled email", ··· 111 111 log.Println("unimplemented") 112 112 return 113 113 case http.MethodPut: 114 - did := s.Auth.GetDid(r) 114 + did := s.OAuth.GetDid(r) 115 115 emAddr := r.FormValue("email") 116 116 emAddr = strings.TrimSpace(emAddr) 117 117 ··· 174 174 s.Pages.Notice(w, "settings-emails-success", "Click the link in the email we sent you to verify your email address.") 175 175 return 176 176 case http.MethodDelete: 177 - did := s.Auth.GetDid(r) 177 + did := s.OAuth.GetDid(r) 178 178 emailAddr := r.FormValue("email") 179 179 emailAddr = strings.TrimSpace(emailAddr) 180 180 ··· 207 207 208 208 func (s *Settings) verifyUrl(did string, email string, code string) string { 209 209 var appUrl string 210 - if s.Config.Dev { 211 - appUrl = "http://" + s.Config.ListenAddr 210 + if s.Config.Core.Dev { 211 + appUrl = "http://" + s.Config.Core.ListenAddr 212 212 } else { 213 213 appUrl = "https://tangled.sh" 214 214 } ··· 252 252 return 253 253 } 254 254 255 - did := s.Auth.GetDid(r) 255 + did := s.OAuth.GetDid(r) 256 256 emAddr := r.FormValue("email") 257 257 emAddr = strings.TrimSpace(emAddr) 258 258 ··· 323 323 } 324 324 325 325 func (s *Settings) emailsPrimary(w http.ResponseWriter, r *http.Request) { 326 - did := s.Auth.GetDid(r) 326 + did := s.OAuth.GetDid(r) 327 327 emailAddr := r.FormValue("email") 328 328 emailAddr = strings.TrimSpace(emailAddr) 329 329 ··· 348 348 log.Println("unimplemented") 349 349 return 350 350 case http.MethodPut: 351 - did := s.Auth.GetDid(r) 351 + did := s.OAuth.GetDid(r) 352 352 key := r.FormValue("key") 353 353 key = strings.TrimSpace(key) 354 354 name := r.FormValue("name") 355 - client, _ := s.Auth.AuthorizedClient(r) 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 + } 356 360 357 - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) 361 + _, _, _, _, err = ssh.ParseAuthorizedKey([]byte(key)) 358 362 if err != nil { 359 363 log.Printf("parsing public key: %s", err) 360 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.") ··· 378 382 } 379 383 380 384 // store in pds too 381 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 385 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 382 386 Collection: tangled.PublicKeyNSID, 383 387 Repo: did, 384 388 Rkey: rkey, ··· 409 413 return 410 414 411 415 case http.MethodDelete: 412 - did := s.Auth.GetDid(r) 416 + did := s.OAuth.GetDid(r) 413 417 q := r.URL.Query() 414 418 415 419 name := q.Get("name") ··· 420 424 log.Println(rkey) 421 425 log.Println(key) 422 426 423 - client, _ := s.Auth.AuthorizedClient(r) 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 + } 424 433 425 - if err := db.RemovePublicKey(s.Db, did, name, key); err != nil { 434 + if err := db.DeletePublicKey(s.Db, did, name, key); err != nil { 426 435 log.Printf("removing public key: %s", err) 427 436 s.Pages.Notice(w, "settings-keys", "Failed to remove public key.") 428 437 return ··· 430 439 431 440 if rkey != "" { 432 441 // remove from pds too 433 - _, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 442 + _, err := client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 434 443 Collection: tangled.PublicKeyNSID, 435 444 Repo: did, 436 445 Rkey: rkey,
+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 + }
+10 -6
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 11 "tangled.sh/tangled.sh/core/appview" 12 12 "tangled.sh/tangled.sh/core/appview/db" 13 13 "tangled.sh/tangled.sh/core/appview/pages" 14 14 ) 15 15 16 16 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { 17 - currentUser := s.auth.GetUser(r) 17 + currentUser := s.oauth.GetUser(r) 18 18 19 19 subject := r.URL.Query().Get("subject") 20 20 if subject == "" { ··· 32 32 return 33 33 } 34 34 35 - 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 + } 36 40 37 41 switch r.Method { 38 42 case http.MethodPost: 39 43 createdAt := time.Now().Format(time.RFC3339) 40 44 rkey := appview.TID() 41 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 45 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 42 46 Collection: tangled.GraphFollowNSID, 43 47 Repo: currentUser.Did, 44 48 Rkey: rkey, ··· 75 79 return 76 80 } 77 81 78 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 82 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 79 83 Collection: tangled.GraphFollowNSID, 80 84 Repo: currentUser.Did, 81 85 Rkey: follow.Rkey, ··· 86 90 return 87 91 } 88 92 89 - err = db.DeleteFollow(s.db, currentUser.Did, subjectIdent.DID.String()) 93 + err = db.DeleteFollowByRkey(s.db, currentUser.Did, follow.Rkey) 90 94 if err != nil { 91 95 log.Println("failed to delete follow from DB") 92 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 - }
+38 -5
appview/state/middleware.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "fmt" 5 6 "log" 6 7 "net/http" 7 8 "strconv" ··· 20 21 return func(next http.Handler) http.Handler { 21 22 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 23 // requires auth also 23 - actor := s.auth.GetUser(r) 24 + actor := s.oauth.GetUser(r) 24 25 if actor == nil { 25 26 // we need a logged in user 26 27 log.Printf("not logged in, redirecting") ··· 54 55 return func(next http.Handler) http.Handler { 55 56 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 56 57 // requires auth also 57 - actor := s.auth.GetUser(r) 58 + actor := s.oauth.GetUser(r) 58 59 if actor == nil { 59 60 // we need a logged in user 60 61 log.Printf("not logged in, redirecting") 61 62 http.Error(w, "Forbiden", http.StatusUnauthorized) 62 63 return 63 64 } 64 - f, err := fullyResolvedRepo(r) 65 + f, err := s.fullyResolvedRepo(r) 65 66 if err != nil { 66 67 http.Error(w, "malformed url", http.StatusBadRequest) 67 68 return ··· 131 132 if err != nil { 132 133 // invalid did or handle 133 134 log.Println("failed to resolve repo") 134 - w.WriteHeader(http.StatusNotFound) 135 + s.pages.Error404(w) 135 136 return 136 137 } 137 138 ··· 148 149 func ResolvePull(s *State) middleware.Middleware { 149 150 return func(next http.Handler) http.Handler { 150 151 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 - f, err := fullyResolvedRepo(r) 152 + f, err := s.fullyResolvedRepo(r) 152 153 if err != nil { 153 154 log.Println("failed to fully resolve repo", err) 154 155 http.Error(w, "invalid repo url", http.StatusNotFound) ··· 175 176 }) 176 177 } 177 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 }
+103 -72
appview/state/pull.go
··· 13 13 14 14 "tangled.sh/tangled.sh/core/api/tangled" 15 15 "tangled.sh/tangled.sh/core/appview" 16 - "tangled.sh/tangled.sh/core/appview/auth" 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 ··· 143 144 } 144 145 } 145 146 146 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 147 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 147 148 if err != nil { 148 149 log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 149 150 return types.MergeCheckResponse{ ··· 215 216 repoName = f.RepoName 216 217 } 217 218 218 - us, err := NewUnsignedClient(knot, s.config.Dev) 219 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 219 220 if err != nil { 220 221 log.Printf("failed to setup client for %s; ignoring: %v", knot, err) 221 222 return pages.Unknown ··· 250 251 } 251 252 252 253 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 253 - user := s.auth.GetUser(r) 254 - f, err := fullyResolvedRepo(r) 254 + user := s.oauth.GetUser(r) 255 + f, err := s.fullyResolvedRepo(r) 255 256 if err != nil { 256 257 log.Println("failed to get repo and knot", err) 257 258 return ··· 298 299 } 299 300 300 301 func (s *State) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { 301 - user := s.auth.GetUser(r) 302 + user := s.oauth.GetUser(r) 302 303 303 - f, err := fullyResolvedRepo(r) 304 + f, err := s.fullyResolvedRepo(r) 304 305 if err != nil { 305 306 log.Println("failed to get repo and knot", err) 306 307 return ··· 355 356 interdiff := patchutil.Interdiff(previousPatch, currentPatch) 356 357 357 358 s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ 358 - LoggedInUser: s.auth.GetUser(r), 359 + LoggedInUser: s.oauth.GetUser(r), 359 360 RepoInfo: f.RepoInfo(s, user), 360 361 Pull: pull, 361 362 Round: roundIdInt, ··· 397 398 } 398 399 399 400 func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) { 400 - user := s.auth.GetUser(r) 401 + user := s.oauth.GetUser(r) 401 402 params := r.URL.Query() 402 403 403 404 state := db.PullOpen ··· 408 409 state = db.PullMerged 409 410 } 410 411 411 - f, err := fullyResolvedRepo(r) 412 + f, err := s.fullyResolvedRepo(r) 412 413 if err != nil { 413 414 log.Println("failed to get repo and knot", err) 414 415 return ··· 451 452 } 452 453 453 454 s.pages.RepoPulls(w, pages.RepoPullsParams{ 454 - LoggedInUser: s.auth.GetUser(r), 455 + LoggedInUser: s.oauth.GetUser(r), 455 456 RepoInfo: f.RepoInfo(s, user), 456 457 Pulls: pulls, 457 458 DidHandleMap: didHandleMap, ··· 461 462 } 462 463 463 464 func (s *State) PullComment(w http.ResponseWriter, r *http.Request) { 464 - user := s.auth.GetUser(r) 465 - f, err := fullyResolvedRepo(r) 465 + user := s.oauth.GetUser(r) 466 + f, err := s.fullyResolvedRepo(r) 466 467 if err != nil { 467 468 log.Println("failed to get repo and knot", err) 468 469 return ··· 519 520 } 520 521 521 522 atUri := f.RepoAt.String() 522 - client, _ := s.auth.AuthorizedClient(r) 523 - 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{ 524 530 Collection: tangled.RepoPullCommentNSID, 525 531 Repo: user.Did, 526 532 Rkey: appview.TID(), ··· 568 574 } 569 575 570 576 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) { 571 - user := s.auth.GetUser(r) 572 - f, err := fullyResolvedRepo(r) 577 + user := s.oauth.GetUser(r) 578 + f, err := s.fullyResolvedRepo(r) 573 579 if err != nil { 574 580 log.Println("failed to get repo and knot", err) 575 581 return ··· 577 583 578 584 switch r.Method { 579 585 case http.MethodGet: 580 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 586 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 581 587 if err != nil { 582 588 log.Printf("failed to create unsigned client for %s", f.Knot) 583 589 s.pages.Error503(w) ··· 646 652 return 647 653 } 648 654 649 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 655 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 650 656 if err != nil { 651 657 log.Printf("failed to create unsigned client to %s: %v", f.Knot, err) 652 658 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.") ··· 689 695 } 690 696 } 691 697 692 - 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) { 693 699 pullSource := &db.PullSource{ 694 700 Branch: sourceBranch, 695 701 } ··· 698 704 } 699 705 700 706 // Generate a patch using /compare 701 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 707 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 702 708 if err != nil { 703 709 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 704 710 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 723 729 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, sourceRev, pullSource, recordPullSource) 724 730 } 725 731 726 - 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) { 727 733 if !patchutil.IsPatchValid(patch) { 728 734 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 729 735 return ··· 732 738 s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", nil, nil) 733 739 } 734 740 735 - 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) { 736 742 fork, err := db.GetForkByDid(s.db, user.Did, forkRepo) 737 743 if errors.Is(err, sql.ErrNoRows) { 738 744 s.pages.Notice(w, "pull", "No such fork.") ··· 750 756 return 751 757 } 752 758 753 - sc, err := NewSignedClient(fork.Knot, secret, s.config.Dev) 759 + sc, err := knotclient.NewSignedClient(fork.Knot, secret, s.config.Core.Dev) 754 760 if err != nil { 755 761 log.Println("failed to create signed client:", err) 756 762 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 757 763 return 758 764 } 759 765 760 - us, err := NewUnsignedClient(fork.Knot, s.config.Dev) 766 + us, err := knotclient.NewUnsignedClient(fork.Knot, s.config.Core.Dev) 761 767 if err != nil { 762 768 log.Println("failed to create unsigned client:", err) 763 769 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") ··· 816 822 w http.ResponseWriter, 817 823 r *http.Request, 818 824 f *FullyResolvedRepo, 819 - user *auth.User, 825 + user *oauth.User, 820 826 title, body, targetBranch string, 821 827 patch string, 822 828 sourceRev string, ··· 870 876 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 871 877 return 872 878 } 873 - client, _ := s.auth.AuthorizedClient(r) 874 - 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) 875 886 if err != nil { 876 887 log.Println("failed to get pull id", err) 877 888 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 878 889 return 879 890 } 880 891 881 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 892 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 882 893 Collection: tangled.RepoPullNSID, 883 894 Repo: user.Did, 884 895 Rkey: rkey, ··· 893 904 }, 894 905 }, 895 906 }) 907 + if err != nil { 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 + } 896 912 897 - if err != nil { 913 + if err = tx.Commit(); err != nil { 898 914 log.Println("failed to create pull request", err) 899 915 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 900 916 return ··· 904 920 } 905 921 906 922 func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) { 907 - _, err := fullyResolvedRepo(r) 923 + _, err := s.fullyResolvedRepo(r) 908 924 if err != nil { 909 925 log.Println("failed to get repo and knot", err) 910 926 return ··· 929 945 } 930 946 931 947 func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) { 932 - user := s.auth.GetUser(r) 933 - f, err := fullyResolvedRepo(r) 948 + user := s.oauth.GetUser(r) 949 + f, err := s.fullyResolvedRepo(r) 934 950 if err != nil { 935 951 log.Println("failed to get repo and knot", err) 936 952 return ··· 942 958 } 943 959 944 960 func (s *State) CompareBranchesFragment(w http.ResponseWriter, r *http.Request) { 945 - user := s.auth.GetUser(r) 946 - f, err := fullyResolvedRepo(r) 961 + user := s.oauth.GetUser(r) 962 + f, err := s.fullyResolvedRepo(r) 947 963 if err != nil { 948 964 log.Println("failed to get repo and knot", err) 949 965 return 950 966 } 951 967 952 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 968 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 953 969 if err != nil { 954 970 log.Printf("failed to create unsigned client for %s", f.Knot) 955 971 s.pages.Error503(w) ··· 982 998 } 983 999 984 1000 func (s *State) CompareForksFragment(w http.ResponseWriter, r *http.Request) { 985 - user := s.auth.GetUser(r) 986 - f, err := fullyResolvedRepo(r) 1001 + user := s.oauth.GetUser(r) 1002 + f, err := s.fullyResolvedRepo(r) 987 1003 if err != nil { 988 1004 log.Println("failed to get repo and knot", err) 989 1005 return ··· 1002 1018 } 1003 1019 1004 1020 func (s *State) CompareForksBranchesFragment(w http.ResponseWriter, r *http.Request) { 1005 - user := s.auth.GetUser(r) 1021 + user := s.oauth.GetUser(r) 1006 1022 1007 - f, err := fullyResolvedRepo(r) 1023 + f, err := s.fullyResolvedRepo(r) 1008 1024 if err != nil { 1009 1025 log.Println("failed to get repo and knot", err) 1010 1026 return ··· 1019 1035 return 1020 1036 } 1021 1037 1022 - sourceBranchesClient, err := NewUnsignedClient(repo.Knot, s.config.Dev) 1038 + sourceBranchesClient, err := knotclient.NewUnsignedClient(repo.Knot, s.config.Core.Dev) 1023 1039 if err != nil { 1024 1040 log.Printf("failed to create unsigned client for %s", repo.Knot) 1025 1041 s.pages.Error503(w) ··· 1046 1062 return 1047 1063 } 1048 1064 1049 - targetBranchesClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1065 + targetBranchesClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1050 1066 if err != nil { 1051 1067 log.Printf("failed to create unsigned client for target knot %s", f.Knot) 1052 1068 s.pages.Error503(w) ··· 1081 1097 } 1082 1098 1083 1099 func (s *State) ResubmitPull(w http.ResponseWriter, r *http.Request) { 1084 - user := s.auth.GetUser(r) 1085 - f, err := fullyResolvedRepo(r) 1100 + user := s.oauth.GetUser(r) 1101 + f, err := s.fullyResolvedRepo(r) 1086 1102 if err != nil { 1087 1103 log.Println("failed to get repo and knot", err) 1088 1104 return ··· 1117 1133 } 1118 1134 1119 1135 func (s *State) resubmitPatch(w http.ResponseWriter, r *http.Request) { 1120 - user := s.auth.GetUser(r) 1136 + user := s.oauth.GetUser(r) 1121 1137 1122 1138 pull, ok := r.Context().Value("pull").(*db.Pull) 1123 1139 if !ok { ··· 1126 1142 return 1127 1143 } 1128 1144 1129 - f, err := fullyResolvedRepo(r) 1145 + f, err := s.fullyResolvedRepo(r) 1130 1146 if err != nil { 1131 1147 log.Println("failed to get repo and knot", err) 1132 1148 return ··· 1159 1175 s.pages.Notice(w, "resubmit-error", "Failed to resubmit pull request. Try again later.") 1160 1176 return 1161 1177 } 1162 - 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 + } 1163 1184 1164 - 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) 1165 1186 if err != nil { 1166 1187 // failed to get record 1167 1188 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") 1168 1189 return 1169 1190 } 1170 1191 1171 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1192 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1172 1193 Collection: tangled.RepoPullNSID, 1173 1194 Repo: user.Did, 1174 1195 Rkey: pull.Rkey, ··· 1200 1221 } 1201 1222 1202 1223 func (s *State) resubmitBranch(w http.ResponseWriter, r *http.Request) { 1203 - user := s.auth.GetUser(r) 1224 + user := s.oauth.GetUser(r) 1204 1225 1205 1226 pull, ok := r.Context().Value("pull").(*db.Pull) 1206 1227 if !ok { ··· 1209 1230 return 1210 1231 } 1211 1232 1212 - f, err := fullyResolvedRepo(r) 1233 + f, err := s.fullyResolvedRepo(r) 1213 1234 if err != nil { 1214 1235 log.Println("failed to get repo and knot", err) 1215 1236 return ··· 1227 1248 return 1228 1249 } 1229 1250 1230 - ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 1251 + ksClient, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1231 1252 if err != nil { 1232 1253 log.Printf("failed to create client for %s: %s", f.Knot, err) 1233 1254 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1268 1289 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1269 1290 return 1270 1291 } 1271 - 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 + } 1272 1298 1273 - 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) 1274 1300 if err != nil { 1275 1301 // failed to get record 1276 1302 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1280 1306 recordPullSource := &tangled.RepoPull_Source{ 1281 1307 Branch: pull.PullSource.Branch, 1282 1308 } 1283 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1309 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1284 1310 Collection: tangled.RepoPullNSID, 1285 1311 Repo: user.Did, 1286 1312 Rkey: pull.Rkey, ··· 1313 1339 } 1314 1340 1315 1341 func (s *State) resubmitFork(w http.ResponseWriter, r *http.Request) { 1316 - user := s.auth.GetUser(r) 1342 + user := s.oauth.GetUser(r) 1317 1343 1318 1344 pull, ok := r.Context().Value("pull").(*db.Pull) 1319 1345 if !ok { ··· 1322 1348 return 1323 1349 } 1324 1350 1325 - f, err := fullyResolvedRepo(r) 1351 + f, err := s.fullyResolvedRepo(r) 1326 1352 if err != nil { 1327 1353 log.Println("failed to get repo and knot", err) 1328 1354 return ··· 1342 1368 } 1343 1369 1344 1370 // extract patch by performing compare 1345 - ksClient, err := NewUnsignedClient(forkRepo.Knot, s.config.Dev) 1371 + ksClient, err := knotclient.NewUnsignedClient(forkRepo.Knot, s.config.Core.Dev) 1346 1372 if err != nil { 1347 1373 log.Printf("failed to create client for %s: %s", forkRepo.Knot, err) 1348 1374 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1357 1383 } 1358 1384 1359 1385 // update the hidden tracking branch to latest 1360 - signedClient, err := NewSignedClient(forkRepo.Knot, secret, s.config.Dev) 1386 + signedClient, err := knotclient.NewSignedClient(forkRepo.Knot, secret, s.config.Core.Dev) 1361 1387 if err != nil { 1362 1388 log.Printf("failed to create signed client for %s: %s", forkRepo.Knot, err) 1363 1389 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 1406 1432 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 1407 1433 return 1408 1434 } 1409 - 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 + } 1410 1441 1411 - 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) 1412 1443 if err != nil { 1413 1444 // failed to get record 1414 1445 s.pages.Notice(w, "resubmit-error", "Failed to update pull, no record found on PDS.") ··· 1420 1451 Branch: pull.PullSource.Branch, 1421 1452 Repo: &repoAt, 1422 1453 } 1423 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1454 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1424 1455 Collection: tangled.RepoPullNSID, 1425 1456 Repo: user.Did, 1426 1457 Rkey: pull.Rkey, ··· 1470 1501 } 1471 1502 1472 1503 func (s *State) MergePull(w http.ResponseWriter, r *http.Request) { 1473 - f, err := fullyResolvedRepo(r) 1504 + f, err := s.fullyResolvedRepo(r) 1474 1505 if err != nil { 1475 1506 log.Println("failed to resolve repo:", err) 1476 1507 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1503 1534 log.Printf("failed to get primary email: %s", err) 1504 1535 } 1505 1536 1506 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 1537 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1507 1538 if err != nil { 1508 1539 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 1509 1540 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 1533 1564 } 1534 1565 1535 1566 func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) { 1536 - user := s.auth.GetUser(r) 1567 + user := s.oauth.GetUser(r) 1537 1568 1538 - f, err := fullyResolvedRepo(r) 1569 + f, err := s.fullyResolvedRepo(r) 1539 1570 if err != nil { 1540 1571 log.Println("malformed middleware") 1541 1572 return ··· 1587 1618 } 1588 1619 1589 1620 func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) { 1590 - user := s.auth.GetUser(r) 1621 + user := s.oauth.GetUser(r) 1591 1622 1592 - f, err := fullyResolvedRepo(r) 1623 + f, err := s.fullyResolvedRepo(r) 1593 1624 if err != nil { 1594 1625 log.Println("failed to resolve repo", err) 1595 1626 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
+196 -122
appview/state/repo.go
··· 16 16 "strings" 17 17 "time" 18 18 19 - "github.com/bluesky-social/indigo/atproto/data" 20 - "github.com/bluesky-social/indigo/atproto/identity" 21 - "github.com/bluesky-social/indigo/atproto/syntax" 22 - securejoin "github.com/cyphar/filepath-securejoin" 23 - "github.com/go-chi/chi/v5" 24 - "github.com/go-git/go-git/v5/plumbing" 25 19 "tangled.sh/tangled.sh/core/api/tangled" 26 20 "tangled.sh/tangled.sh/core/appview" 27 - "tangled.sh/tangled.sh/core/appview/auth" 28 21 "tangled.sh/tangled.sh/core/appview/db" 22 + "tangled.sh/tangled.sh/core/appview/knotclient" 23 + "tangled.sh/tangled.sh/core/appview/oauth" 29 24 "tangled.sh/tangled.sh/core/appview/pages" 30 25 "tangled.sh/tangled.sh/core/appview/pages/markup" 26 + "tangled.sh/tangled.sh/core/appview/pages/repoinfo" 31 27 "tangled.sh/tangled.sh/core/appview/pagination" 32 28 "tangled.sh/tangled.sh/core/types" 33 29 30 + "github.com/bluesky-social/indigo/atproto/data" 31 + "github.com/bluesky-social/indigo/atproto/identity" 32 + "github.com/bluesky-social/indigo/atproto/syntax" 33 + securejoin "github.com/cyphar/filepath-securejoin" 34 + "github.com/go-chi/chi/v5" 35 + "github.com/go-git/go-git/v5/plumbing" 36 + 34 37 comatproto "github.com/bluesky-social/indigo/api/atproto" 35 38 lexutil "github.com/bluesky-social/indigo/lex/util" 36 39 ) 37 40 38 41 func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 39 42 ref := chi.URLParam(r, "ref") 40 - f, err := fullyResolvedRepo(r) 43 + f, err := s.fullyResolvedRepo(r) 41 44 if err != nil { 42 45 log.Println("failed to fully resolve repo", err) 43 46 return 44 47 } 45 48 46 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 49 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 47 50 if err != nil { 48 51 log.Printf("failed to create unsigned client for %s", f.Knot) 49 52 s.pages.Error503(w) ··· 74 77 tagMap := make(map[string][]string) 75 78 for _, tag := range result.Tags { 76 79 hash := tag.Hash 80 + if tag.Tag != nil { 81 + hash = tag.Tag.Target.String() 82 + } 77 83 tagMap[hash] = append(tagMap[hash], tag.Name) 78 84 } 79 85 ··· 89 95 if a.IsDefault { 90 96 return -1 91 97 } 98 + if b.IsDefault { 99 + return 1 100 + } 92 101 if a.Commit != nil { 93 102 if a.Commit.Author.When.Before(b.Commit.Author.When) { 94 103 return 1 ··· 111 120 112 121 emails := uniqueEmails(commitsTrunc) 113 122 114 - user := s.auth.GetUser(r) 123 + user := s.oauth.GetUser(r) 115 124 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 116 125 LoggedInUser: user, 117 126 RepoInfo: f.RepoInfo(s, user), ··· 126 135 } 127 136 128 137 func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 129 - f, err := fullyResolvedRepo(r) 138 + f, err := s.fullyResolvedRepo(r) 130 139 if err != nil { 131 140 log.Println("failed to fully resolve repo", err) 132 141 return ··· 142 151 143 152 ref := chi.URLParam(r, "ref") 144 153 145 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 154 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 146 155 if err != nil { 147 156 log.Println("failed to create unsigned client", err) 148 157 return ··· 167 176 return 168 177 } 169 178 170 - 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) 171 195 s.pages.RepoLog(w, pages.RepoLogParams{ 172 196 LoggedInUser: user, 197 + TagMap: tagMap, 173 198 RepoInfo: f.RepoInfo(s, user), 174 199 RepoLogResponse: repolog, 175 200 EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), ··· 178 203 } 179 204 180 205 func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 181 - f, err := fullyResolvedRepo(r) 206 + f, err := s.fullyResolvedRepo(r) 182 207 if err != nil { 183 208 log.Println("failed to get repo and knot", err) 184 209 w.WriteHeader(http.StatusBadRequest) 185 210 return 186 211 } 187 212 188 - user := s.auth.GetUser(r) 213 + user := s.oauth.GetUser(r) 189 214 s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 190 215 RepoInfo: f.RepoInfo(s, user), 191 216 }) ··· 193 218 } 194 219 195 220 func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 196 - f, err := fullyResolvedRepo(r) 221 + f, err := s.fullyResolvedRepo(r) 197 222 if err != nil { 198 223 log.Println("failed to get repo and knot", err) 199 224 w.WriteHeader(http.StatusBadRequest) ··· 208 233 return 209 234 } 210 235 211 - user := s.auth.GetUser(r) 236 + user := s.oauth.GetUser(r) 212 237 213 238 switch r.Method { 214 239 case http.MethodGet: ··· 217 242 }) 218 243 return 219 244 case http.MethodPut: 220 - user := s.auth.GetUser(r) 245 + user := s.oauth.GetUser(r) 221 246 newDescription := r.FormValue("description") 222 - 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 + } 223 253 224 254 // optimistic update 225 255 err = db.UpdateDescription(s.db, string(repoAt), newDescription) ··· 232 262 // this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field 233 263 // 234 264 // SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests 235 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey) 265 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 236 266 if err != nil { 237 267 // failed to get record 238 268 s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 239 269 return 240 270 } 241 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 271 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 242 272 Collection: tangled.RepoNSID, 243 273 Repo: user.Did, 244 274 Rkey: rkey, ··· 272 302 } 273 303 274 304 func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 275 - f, err := fullyResolvedRepo(r) 305 + f, err := s.fullyResolvedRepo(r) 276 306 if err != nil { 277 307 log.Println("failed to fully resolve repo", err) 278 308 return 279 309 } 280 310 ref := chi.URLParam(r, "ref") 281 311 protocol := "http" 282 - if !s.config.Dev { 312 + if !s.config.Core.Dev { 283 313 protocol = "https" 284 314 } 285 315 ··· 307 337 return 308 338 } 309 339 310 - user := s.auth.GetUser(r) 340 + user := s.oauth.GetUser(r) 311 341 s.pages.RepoCommit(w, pages.RepoCommitParams{ 312 342 LoggedInUser: user, 313 343 RepoInfo: f.RepoInfo(s, user), ··· 318 348 } 319 349 320 350 func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 321 - f, err := fullyResolvedRepo(r) 351 + f, err := s.fullyResolvedRepo(r) 322 352 if err != nil { 323 353 log.Println("failed to fully resolve repo", err) 324 354 return ··· 327 357 ref := chi.URLParam(r, "ref") 328 358 treePath := chi.URLParam(r, "*") 329 359 protocol := "http" 330 - if !s.config.Dev { 360 + if !s.config.Core.Dev { 331 361 protocol = "https" 332 362 } 333 363 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) ··· 349 379 return 350 380 } 351 381 352 - 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) 353 390 354 391 var breadcrumbs [][]string 355 392 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) ··· 374 411 } 375 412 376 413 func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 377 - f, err := fullyResolvedRepo(r) 414 + f, err := s.fullyResolvedRepo(r) 378 415 if err != nil { 379 416 log.Println("failed to get repo and knot", err) 380 417 return 381 418 } 382 419 383 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 420 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 384 421 if err != nil { 385 422 log.Println("failed to create unsigned client", err) 386 423 return 387 424 } 388 425 389 - resp, err := us.Tags(f.OwnerDid(), f.RepoName) 426 + result, err := us.Tags(f.OwnerDid(), f.RepoName) 390 427 if err != nil { 391 428 log.Println("failed to reach knotserver", err) 392 429 return 393 430 } 394 431 395 - body, err := io.ReadAll(resp.Body) 432 + artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt)) 396 433 if err != nil { 397 - log.Printf("Error reading response body: %v", err) 434 + log.Println("failed grab artifacts", err) 398 435 return 399 436 } 400 437 401 - var result types.RepoTagsResponse 402 - err = json.Unmarshal(body, &result) 403 - if err != nil { 404 - log.Println("failed to parse response:", err) 405 - 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) 442 + } 443 + 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 + } 406 458 } 407 459 408 - user := s.auth.GetUser(r) 460 + user := s.oauth.GetUser(r) 409 461 s.pages.RepoTags(w, pages.RepoTagsParams{ 410 - LoggedInUser: user, 411 - RepoInfo: f.RepoInfo(s, user), 412 - RepoTagsResponse: result, 462 + LoggedInUser: user, 463 + RepoInfo: f.RepoInfo(s, user), 464 + RepoTagsResponse: *result, 465 + ArtifactMap: artifactMap, 466 + DanglingArtifacts: danglingArtifacts, 413 467 }) 414 468 return 415 469 } 416 470 417 471 func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 418 - f, err := fullyResolvedRepo(r) 472 + f, err := s.fullyResolvedRepo(r) 419 473 if err != nil { 420 474 log.Println("failed to get repo and knot", err) 421 475 return 422 476 } 423 477 424 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 478 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 425 479 if err != nil { 426 480 log.Println("failed to create unsigned client", err) 427 481 return ··· 450 504 if a.IsDefault { 451 505 return -1 452 506 } 507 + if b.IsDefault { 508 + return 1 509 + } 453 510 if a.Commit != nil { 454 511 if a.Commit.Author.When.Before(b.Commit.Author.When) { 455 512 return 1 ··· 460 517 return strings.Compare(a.Name, b.Name) * -1 461 518 }) 462 519 463 - user := s.auth.GetUser(r) 520 + user := s.oauth.GetUser(r) 464 521 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 465 522 LoggedInUser: user, 466 523 RepoInfo: f.RepoInfo(s, user), ··· 470 527 } 471 528 472 529 func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 473 - f, err := fullyResolvedRepo(r) 530 + f, err := s.fullyResolvedRepo(r) 474 531 if err != nil { 475 532 log.Println("failed to get repo and knot", err) 476 533 return ··· 479 536 ref := chi.URLParam(r, "ref") 480 537 filePath := chi.URLParam(r, "*") 481 538 protocol := "http" 482 - if !s.config.Dev { 539 + if !s.config.Core.Dev { 483 540 protocol = "https" 484 541 } 485 542 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 517 574 showRendered = r.URL.Query().Get("code") != "true" 518 575 } 519 576 520 - user := s.auth.GetUser(r) 577 + user := s.oauth.GetUser(r) 521 578 s.pages.RepoBlob(w, pages.RepoBlobParams{ 522 579 LoggedInUser: user, 523 580 RepoInfo: f.RepoInfo(s, user), ··· 530 587 } 531 588 532 589 func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 533 - f, err := fullyResolvedRepo(r) 590 + f, err := s.fullyResolvedRepo(r) 534 591 if err != nil { 535 592 log.Println("failed to get repo and knot", err) 536 593 return ··· 540 597 filePath := chi.URLParam(r, "*") 541 598 542 599 protocol := "http" 543 - if !s.config.Dev { 600 + if !s.config.Core.Dev { 544 601 protocol = "https" 545 602 } 546 603 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 574 631 } 575 632 576 633 func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 577 - f, err := fullyResolvedRepo(r) 634 + f, err := s.fullyResolvedRepo(r) 578 635 if err != nil { 579 636 log.Println("failed to get repo and knot", err) 580 637 return ··· 601 658 return 602 659 } 603 660 604 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 661 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 605 662 if err != nil { 606 663 log.Println("failed to create client to ", f.Knot) 607 664 return ··· 663 720 } 664 721 665 722 func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 666 - user := s.auth.GetUser(r) 723 + user := s.oauth.GetUser(r) 667 724 668 - f, err := fullyResolvedRepo(r) 725 + f, err := s.fullyResolvedRepo(r) 669 726 if err != nil { 670 727 log.Println("failed to get repo and knot", err) 671 728 return 672 729 } 673 730 674 731 // remove record from pds 675 - 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 + } 676 737 repoRkey := f.RepoAt.RecordKey().String() 677 - _, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{ 738 + _, err = xrpcClient.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 678 739 Collection: tangled.RepoNSID, 679 740 Repo: user.Did, 680 741 Rkey: repoRkey, ··· 692 753 return 693 754 } 694 755 695 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 756 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 696 757 if err != nil { 697 758 log.Println("failed to create client to ", f.Knot) 698 759 return ··· 769 830 } 770 831 771 832 func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 772 - f, err := fullyResolvedRepo(r) 833 + f, err := s.fullyResolvedRepo(r) 773 834 if err != nil { 774 835 log.Println("failed to get repo and knot", err) 775 836 return ··· 787 848 return 788 849 } 789 850 790 - ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 851 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 791 852 if err != nil { 792 853 log.Println("failed to create client to ", f.Knot) 793 854 return ··· 808 869 } 809 870 810 871 func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 811 - f, err := fullyResolvedRepo(r) 872 + f, err := s.fullyResolvedRepo(r) 812 873 if err != nil { 813 874 log.Println("failed to get repo and knot", err) 814 875 return ··· 817 878 switch r.Method { 818 879 case http.MethodGet: 819 880 // for now, this is just pubkeys 820 - user := s.auth.GetUser(r) 881 + user := s.oauth.GetUser(r) 821 882 repoCollaborators, err := f.Collaborators(r.Context(), s) 822 883 if err != nil { 823 884 log.Println("failed to get collaborators", err) ··· 833 894 834 895 var branchNames []string 835 896 var defaultBranch string 836 - us, err := NewUnsignedClient(f.Knot, s.config.Dev) 897 + us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 837 898 if err != nil { 838 899 log.Println("failed to create unsigned client", err) 839 900 } else { ··· 859 920 } 860 921 } 861 922 862 - resp, err = us.DefaultBranch(f.OwnerDid(), f.RepoName) 923 + defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName) 863 924 if err != nil { 864 925 log.Println("failed to reach knotserver", err) 865 926 } else { 866 - defer resp.Body.Close() 867 - 868 - body, err := io.ReadAll(resp.Body) 869 - if err != nil { 870 - log.Printf("Error reading response body: %v", err) 871 - } else { 872 - var result types.RepoDefaultBranchResponse 873 - err = json.Unmarshal(body, &result) 874 - if err != nil { 875 - log.Println("failed to parse response:", err) 876 - } else { 877 - defaultBranch = result.Branch 878 - } 879 - } 927 + defaultBranch = defaultBranchResp.Branch 880 928 } 881 929 } 882 - 883 930 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 884 931 LoggedInUser: user, 885 932 RepoInfo: f.RepoInfo(s, user), ··· 898 945 RepoAt syntax.ATURI 899 946 Description string 900 947 CreatedAt string 948 + Ref string 901 949 } 902 950 903 951 func (f *FullyResolvedRepo) OwnerDid() string { ··· 970 1018 return collaborators, nil 971 1019 } 972 1020 973 - func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo { 1021 + func (f *FullyResolvedRepo) RepoInfo(s *State, u *oauth.User) repoinfo.RepoInfo { 974 1022 isStarred := false 975 1023 if u != nil { 976 1024 isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) ··· 1013 1061 1014 1062 knot := f.Knot 1015 1063 var disableFork bool 1016 - us, err := NewUnsignedClient(knot, s.config.Dev) 1064 + us, err := knotclient.NewUnsignedClient(knot, s.config.Core.Dev) 1017 1065 if err != nil { 1018 1066 log.Printf("failed to create unsigned client for %s: %v", knot, err) 1019 1067 } else { ··· 1040 1088 } 1041 1089 } 1042 1090 1043 - if knot == "knot1.tangled.sh" { 1044 - knot = "tangled.sh" 1045 - } 1046 - 1047 - repoInfo := pages.RepoInfo{ 1091 + repoInfo := repoinfo.RepoInfo{ 1048 1092 OwnerDid: f.OwnerDid(), 1049 1093 OwnerHandle: f.OwnerHandle(), 1050 1094 Name: f.RepoName, 1051 1095 RepoAt: f.RepoAt, 1052 1096 Description: f.Description, 1097 + Ref: f.Ref, 1053 1098 IsStarred: isStarred, 1054 1099 Knot: knot, 1055 1100 Roles: RolesInRepo(s, u, f), ··· 1070 1115 } 1071 1116 1072 1117 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1073 - user := s.auth.GetUser(r) 1074 - f, err := fullyResolvedRepo(r) 1118 + user := s.oauth.GetUser(r) 1119 + f, err := s.fullyResolvedRepo(r) 1075 1120 if err != nil { 1076 1121 log.Println("failed to get repo and knot", err) 1077 1122 return ··· 1124 1169 } 1125 1170 1126 1171 func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1127 - user := s.auth.GetUser(r) 1128 - f, err := fullyResolvedRepo(r) 1172 + user := s.oauth.GetUser(r) 1173 + f, err := s.fullyResolvedRepo(r) 1129 1174 if err != nil { 1130 1175 log.Println("failed to get repo and knot", err) 1131 1176 return ··· 1160 1205 1161 1206 closed := tangled.RepoIssueStateClosed 1162 1207 1163 - client, _ := s.auth.AuthorizedClient(r) 1164 - _, 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{ 1165 1214 Collection: tangled.RepoIssueStateNSID, 1166 1215 Repo: user.Did, 1167 1216 Rkey: appview.TID(), ··· 1179 1228 return 1180 1229 } 1181 1230 1182 - err := db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1231 + err = db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1183 1232 if err != nil { 1184 1233 log.Println("failed to close issue", err) 1185 1234 s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") ··· 1196 1245 } 1197 1246 1198 1247 func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1199 - user := s.auth.GetUser(r) 1200 - f, err := fullyResolvedRepo(r) 1248 + user := s.oauth.GetUser(r) 1249 + f, err := s.fullyResolvedRepo(r) 1201 1250 if err != nil { 1202 1251 log.Println("failed to get repo and knot", err) 1203 1252 return ··· 1244 1293 } 1245 1294 1246 1295 func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1247 - user := s.auth.GetUser(r) 1248 - f, err := fullyResolvedRepo(r) 1296 + user := s.oauth.GetUser(r) 1297 + f, err := s.fullyResolvedRepo(r) 1249 1298 if err != nil { 1250 1299 log.Println("failed to get repo and knot", err) 1251 1300 return ··· 1295 1344 } 1296 1345 1297 1346 atUri := f.RepoAt.String() 1298 - client, _ := s.auth.AuthorizedClient(r) 1299 - _, 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{ 1300 1354 Collection: tangled.RepoIssueCommentNSID, 1301 1355 Repo: user.Did, 1302 1356 Rkey: rkey, ··· 1323 1377 } 1324 1378 1325 1379 func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1326 - user := s.auth.GetUser(r) 1327 - f, err := fullyResolvedRepo(r) 1380 + user := s.oauth.GetUser(r) 1381 + f, err := s.fullyResolvedRepo(r) 1328 1382 if err != nil { 1329 1383 log.Println("failed to get repo and knot", err) 1330 1384 return ··· 1382 1436 } 1383 1437 1384 1438 func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1385 - user := s.auth.GetUser(r) 1386 - f, err := fullyResolvedRepo(r) 1439 + user := s.oauth.GetUser(r) 1440 + f, err := s.fullyResolvedRepo(r) 1387 1441 if err != nil { 1388 1442 log.Println("failed to get repo and knot", err) 1389 1443 return ··· 1434 1488 case http.MethodPost: 1435 1489 // extract form value 1436 1490 newBody := r.FormValue("body") 1437 - 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 + } 1438 1497 rkey := comment.Rkey 1439 1498 1440 1499 // optimistic update ··· 1449 1508 // rkey is optional, it was introduced later 1450 1509 if comment.Rkey != "" { 1451 1510 // update the record on pds 1452 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1511 + ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoIssueCommentNSID, user.Did, rkey) 1453 1512 if err != nil { 1454 1513 // failed to get record 1455 1514 log.Println(err, rkey) ··· 1464 1523 createdAt := record["createdAt"].(string) 1465 1524 commentIdInt64 := int64(commentIdInt) 1466 1525 1467 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1526 + _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1468 1527 Collection: tangled.RepoIssueCommentNSID, 1469 1528 Repo: user.Did, 1470 1529 Rkey: rkey, ··· 1507 1566 } 1508 1567 1509 1568 func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1510 - user := s.auth.GetUser(r) 1511 - f, err := fullyResolvedRepo(r) 1569 + user := s.oauth.GetUser(r) 1570 + f, err := s.fullyResolvedRepo(r) 1512 1571 if err != nil { 1513 1572 log.Println("failed to get repo and knot", err) 1514 1573 return ··· 1564 1623 1565 1624 // delete from pds 1566 1625 if comment.Rkey != "" { 1567 - client, _ := s.auth.AuthorizedClient(r) 1568 - _, 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{ 1569 1633 Collection: tangled.GraphFollowNSID, 1570 1634 Repo: user.Did, 1571 1635 Rkey: comment.Rkey, ··· 1612 1676 page = pagination.FirstPage() 1613 1677 } 1614 1678 1615 - user := s.auth.GetUser(r) 1616 - f, err := fullyResolvedRepo(r) 1679 + user := s.oauth.GetUser(r) 1680 + f, err := s.fullyResolvedRepo(r) 1617 1681 if err != nil { 1618 1682 log.Println("failed to get repo and knot", err) 1619 1683 return ··· 1641 1705 } 1642 1706 1643 1707 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1644 - LoggedInUser: s.auth.GetUser(r), 1708 + LoggedInUser: s.oauth.GetUser(r), 1645 1709 RepoInfo: f.RepoInfo(s, user), 1646 1710 Issues: issues, 1647 1711 DidHandleMap: didHandleMap, ··· 1652 1716 } 1653 1717 1654 1718 func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1655 - user := s.auth.GetUser(r) 1719 + user := s.oauth.GetUser(r) 1656 1720 1657 - f, err := fullyResolvedRepo(r) 1721 + f, err := s.fullyResolvedRepo(r) 1658 1722 if err != nil { 1659 1723 log.Println("failed to get repo and knot", err) 1660 1724 return ··· 1700 1764 return 1701 1765 } 1702 1766 1703 - 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 + } 1704 1773 atUri := f.RepoAt.String() 1705 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1774 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1706 1775 Collection: tangled.RepoIssueNSID, 1707 1776 Repo: user.Did, 1708 1777 Rkey: appview.TID(), ··· 1735 1804 } 1736 1805 1737 1806 func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1738 - user := s.auth.GetUser(r) 1739 - f, err := fullyResolvedRepo(r) 1807 + user := s.oauth.GetUser(r) 1808 + f, err := s.fullyResolvedRepo(r) 1740 1809 if err != nil { 1741 1810 log.Printf("failed to resolve source repo: %v", err) 1742 1811 return ··· 1744 1813 1745 1814 switch r.Method { 1746 1815 case http.MethodGet: 1747 - user := s.auth.GetUser(r) 1816 + user := s.oauth.GetUser(r) 1748 1817 knots, err := s.enforcer.GetDomainsForUser(user.Did) 1749 1818 if err != nil { 1750 1819 s.pages.Notice(w, "repo", "Invalid user account.") ··· 1794 1863 return 1795 1864 } 1796 1865 1797 - client, err := NewSignedClient(knot, secret, s.config.Dev) 1866 + client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev) 1798 1867 if err != nil { 1799 1868 s.pages.Notice(w, "repo", "Failed to reach knot server.") 1800 1869 return 1801 1870 } 1802 1871 1803 1872 var uri string 1804 - if s.config.Dev { 1873 + if s.config.Core.Dev { 1805 1874 uri = "http" 1806 1875 } else { 1807 1876 uri = "https" ··· 1848 1917 // continue 1849 1918 } 1850 1919 1851 - 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 + } 1852 1926 1853 1927 createdAt := time.Now().Format(time.RFC3339) 1854 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 1928 + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 1855 1929 Collection: tangled.RepoNSID, 1856 1930 Repo: user.Did, 1857 1931 Rkey: rkey,
+24 -6
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) ··· 53 70 RepoAt: parsedRepoAt, 54 71 Description: description, 55 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
+60 -21
appview/state/router.go
··· 5 5 "strings" 6 6 7 7 "github.com/go-chi/chi/v5" 8 + "github.com/gorilla/sessions" 8 9 "tangled.sh/tangled.sh/core/appview/middleware" 10 + oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" 9 11 "tangled.sh/tangled.sh/core/appview/settings" 10 12 "tangled.sh/tangled.sh/core/appview/state/userutil" 11 13 ) ··· 53 55 r.Use(StripLeadingAt) 54 56 55 57 r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 56 - r.Get("/", s.ProfilePage) 58 + r.Get("/", s.Profile) 59 + 57 60 r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) { 61 + r.Use(GoImport(s)) 62 + 58 63 r.Get("/", s.RepoIndex) 59 64 r.Get("/commits/{ref}", s.RepoLog) 60 65 r.Route("/tree/{ref}", func(r chi.Router) { ··· 63 68 }) 64 69 r.Get("/commit/{ref}", s.RepoCommit) 65 70 r.Get("/branches", s.RepoBranches) 66 - 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 + }) 67 89 r.Get("/blob/{ref}/*", s.RepoBlob) 68 - r.Get("/blob/{ref}/raw/*", s.RepoBlobRaw) 90 + r.Get("/raw/{ref}/*", s.RepoBlobRaw) 69 91 70 92 r.Route("/issues", func(r chi.Router) { 71 93 r.With(middleware.Paginate).Get("/", s.RepoIssues) 72 94 r.Get("/{issue}", s.RepoSingleIssue) 73 95 74 96 r.Group(func(r chi.Router) { 75 - r.Use(middleware.AuthMiddleware(s.auth)) 97 + r.Use(middleware.AuthMiddleware(s.oauth)) 76 98 r.Get("/new", s.NewIssue) 77 99 r.Post("/new", s.NewIssue) 78 100 r.Post("/{issue}/comment", s.NewIssueComment) ··· 88 110 }) 89 111 90 112 r.Route("/fork", func(r chi.Router) { 91 - r.Use(middleware.AuthMiddleware(s.auth)) 113 + r.Use(middleware.AuthMiddleware(s.oauth)) 92 114 r.Get("/", s.ForkRepo) 93 115 r.Post("/", s.ForkRepo) 94 116 }) 95 117 96 118 r.Route("/pulls", func(r chi.Router) { 97 119 r.Get("/", s.RepoPulls) 98 - r.With(middleware.AuthMiddleware(s.auth)).Route("/new", func(r chi.Router) { 120 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/new", func(r chi.Router) { 99 121 r.Get("/", s.NewPull) 100 122 r.Get("/patch-upload", s.PatchUploadFragment) 101 123 r.Post("/validate-patch", s.ValidatePatch) ··· 113 135 r.Get("/", s.RepoPullPatch) 114 136 r.Get("/interdiff", s.RepoPullInterdiff) 115 137 r.Get("/actions", s.PullActions) 116 - r.With(middleware.AuthMiddleware(s.auth)).Route("/comment", func(r chi.Router) { 138 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/comment", func(r chi.Router) { 117 139 r.Get("/", s.PullComment) 118 140 r.Post("/", s.PullComment) 119 141 }) ··· 124 146 }) 125 147 126 148 r.Group(func(r chi.Router) { 127 - r.Use(middleware.AuthMiddleware(s.auth)) 149 + r.Use(middleware.AuthMiddleware(s.oauth)) 128 150 r.Route("/resubmit", func(r chi.Router) { 129 151 r.Get("/", s.ResubmitPull) 130 152 r.Post("/", s.ResubmitPull) ··· 147 169 148 170 // settings routes, needs auth 149 171 r.Group(func(r chi.Router) { 150 - r.Use(middleware.AuthMiddleware(s.auth)) 172 + r.Use(middleware.AuthMiddleware(s.oauth)) 151 173 // repo description can only be edited by owner 152 174 r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 153 175 r.Put("/", s.RepoDescription) ··· 178 200 179 201 r.Get("/", s.Timeline) 180 202 181 - r.With(middleware.AuthMiddleware(s.auth)).Post("/logout", s.Logout) 182 - 183 - r.Route("/login", func(r chi.Router) { 184 - r.Get("/", s.Login) 185 - r.Post("/", s.Login) 186 - }) 203 + r.With(middleware.AuthMiddleware(s.oauth)).Post("/logout", s.Logout) 187 204 188 205 r.Route("/knots", func(r chi.Router) { 189 - r.Use(middleware.AuthMiddleware(s.auth)) 206 + r.Use(middleware.AuthMiddleware(s.oauth)) 190 207 r.Get("/", s.Knots) 191 208 r.Post("/key", s.RegistrationKey) 192 209 ··· 204 221 205 222 r.Route("/repo", func(r chi.Router) { 206 223 r.Route("/new", func(r chi.Router) { 207 - r.Use(middleware.AuthMiddleware(s.auth)) 224 + r.Use(middleware.AuthMiddleware(s.oauth)) 208 225 r.Get("/", s.NewRepo) 209 226 r.Post("/", s.NewRepo) 210 227 }) 211 228 // r.Post("/import", s.ImportRepo) 212 229 }) 213 230 214 - r.With(middleware.AuthMiddleware(s.auth)).Route("/follow", func(r chi.Router) { 231 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/follow", func(r chi.Router) { 215 232 r.Post("/", s.Follow) 216 233 r.Delete("/", s.Follow) 217 234 }) 218 235 219 - r.With(middleware.AuthMiddleware(s.auth)).Route("/star", func(r chi.Router) { 236 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/star", func(r chi.Router) { 220 237 r.Post("/", s.Star) 221 238 r.Delete("/", s.Star) 222 239 }) 223 240 224 - r.Mount("/settings", s.SettingsRouter()) 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) 247 + }) 225 248 249 + r.Mount("/settings", s.SettingsRouter()) 250 + r.Mount("/", s.OAuthRouter()) 226 251 r.Get("/keys/{user}", s.Keys) 227 252 228 253 r.NotFound(func(w http.ResponseWriter, r *http.Request) { ··· 231 256 return r 232 257 } 233 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 + 234 273 func (s *State) SettingsRouter() http.Handler { 235 274 settings := &settings.Settings{ 236 275 Db: s.db, 237 - Auth: s.auth, 276 + OAuth: s.oauth, 238 277 Pages: s.pages, 239 278 Config: s.config, 240 279 }
-462
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 - "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) (*http.Response, 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 - return us.client.Do(req) 366 - } 367 - 368 - func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) { 369 - const ( 370 - Method = "GET" 371 - ) 372 - 373 - endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, url.PathEscape(branch)) 374 - 375 - req, err := us.newRequest(Method, endpoint, nil, nil) 376 - if err != nil { 377 - return nil, err 378 - } 379 - 380 - return us.client.Do(req) 381 - } 382 - 383 - func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*http.Response, error) { 384 - const ( 385 - Method = "GET" 386 - ) 387 - 388 - endpoint := fmt.Sprintf("/%s/%s/branches/default", ownerDid, repoName) 389 - 390 - req, err := us.newRequest(Method, endpoint, nil, nil) 391 - if err != nil { 392 - return nil, err 393 - } 394 - 395 - return us.client.Do(req) 396 - } 397 - 398 - func (us *UnsignedClient) Capabilities() (*types.Capabilities, error) { 399 - const ( 400 - Method = "GET" 401 - Endpoint = "/capabilities" 402 - ) 403 - 404 - req, err := us.newRequest(Method, Endpoint, nil, nil) 405 - if err != nil { 406 - return nil, err 407 - } 408 - 409 - resp, err := us.client.Do(req) 410 - if err != nil { 411 - return nil, err 412 - } 413 - defer resp.Body.Close() 414 - 415 - var capabilities types.Capabilities 416 - if err := json.NewDecoder(resp.Body).Decode(&capabilities); err != nil { 417 - return nil, err 418 - } 419 - 420 - return &capabilities, nil 421 - } 422 - 423 - func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*types.RepoFormatPatchResponse, error) { 424 - const ( 425 - Method = "GET" 426 - ) 427 - 428 - endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, url.PathEscape(rev1), url.PathEscape(rev2)) 429 - 430 - req, err := us.newRequest(Method, endpoint, nil, nil) 431 - if err != nil { 432 - return nil, fmt.Errorf("Failed to create request.") 433 - } 434 - 435 - compareResp, err := us.client.Do(req) 436 - if err != nil { 437 - return nil, fmt.Errorf("Failed to create request.") 438 - } 439 - defer compareResp.Body.Close() 440 - 441 - switch compareResp.StatusCode { 442 - case 404: 443 - case 400: 444 - return nil, fmt.Errorf("Branch comparisons not supported on this knot.") 445 - } 446 - 447 - respBody, err := io.ReadAll(compareResp.Body) 448 - if err != nil { 449 - log.Println("failed to compare across branches") 450 - return nil, fmt.Errorf("Failed to compare branches.") 451 - } 452 - defer compareResp.Body.Close() 453 - 454 - var formatPatchResponse types.RepoFormatPatchResponse 455 - err = json.Unmarshal(respBody, &formatPatchResponse) 456 - if err != nil { 457 - log.Println("failed to unmarshal format-patch response", err) 458 - return nil, fmt.Errorf("failed to compare branches.") 459 - } 460 - 461 - return &formatPatchResponse, nil 462 - }
+11 -6
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 12 "tangled.sh/tangled.sh/core/appview" 13 13 "tangled.sh/tangled.sh/core/appview/db" 14 14 "tangled.sh/tangled.sh/core/appview/pages" 15 15 ) 16 16 17 17 func (s *State) Star(w http.ResponseWriter, r *http.Request) { 18 - currentUser := s.auth.GetUser(r) 18 + currentUser := s.oauth.GetUser(r) 19 19 20 20 subject := r.URL.Query().Get("subject") 21 21 if subject == "" { ··· 29 29 return 30 30 } 31 31 32 - 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 + } 33 37 34 38 switch r.Method { 35 39 case http.MethodPost: 36 40 createdAt := time.Now().Format(time.RFC3339) 37 41 rkey := appview.TID() 38 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 42 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 39 43 Collection: tangled.FeedStarNSID, 40 44 Repo: currentUser.Did, 41 45 Rkey: rkey, ··· 80 84 return 81 85 } 82 86 83 - _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 87 + _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ 84 88 Collection: tangled.FeedStarNSID, 85 89 Repo: currentUser.Did, 86 90 Rkey: star.Rkey, ··· 91 95 return 92 96 } 93 97 94 - err = db.DeleteStar(s.db, currentUser.Did, subjectUri) 98 + err = db.DeleteStarByRkey(s.db, currentUser.Did, star.Rkey) 95 99 if err != nil { 96 100 log.Println("failed to delete star from DB") 97 101 // this is not an issue, the firehose event might have already done this ··· 100 104 starCount, err := db.GetStarCount(s.db, subjectUri) 101 105 if err != nil { 102 106 log.Println("failed to get star count for ", subjectUri) 107 + return 103 108 } 104 109 105 110 s.pages.RepoActionsFragment(w, pages.RepoActionsFragmentParams{
+143 -110
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 - if err != nil { 43 - return nil, err 44 - } 45 - 46 - auth, err := auth.Make(config.CookieSecret) 42 + d, err := db.Make(config.Core.DbPath) 47 43 if err != nil { 48 44 return nil, err 49 45 } 50 46 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(config.Dev) 54 + pgs := pages.NewPages(config) 59 55 60 56 resolver := appview.NewResolver() 61 57 58 + oauth := oauth.NewOAuth(d, config) 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, ··· 95 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) ··· 516 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) 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) 521 529 createdAt := time.Now().Format(time.RFC3339) 522 - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 530 + resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 523 531 Collection: tangled.KnotMemberNSID, 524 532 Repo: currentUser.Did, 525 533 Rkey: appview.TID(), ··· 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 ··· 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 ··· 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 697 createdAt := time.Now().Format(time.RFC3339) 661 - atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{ 698 + atresp, err := xrpcClient.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ 662 699 Collection: tangled.RepoNSID, 663 700 Repo: user.Did, 664 701 Rkey: rkey, ··· 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 - }
+1 -1
appview/tid.go
··· 4 4 "github.com/bluesky-social/indigo/atproto/syntax" 5 5 ) 6 6 7 - var c *syntax.TIDClock = syntax.NewTIDClock(0) 7 + var c syntax.TIDClock = syntax.NewTIDClock(0) 8 8 9 9 func TID() string { 10 10 return c.Next().String()
+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:
+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 + ```
+4 -4
flake.lock
··· 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 }
+373 -378
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; ··· 29 29 }; 30 30 }; 31 31 32 - outputs = 33 - { self 34 - , nixpkgs 35 - , indigo 36 - , htmx-src 37 - , lucide-src 38 - , gitignore 39 - , inter-fonts-src 40 - , ibm-plex-mono-src 41 - , 32 + outputs = { 33 + self, 34 + nixpkgs, 35 + indigo, 36 + htmx-src, 37 + lucide-src, 38 + gitignore, 39 + inter-fonts-src, 40 + ibm-plex-mono-src, 41 + }: let 42 + supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; 43 + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 44 + nixpkgsFor = forAllSystems (system: 45 + import nixpkgs { 46 + inherit system; 47 + overlays = [self.overlays.default]; 48 + }); 49 + inherit (gitignore.lib) gitignoreSource; 50 + in { 51 + overlays.default = final: prev: let 52 + goModHash = "sha256-zcfTNo7QsiihzLa4qHEX8uGGtbcmBn8TlSm0YHBRNw8="; 53 + buildCmdPackage = name: 54 + final.buildGoModule { 55 + pname = name; 56 + version = "0.1.0"; 57 + src = gitignoreSource ./.; 58 + subPackages = ["cmd/${name}"]; 59 + vendorHash = goModHash; 60 + env.CGO_ENABLED = 0; 61 + }; 62 + in { 63 + indigo-lexgen = final.buildGoModule { 64 + pname = "indigo-lexgen"; 65 + version = "0.1.0"; 66 + src = indigo; 67 + subPackages = ["cmd/lexgen"]; 68 + vendorHash = "sha256-pGc29fgJFq8LP7n/pY1cv6ExZl88PAeFqIbFEhB3xXs="; 69 + doCheck = false; 70 + }; 71 + 72 + appview = with final; 73 + final.pkgsStatic.buildGoModule { 74 + pname = "appview"; 75 + version = "0.1.0"; 76 + src = gitignoreSource ./.; 77 + postUnpack = '' 78 + pushd source 79 + mkdir -p appview/pages/static/{fonts,icons} 80 + cp -f ${htmx-src} appview/pages/static/htmx.min.js 81 + cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 82 + cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 83 + cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 84 + cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 85 + ${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o appview/pages/static/tw.css 86 + popd 87 + ''; 88 + doCheck = false; 89 + subPackages = ["cmd/appview"]; 90 + vendorHash = goModHash; 91 + env.CGO_ENABLED = 1; 92 + stdenv = pkgsStatic.stdenv; 93 + }; 94 + 95 + knotserver = with final; 96 + final.pkgsStatic.buildGoModule { 97 + pname = "knotserver"; 98 + version = "0.1.0"; 99 + src = gitignoreSource ./.; 100 + nativeBuildInputs = [final.makeWrapper]; 101 + subPackages = ["cmd/knotserver"]; 102 + vendorHash = goModHash; 103 + installPhase = '' 104 + runHook preInstall 105 + 106 + mkdir -p $out/bin 107 + cp $GOPATH/bin/knotserver $out/bin/knotserver 108 + 109 + wrapProgram $out/bin/knotserver \ 110 + --prefix PATH : ${pkgs.git}/bin 111 + 112 + runHook postInstall 113 + ''; 114 + env.CGO_ENABLED = 1; 115 + }; 116 + knotserver-unwrapped = final.pkgsStatic.buildGoModule { 117 + pname = "knotserver"; 118 + version = "0.1.0"; 119 + src = gitignoreSource ./.; 120 + subPackages = ["cmd/knotserver"]; 121 + vendorHash = goModHash; 122 + env.CGO_ENABLED = 1; 123 + }; 124 + repoguard = buildCmdPackage "repoguard"; 125 + keyfetch = buildCmdPackage "keyfetch"; 126 + genjwks = buildCmdPackage "genjwks"; 127 + }; 128 + packages = forAllSystems (system: { 129 + inherit 130 + (nixpkgsFor."${system}") 131 + indigo-lexgen 132 + appview 133 + knotserver 134 + knotserver-unwrapped 135 + repoguard 136 + keyfetch 137 + genjwks 138 + ; 139 + }); 140 + defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); 141 + formatter = forAllSystems (system: nixpkgsFor."${system}".alejandra); 142 + devShells = forAllSystems (system: let 143 + pkgs = nixpkgsFor.${system}; 144 + staticShell = pkgs.mkShell.override { 145 + stdenv = pkgs.pkgsStatic.stdenv; 146 + }; 147 + in { 148 + default = staticShell { 149 + nativeBuildInputs = [ 150 + pkgs.go 151 + pkgs.air 152 + pkgs.gopls 153 + pkgs.httpie 154 + pkgs.indigo-lexgen 155 + pkgs.litecli 156 + pkgs.websocat 157 + pkgs.tailwindcss 158 + pkgs.nixos-shell 159 + ]; 160 + shellHook = '' 161 + mkdir -p appview/pages/static/{fonts,icons} 162 + cp -f ${htmx-src} appview/pages/static/htmx.min.js 163 + cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 164 + cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 165 + cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 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)" 168 + ''; 169 + env.CGO_ENABLED = 1; 170 + }; 171 + }); 172 + apps = forAllSystems (system: let 173 + pkgs = nixpkgsFor."${system}"; 174 + air-watcher = name: 175 + pkgs.writeShellScriptBin "run" 176 + '' 177 + ${pkgs.air}/bin/air -c /dev/null \ 178 + -build.cmd "${pkgs.go}/bin/go build -o ./out/${name}.out ./cmd/${name}/main.go" \ 179 + -build.bin "./out/${name}.out" \ 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 187 + ''; 188 + in { 189 + watch-appview = { 190 + type = "app"; 191 + program = ''${air-watcher "appview"}/bin/run''; 192 + }; 193 + watch-knotserver = { 194 + type = "app"; 195 + program = ''${air-watcher "knotserver"}/bin/run''; 196 + }; 197 + watch-tailwind = { 198 + type = "app"; 199 + program = ''${tailwind-watcher}/bin/run''; 200 + }; 201 + }); 202 + 203 + nixosModules.appview = { 204 + config, 205 + pkgs, 206 + lib, 207 + ... 42 208 }: 43 - let 44 - supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ]; 45 - forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 46 - nixpkgsFor = forAllSystems (system: 47 - import nixpkgs { 48 - inherit system; 49 - overlays = [ self.overlays.default ]; 50 - }); 51 - inherit (gitignore.lib) gitignoreSource; 52 - in 53 - { 54 - overlays.default = final: prev: 55 - let 56 - goModHash = "sha256-EilWxfqrcKDaSR5zA3ZuDSCq7V+/IfWpKPu8HWhpndA="; 57 - buildCmdPackage = name: 58 - final.buildGoModule { 59 - pname = name; 60 - version = "0.1.0"; 61 - src = gitignoreSource ./.; 62 - subPackages = [ "cmd/${name}" ]; 63 - vendorHash = goModHash; 64 - CGO_ENABLED = 0; 209 + with lib; { 210 + options = { 211 + services.tangled-appview = { 212 + enable = mkOption { 213 + type = types.bool; 214 + default = false; 215 + description = "Enable tangled appview"; 216 + }; 217 + port = mkOption { 218 + type = types.int; 219 + default = 3000; 220 + description = "Port to run the appview on"; 65 221 }; 66 - in 67 - { 68 - indigo-lexgen = final.buildGoModule { 69 - pname = "indigo-lexgen"; 70 - version = "0.1.0"; 71 - src = indigo; 72 - subPackages = [ "cmd/lexgen" ]; 73 - vendorHash = "sha256-pGc29fgJFq8LP7n/pY1cv6ExZl88PAeFqIbFEhB3xXs="; 74 - doCheck = false; 222 + cookie_secret = mkOption { 223 + type = types.str; 224 + default = "00000000000000000000000000000000"; 225 + description = "Cookie secret"; 226 + }; 75 227 }; 228 + }; 76 229 77 - appview = with final; 78 - final.pkgsStatic.buildGoModule { 79 - pname = "appview"; 80 - version = "0.1.0"; 81 - src = gitignoreSource ./.; 82 - postUnpack = '' 83 - pushd source 84 - mkdir -p appview/pages/static/{fonts,icons} 85 - cp -f ${htmx-src} appview/pages/static/htmx.min.js 86 - cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 87 - cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 88 - cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 89 - cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 90 - ${pkgs.tailwindcss}/bin/tailwindcss -i input.css -o appview/pages/static/tw.css 91 - popd 92 - ''; 93 - doCheck = false; 94 - subPackages = [ "cmd/appview" ]; 95 - vendorHash = goModHash; 96 - CGO_ENABLED = 1; 97 - stdenv = pkgsStatic.stdenv; 230 + config = mkIf config.services.tangled-appview.enable { 231 + systemd.services.tangled-appview = { 232 + description = "tangled appview service"; 233 + wantedBy = ["multi-user.target"]; 234 + 235 + serviceConfig = { 236 + ListenStream = "0.0.0.0:${toString config.services.tangled-appview.port}"; 237 + ExecStart = "${self.packages.${pkgs.system}.appview}/bin/appview"; 238 + Restart = "always"; 98 239 }; 99 240 100 - knotserver = with final; 101 - final.pkgsStatic.buildGoModule { 102 - pname = "knotserver"; 103 - version = "0.1.0"; 104 - src = gitignoreSource ./.; 105 - nativeBuildInputs = [ final.makeWrapper ]; 106 - subPackages = [ "cmd/knotserver" ]; 107 - vendorHash = goModHash; 108 - installPhase = '' 109 - runHook preInstall 241 + environment = { 242 + TANGLED_DB_PATH = "appview.db"; 243 + TANGLED_COOKIE_SECRET = config.services.tangled-appview.cookie_secret; 244 + }; 245 + }; 246 + }; 247 + }; 110 248 111 - mkdir -p $out/bin 112 - cp $GOPATH/bin/knotserver $out/bin/knotserver 249 + nixosModules.knotserver = { 250 + config, 251 + pkgs, 252 + lib, 253 + ... 254 + }: let 255 + cfg = config.services.tangled-knotserver; 256 + in 257 + with lib; { 258 + options = { 259 + services.tangled-knotserver = { 260 + enable = mkOption { 261 + type = types.bool; 262 + default = false; 263 + description = "Enable a tangled knotserver"; 264 + }; 113 265 114 - wrapProgram $out/bin/knotserver \ 115 - --prefix PATH : ${pkgs.git}/bin 266 + appviewEndpoint = mkOption { 267 + type = types.str; 268 + default = "https://tangled.sh"; 269 + description = "Appview endpoint"; 270 + }; 116 271 117 - runHook postInstall 118 - ''; 119 - CGO_ENABLED = 1; 272 + gitUser = mkOption { 273 + type = types.str; 274 + default = "git"; 275 + description = "User that hosts git repos and performs git operations"; 120 276 }; 121 - knotserver-unwrapped = final.pkgsStatic.buildGoModule { 122 - pname = "knotserver"; 123 - version = "0.1.0"; 124 - src = gitignoreSource ./.; 125 - subPackages = [ "cmd/knotserver" ]; 126 - vendorHash = goModHash; 127 - CGO_ENABLED = 1; 128 - }; 129 - repoguard = buildCmdPackage "repoguard"; 130 - keyfetch = buildCmdPackage "keyfetch"; 131 - }; 132 - packages = forAllSystems (system: { 133 - inherit 134 - (nixpkgsFor."${system}") 135 - indigo-lexgen 136 - appview 137 - knotserver 138 - knotserver-unwrapped 139 - repoguard 140 - keyfetch 141 - ; 142 - }); 143 - defaultPackage = forAllSystems (system: nixpkgsFor.${system}.appview); 144 - formatter = forAllSystems (system: nixpkgsFor."${system}".alejandra); 145 - devShells = forAllSystems (system: 146 - let 147 - pkgs = nixpkgsFor.${system}; 148 - staticShell = pkgs.mkShell.override { 149 - stdenv = pkgs.pkgsStatic.stdenv; 150 - }; 151 - in 152 - { 153 - default = staticShell { 154 - nativeBuildInputs = [ 155 - pkgs.go 156 - pkgs.air 157 - pkgs.gopls 158 - pkgs.httpie 159 - pkgs.indigo-lexgen 160 - pkgs.litecli 161 - pkgs.websocat 162 - pkgs.tailwindcss 163 - pkgs.nixos-shell 164 - ]; 165 - shellHook = '' 166 - mkdir -p appview/pages/static/{fonts,icons} 167 - cp -f ${htmx-src} appview/pages/static/htmx.min.js 168 - cp -rf ${lucide-src}/*.svg appview/pages/static/icons/ 169 - cp -f ${inter-fonts-src}/web/InterVariable*.woff2 appview/pages/static/fonts/ 170 - cp -f ${inter-fonts-src}/web/InterDisplay*.woff2 appview/pages/static/fonts/ 171 - cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono-Regular.woff2 appview/pages/static/fonts/ 172 - ''; 173 - }; 174 - }); 175 - apps = forAllSystems (system: 176 - let 177 - pkgs = nixpkgsFor."${system}"; 178 - air-watcher = name: 179 - pkgs.writeShellScriptBin "run" 180 - '' 181 - TANGLED_DEV=true ${pkgs.air}/bin/air -c /dev/null \ 182 - -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" \ 183 - -build.bin "./out/${name}.out" \ 184 - -build.include_ext "go" 185 - ''; 186 - tailwind-watcher = 187 - pkgs.writeShellScriptBin "run" 188 - '' 189 - ${pkgs.tailwindcss}/bin/tailwindcss -w -i input.css -o ./appview/pages/static/tw.css 190 - ''; 191 - in 192 - { 193 - watch-appview = { 194 - type = "app"; 195 - program = ''${air-watcher "appview"}/bin/run''; 196 - }; 197 - watch-knotserver = { 198 - type = "app"; 199 - program = ''${air-watcher "knotserver"}/bin/run''; 200 - }; 201 - watch-tailwind = { 202 - type = "app"; 203 - program = ''${tailwind-watcher}/bin/run''; 204 - }; 205 - }); 206 277 207 - nixosModules.appview = 208 - { config 209 - , pkgs 210 - , lib 211 - , ... 212 - }: 213 - with lib; { 214 - options = { 215 - services.tangled-appview = { 216 - enable = mkOption { 217 - type = types.bool; 218 - default = false; 219 - description = "Enable tangled appview"; 220 - }; 221 - port = mkOption { 222 - type = types.int; 223 - default = 3000; 224 - description = "Port to run the appview on"; 225 - }; 226 - cookie_secret = mkOption { 227 - type = types.str; 228 - default = "00000000000000000000000000000000"; 229 - description = "Cookie secret"; 230 - }; 231 - }; 278 + openFirewall = mkOption { 279 + type = types.bool; 280 + default = true; 281 + description = "Open port 22 in the firewall for ssh"; 232 282 }; 233 283 234 - config = mkIf config.services.tangled-appview.enable { 235 - systemd.services.tangled-appview = { 236 - description = "tangled appview service"; 237 - wantedBy = [ "multi-user.target" ]; 284 + stateDir = mkOption { 285 + type = types.path; 286 + default = "/home/${cfg.gitUser}"; 287 + description = "Tangled knot data directory"; 288 + }; 238 289 239 - serviceConfig = { 240 - ListenStream = "0.0.0.0:${toString config.services.tangled-appview.port}"; 241 - ExecStart = "${self.packages.${pkgs.system}.appview}/bin/appview"; 242 - Restart = "always"; 243 - }; 244 - 245 - environment = { 246 - TANGLED_DB_PATH = "appview.db"; 247 - TANGLED_COOKIE_SECRET = config.services.tangled-appview.cookie_secret; 248 - }; 290 + repo = { 291 + scanPath = mkOption { 292 + type = types.path; 293 + default = cfg.stateDir; 294 + description = "Path where repositories are scanned from"; 249 295 }; 250 - }; 251 - }; 252 296 253 - nixosModules.knotserver = 254 - { config 255 - , pkgs 256 - , lib 257 - , ... 258 - }: 259 - let 260 - cfg = config.services.tangled-knotserver; 261 - in 262 - with lib; { 263 - options = { 264 - services.tangled-knotserver = { 265 - enable = mkOption { 266 - type = types.bool; 267 - default = false; 268 - description = "Enable a tangled knotserver"; 297 + mainBranch = mkOption { 298 + type = types.str; 299 + default = "main"; 300 + description = "Default branch name for repositories"; 269 301 }; 302 + }; 270 303 271 - appviewEndpoint = mkOption { 304 + server = { 305 + listenAddr = mkOption { 272 306 type = types.str; 273 - default = "https://tangled.sh"; 274 - description = "Appview endpoint"; 307 + default = "0.0.0.0:5555"; 308 + description = "Address to listen on"; 275 309 }; 276 310 277 - gitUser = mkOption { 311 + internalListenAddr = mkOption { 278 312 type = types.str; 279 - default = "git"; 280 - description = "User that hosts git repos and performs git operations"; 313 + default = "127.0.0.1:5444"; 314 + description = "Internal address for inter-service communication"; 281 315 }; 282 316 283 - openFirewall = mkOption { 284 - type = types.bool; 285 - default = true; 286 - description = "Open port 22 in the firewall for ssh"; 317 + secretFile = mkOption { 318 + type = lib.types.path; 319 + example = "KNOT_SERVER_SECRET=<hash>"; 320 + description = "File containing secret key provided by appview (required)"; 287 321 }; 288 322 289 - stateDir = mkOption { 323 + dbPath = mkOption { 290 324 type = types.path; 291 - default = "/home/${cfg.gitUser}"; 292 - description = "Tangled knot data directory"; 325 + default = "${cfg.stateDir}/knotserver.db"; 326 + description = "Path to the database file"; 293 327 }; 294 328 295 - repo = { 296 - scanPath = mkOption { 297 - type = types.path; 298 - default = cfg.stateDir; 299 - description = "Path where repositories are scanned from"; 300 - }; 301 - 302 - mainBranch = mkOption { 303 - type = types.str; 304 - default = "main"; 305 - description = "Default branch name for repositories"; 306 - }; 329 + hostname = mkOption { 330 + type = types.str; 331 + example = "knot.tangled.sh"; 332 + description = "Hostname for the server (required)"; 307 333 }; 308 334 309 - server = { 310 - listenAddr = mkOption { 311 - type = types.str; 312 - default = "0.0.0.0:5555"; 313 - description = "Address to listen on"; 314 - }; 315 - 316 - internalListenAddr = mkOption { 317 - type = types.str; 318 - default = "127.0.0.1:5444"; 319 - description = "Internal address for inter-service communication"; 320 - }; 321 - 322 - secretFile = mkOption { 323 - type = lib.types.path; 324 - example = "KNOT_SERVER_SECRET=<hash>"; 325 - description = "File containing secret key provided by appview (required)"; 326 - }; 327 - 328 - dbPath = mkOption { 329 - type = types.path; 330 - default = "${cfg.stateDir}/knotserver.db"; 331 - description = "Path to the database file"; 332 - }; 333 - 334 - hostname = mkOption { 335 - type = types.str; 336 - example = "knot.tangled.sh"; 337 - description = "Hostname for the server (required)"; 338 - }; 339 - 340 - dev = mkOption { 341 - type = types.bool; 342 - default = false; 343 - description = "Enable development mode (disables signature verification)"; 344 - }; 335 + dev = mkOption { 336 + type = types.bool; 337 + default = false; 338 + description = "Enable development mode (disables signature verification)"; 345 339 }; 346 340 }; 347 341 }; 342 + }; 348 343 349 - config = mkIf cfg.enable { 350 - environment.systemPackages = with pkgs; [ git ]; 344 + config = mkIf cfg.enable { 345 + environment.systemPackages = with pkgs; [git]; 351 346 352 - system.activationScripts.gitConfig = '' 353 - mkdir -p "${cfg.repo.scanPath}" 354 - chown -R ${cfg.gitUser}:${cfg.gitUser} \ 355 - "${cfg.repo.scanPath}" 347 + system.activationScripts.gitConfig = '' 348 + mkdir -p "${cfg.repo.scanPath}" 349 + chown -R ${cfg.gitUser}:${cfg.gitUser} \ 350 + "${cfg.repo.scanPath}" 356 351 357 - mkdir -p "${cfg.stateDir}/.config/git" 358 - cat > "${cfg.stateDir}/.config/git/config" << EOF 359 - [user] 360 - name = Git User 361 - email = git@example.com 362 - EOF 363 - chown -R ${cfg.gitUser}:${cfg.gitUser} \ 364 - "${cfg.stateDir}" 365 - ''; 352 + mkdir -p "${cfg.stateDir}/.config/git" 353 + cat > "${cfg.stateDir}/.config/git/config" << EOF 354 + [user] 355 + name = Git User 356 + email = git@example.com 357 + EOF 358 + chown -R ${cfg.gitUser}:${cfg.gitUser} \ 359 + "${cfg.stateDir}" 360 + ''; 366 361 367 - users.users.${cfg.gitUser} = { 368 - isSystemUser = true; 369 - useDefaultShell = true; 370 - home = cfg.stateDir; 371 - createHome = true; 372 - group = cfg.gitUser; 373 - }; 362 + users.users.${cfg.gitUser} = { 363 + isSystemUser = true; 364 + useDefaultShell = true; 365 + home = cfg.stateDir; 366 + createHome = true; 367 + group = cfg.gitUser; 368 + }; 374 369 375 - users.groups.${cfg.gitUser} = { }; 370 + users.groups.${cfg.gitUser} = {}; 376 371 377 - services.openssh = { 378 - enable = true; 379 - extraConfig = '' 380 - Match User ${cfg.gitUser} 381 - AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 382 - AuthorizedKeysCommandUser nobody 383 - ''; 384 - }; 372 + services.openssh = { 373 + enable = true; 374 + extraConfig = '' 375 + Match User ${cfg.gitUser} 376 + AuthorizedKeysCommand /etc/ssh/keyfetch_wrapper 377 + AuthorizedKeysCommandUser nobody 378 + ''; 379 + }; 385 380 386 - environment.etc."ssh/keyfetch_wrapper" = { 387 - mode = "0555"; 388 - text = '' 389 - #!${pkgs.stdenv.shell} 390 - ${self.packages.${pkgs.system}.keyfetch}/bin/keyfetch \ 391 - -repoguard-path ${self.packages.${pkgs.system}.repoguard}/bin/repoguard \ 392 - -internal-api "http://${cfg.server.internalListenAddr}" \ 393 - -git-dir "${cfg.repo.scanPath}" \ 394 - -log-path /tmp/repoguard.log 395 - ''; 396 - }; 381 + environment.etc."ssh/keyfetch_wrapper" = { 382 + mode = "0555"; 383 + text = '' 384 + #!${pkgs.stdenv.shell} 385 + ${self.packages.${pkgs.system}.keyfetch}/bin/keyfetch \ 386 + -repoguard-path ${self.packages.${pkgs.system}.repoguard}/bin/repoguard \ 387 + -internal-api "http://${cfg.server.internalListenAddr}" \ 388 + -git-dir "${cfg.repo.scanPath}" \ 389 + -log-path /tmp/repoguard.log 390 + ''; 391 + }; 397 392 398 - systemd.services.knotserver = { 399 - description = "knotserver service"; 400 - after = [ "network.target" "sshd.service" ]; 401 - wantedBy = [ "multi-user.target" ]; 402 - serviceConfig = { 403 - User = cfg.gitUser; 404 - WorkingDirectory = cfg.stateDir; 405 - Environment = [ 406 - "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 407 - "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 408 - "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 409 - "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 410 - "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 411 - "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 412 - "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 413 - ]; 414 - EnvironmentFile = cfg.server.secretFile; 415 - ExecStart = "${self.packages.${pkgs.system}.knotserver}/bin/knotserver"; 416 - Restart = "always"; 417 - }; 393 + systemd.services.knotserver = { 394 + description = "knotserver service"; 395 + after = ["network.target" "sshd.service"]; 396 + wantedBy = ["multi-user.target"]; 397 + serviceConfig = { 398 + User = cfg.gitUser; 399 + WorkingDirectory = cfg.stateDir; 400 + Environment = [ 401 + "KNOT_REPO_SCAN_PATH=${cfg.repo.scanPath}" 402 + "KNOT_REPO_MAIN_BRANCH=${cfg.repo.mainBranch}" 403 + "APPVIEW_ENDPOINT=${cfg.appviewEndpoint}" 404 + "KNOT_SERVER_INTERNAL_LISTEN_ADDR=${cfg.server.internalListenAddr}" 405 + "KNOT_SERVER_LISTEN_ADDR=${cfg.server.listenAddr}" 406 + "KNOT_SERVER_DB_PATH=${cfg.server.dbPath}" 407 + "KNOT_SERVER_HOSTNAME=${cfg.server.hostname}" 408 + ]; 409 + EnvironmentFile = cfg.server.secretFile; 410 + ExecStart = "${self.packages.${pkgs.system}.knotserver}/bin/knotserver"; 411 + Restart = "always"; 418 412 }; 419 - 420 - networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ 22 ]; 421 413 }; 414 + 415 + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [22]; 422 416 }; 417 + }; 423 418 424 - nixosConfigurations.knotVM = nixpkgs.lib.nixosSystem { 425 - system = "x86_64-linux"; 426 - modules = [ 427 - self.nixosModules.knotserver 428 - ({ config 429 - , pkgs 430 - , ... 431 - }: { 432 - virtualisation.memorySize = 2048; 433 - virtualisation.cores = 2; 434 - services.getty.autologinUser = "root"; 435 - environment.systemPackages = with pkgs; [ curl vim git ]; 436 - systemd.tmpfiles.rules = 437 - let 438 - u = config.services.tangled-knotserver.gitUser; 439 - g = config.services.tangled-knotserver.gitUser; 440 - in 441 - [ 442 - "d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first 443 - "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=5b42390da4c6659f34c9a545adebd8af82c4a19960d735f651e3d582623ba9f2" 444 - ]; 445 - services.tangled-knotserver = { 446 - enable = true; 447 - server = { 448 - secretFile = "/var/lib/knotserver/secret"; 449 - hostname = "localhost:6000"; 450 - listenAddr = "0.0.0.0:6000"; 451 - }; 419 + nixosConfigurations.knotVM = nixpkgs.lib.nixosSystem { 420 + system = "x86_64-linux"; 421 + modules = [ 422 + self.nixosModules.knotserver 423 + ({ 424 + config, 425 + pkgs, 426 + ... 427 + }: { 428 + virtualisation.memorySize = 2048; 429 + virtualisation.diskSize = 10 * 1024; 430 + virtualisation.cores = 2; 431 + services.getty.autologinUser = "root"; 432 + environment.systemPackages = with pkgs; [curl vim git]; 433 + systemd.tmpfiles.rules = let 434 + u = config.services.tangled-knotserver.gitUser; 435 + g = config.services.tangled-knotserver.gitUser; 436 + in [ 437 + "d /var/lib/knotserver 0770 ${u} ${g} - -" # Create the directory first 438 + "f+ /var/lib/knotserver/secret 0660 ${u} ${g} - KNOT_SERVER_SECRET=679f15000084699abc6a20d3ef449efa3656583f38e456a08f0638250688ff2e" 439 + ]; 440 + services.tangled-knotserver = { 441 + enable = true; 442 + server = { 443 + secretFile = "/var/lib/knotserver/secret"; 444 + hostname = "localhost:6000"; 445 + listenAddr = "0.0.0.0:6000"; 452 446 }; 453 - }) 454 - ]; 455 - }; 447 + }; 448 + }) 449 + ]; 456 450 }; 451 + }; 457 452 }
+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=
+27 -1
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 { ··· 191 192 } else { 192 193 return "", ErrBinaryFile 193 194 } 195 + } 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) 194 220 } 195 221 196 222 func (g *GitRepo) Tags() ([]*TagReference, error) {
+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)
+79 -1
knotserver/routes.go
··· 194 194 return 195 195 } 196 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 + 197 237 func (h *Handle) Blob(w http.ResponseWriter, r *http.Request) { 198 238 treePath := chi.URLParam(r, "*") 199 239 ref := chi.URLParam(r, "ref") 200 240 ref, _ = url.PathUnescape(ref) 201 241 202 - l := h.l.With("handler", "FileContent", "ref", ref, "treePath", treePath) 242 + l := h.l.With("handler", "Blob", "ref", ref, "treePath", treePath) 203 243 204 244 path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 205 245 gr, err := git.Open(path, ref) ··· 559 599 did := data.Did 560 600 name := data.Name 561 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 + } 562 608 563 609 relativeRepoPath := filepath.Join(did, name) 564 610 repoPath, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, relativeRepoPath) ··· 1038 1084 func (h *Handle) Health(w http.ResponseWriter, r *http.Request) { 1039 1085 w.Write([]byte("ok")) 1040 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 + }
-29
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 - "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 - }
+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 + }
+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 + }
-34
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 - "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 - }
-1
lexicons/publicKey.json
··· 22 22 }, 23 23 "name": { 24 24 "type": "string", 25 - "format": "string", 26 25 "description": "human-readable name for this key" 27 26 }, 28 27 "createdAt": {
-29
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 - "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 - }
+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)
+5
scripts/generate-jwks.sh
··· 1 + #! /usr/bin/env bash 2 + 3 + set -e 4 + 5 + go run ./cmd/genjwks/