+1
-1
.air/appview.toml
+1
-1
.air/appview.toml
+6
.gitignore
+6
.gitignore
+31
api/tangled/actorprofile.go
+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
+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
+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
-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
+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
+3
appview/consts.go
+150
appview/db/artifact.go
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
+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
+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
+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
+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
+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
+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
+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
-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
+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
+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
+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/repo/blob.html
+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
+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
+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
+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
+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
+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
+26
-17
appview/pages/templates/repo/issues/issues.html
···
1
1
{{ define "title" }}issues · {{ .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
+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
+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
+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
+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
+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
+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
+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-['ยท']">
+2
-2
appview/pages/templates/repo/tree.html
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
-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
+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
+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
+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
+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
+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
+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
-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
+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
+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
+1
-1
appview/tid.go
+80
appview/xrpcclient/xrpc.go
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+2
-2
cmd/appview/main.go
+15
-13
cmd/gen.go
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+4
knotserver/handler.go
+79
-1
knotserver/routes.go
+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
+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
+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
+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
-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
+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
+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
-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
-1
lexicons/publicKey.json
-29
lexicons/star.json
-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
+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)