+2
api/tangled/actorprofile.go
+2
api/tangled/actorprofile.go
···
18
18
// RECORDTYPE: ActorProfile
19
19
type ActorProfile struct {
20
20
LexiconTypeID string `json:"$type,const=sh.tangled.actor.profile" cborgen:"$type,const=sh.tangled.actor.profile"`
21
+
// avatar: Small image to be displayed next to posts from account. AKA, 'profile picture'
22
+
Avatar *util.LexBlob `json:"avatar,omitempty" cborgen:"avatar,omitempty"`
21
23
// bluesky: Include link to this account on Bluesky.
22
24
Bluesky bool `json:"bluesky" cborgen:"bluesky"`
23
25
// description: Free-form profile description text.
+693
-9
api/tangled/cbor_gen.go
+693
-9
api/tangled/cbor_gen.go
···
26
26
}
27
27
28
28
cw := cbg.NewCborWriter(w)
29
-
fieldCount := 8
29
+
fieldCount := 9
30
+
31
+
if t.Avatar == nil {
32
+
fieldCount--
33
+
}
30
34
31
35
if t.Description == nil {
32
36
fieldCount--
···
144
148
return err
145
149
}
146
150
151
+
}
152
+
}
153
+
154
+
// t.Avatar (util.LexBlob) (struct)
155
+
if t.Avatar != nil {
156
+
157
+
if len("avatar") > 1000000 {
158
+
return xerrors.Errorf("Value in field \"avatar\" was too long")
159
+
}
160
+
161
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("avatar"))); err != nil {
162
+
return err
163
+
}
164
+
if _, err := cw.WriteString(string("avatar")); err != nil {
165
+
return err
166
+
}
167
+
168
+
if err := t.Avatar.MarshalCBOR(cw); err != nil {
169
+
return err
147
170
}
148
171
}
149
172
···
428
451
}
429
452
430
453
}
454
+
}
455
+
// t.Avatar (util.LexBlob) (struct)
456
+
case "avatar":
457
+
458
+
{
459
+
460
+
b, err := cr.ReadByte()
461
+
if err != nil {
462
+
return err
463
+
}
464
+
if b != cbg.CborNull[0] {
465
+
if err := cr.UnreadByte(); err != nil {
466
+
return err
467
+
}
468
+
t.Avatar = new(util.LexBlob)
469
+
if err := t.Avatar.UnmarshalCBOR(cr); err != nil {
470
+
return xerrors.Errorf("unmarshaling t.Avatar pointer: %w", err)
471
+
}
472
+
}
473
+
431
474
}
432
475
// t.Bluesky (bool) (bool)
433
476
case "bluesky":
···
6938
6981
}
6939
6982
6940
6983
cw := cbg.NewCborWriter(w)
6941
-
fieldCount := 5
6984
+
fieldCount := 7
6942
6985
6943
6986
if t.Body == nil {
6987
+
fieldCount--
6988
+
}
6989
+
6990
+
if t.Mentions == nil {
6991
+
fieldCount--
6992
+
}
6993
+
6994
+
if t.References == nil {
6944
6995
fieldCount--
6945
6996
}
6946
6997
···
7045
7096
return err
7046
7097
}
7047
7098
7099
+
// t.Mentions ([]string) (slice)
7100
+
if t.Mentions != nil {
7101
+
7102
+
if len("mentions") > 1000000 {
7103
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
7104
+
}
7105
+
7106
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
7107
+
return err
7108
+
}
7109
+
if _, err := cw.WriteString(string("mentions")); err != nil {
7110
+
return err
7111
+
}
7112
+
7113
+
if len(t.Mentions) > 8192 {
7114
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
7115
+
}
7116
+
7117
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
7118
+
return err
7119
+
}
7120
+
for _, v := range t.Mentions {
7121
+
if len(v) > 1000000 {
7122
+
return xerrors.Errorf("Value in field v was too long")
7123
+
}
7124
+
7125
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7126
+
return err
7127
+
}
7128
+
if _, err := cw.WriteString(string(v)); err != nil {
7129
+
return err
7130
+
}
7131
+
7132
+
}
7133
+
}
7134
+
7048
7135
// t.CreatedAt (string) (string)
7049
7136
if len("createdAt") > 1000000 {
7050
7137
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7067
7154
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7068
7155
return err
7069
7156
}
7157
+
7158
+
// t.References ([]string) (slice)
7159
+
if t.References != nil {
7160
+
7161
+
if len("references") > 1000000 {
7162
+
return xerrors.Errorf("Value in field \"references\" was too long")
7163
+
}
7164
+
7165
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
7166
+
return err
7167
+
}
7168
+
if _, err := cw.WriteString(string("references")); err != nil {
7169
+
return err
7170
+
}
7171
+
7172
+
if len(t.References) > 8192 {
7173
+
return xerrors.Errorf("Slice value in field t.References was too long")
7174
+
}
7175
+
7176
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
7177
+
return err
7178
+
}
7179
+
for _, v := range t.References {
7180
+
if len(v) > 1000000 {
7181
+
return xerrors.Errorf("Value in field v was too long")
7182
+
}
7183
+
7184
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7185
+
return err
7186
+
}
7187
+
if _, err := cw.WriteString(string(v)); err != nil {
7188
+
return err
7189
+
}
7190
+
7191
+
}
7192
+
}
7070
7193
return nil
7071
7194
}
7072
7195
···
7095
7218
7096
7219
n := extra
7097
7220
7098
-
nameBuf := make([]byte, 9)
7221
+
nameBuf := make([]byte, 10)
7099
7222
for i := uint64(0); i < n; i++ {
7100
7223
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7101
7224
if err != nil {
···
7164
7287
}
7165
7288
7166
7289
t.Title = string(sval)
7290
+
}
7291
+
// t.Mentions ([]string) (slice)
7292
+
case "mentions":
7293
+
7294
+
maj, extra, err = cr.ReadHeader()
7295
+
if err != nil {
7296
+
return err
7297
+
}
7298
+
7299
+
if extra > 8192 {
7300
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
7301
+
}
7302
+
7303
+
if maj != cbg.MajArray {
7304
+
return fmt.Errorf("expected cbor array")
7305
+
}
7306
+
7307
+
if extra > 0 {
7308
+
t.Mentions = make([]string, extra)
7309
+
}
7310
+
7311
+
for i := 0; i < int(extra); i++ {
7312
+
{
7313
+
var maj byte
7314
+
var extra uint64
7315
+
var err error
7316
+
_ = maj
7317
+
_ = extra
7318
+
_ = err
7319
+
7320
+
{
7321
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7322
+
if err != nil {
7323
+
return err
7324
+
}
7325
+
7326
+
t.Mentions[i] = string(sval)
7327
+
}
7328
+
7329
+
}
7167
7330
}
7168
7331
// t.CreatedAt (string) (string)
7169
7332
case "createdAt":
···
7176
7339
7177
7340
t.CreatedAt = string(sval)
7178
7341
}
7342
+
// t.References ([]string) (slice)
7343
+
case "references":
7344
+
7345
+
maj, extra, err = cr.ReadHeader()
7346
+
if err != nil {
7347
+
return err
7348
+
}
7349
+
7350
+
if extra > 8192 {
7351
+
return fmt.Errorf("t.References: array too large (%d)", extra)
7352
+
}
7353
+
7354
+
if maj != cbg.MajArray {
7355
+
return fmt.Errorf("expected cbor array")
7356
+
}
7357
+
7358
+
if extra > 0 {
7359
+
t.References = make([]string, extra)
7360
+
}
7361
+
7362
+
for i := 0; i < int(extra); i++ {
7363
+
{
7364
+
var maj byte
7365
+
var extra uint64
7366
+
var err error
7367
+
_ = maj
7368
+
_ = extra
7369
+
_ = err
7370
+
7371
+
{
7372
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7373
+
if err != nil {
7374
+
return err
7375
+
}
7376
+
7377
+
t.References[i] = string(sval)
7378
+
}
7379
+
7380
+
}
7381
+
}
7179
7382
7180
7383
default:
7181
7384
// Field doesn't exist on this type, so ignore it
···
7194
7397
}
7195
7398
7196
7399
cw := cbg.NewCborWriter(w)
7197
-
fieldCount := 5
7400
+
fieldCount := 7
7401
+
7402
+
if t.Mentions == nil {
7403
+
fieldCount--
7404
+
}
7405
+
7406
+
if t.References == nil {
7407
+
fieldCount--
7408
+
}
7198
7409
7199
7410
if t.ReplyTo == nil {
7200
7411
fieldCount--
···
7301
7512
}
7302
7513
}
7303
7514
7515
+
// t.Mentions ([]string) (slice)
7516
+
if t.Mentions != nil {
7517
+
7518
+
if len("mentions") > 1000000 {
7519
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
7520
+
}
7521
+
7522
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
7523
+
return err
7524
+
}
7525
+
if _, err := cw.WriteString(string("mentions")); err != nil {
7526
+
return err
7527
+
}
7528
+
7529
+
if len(t.Mentions) > 8192 {
7530
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
7531
+
}
7532
+
7533
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
7534
+
return err
7535
+
}
7536
+
for _, v := range t.Mentions {
7537
+
if len(v) > 1000000 {
7538
+
return xerrors.Errorf("Value in field v was too long")
7539
+
}
7540
+
7541
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7542
+
return err
7543
+
}
7544
+
if _, err := cw.WriteString(string(v)); err != nil {
7545
+
return err
7546
+
}
7547
+
7548
+
}
7549
+
}
7550
+
7304
7551
// t.CreatedAt (string) (string)
7305
7552
if len("createdAt") > 1000000 {
7306
7553
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7323
7570
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7324
7571
return err
7325
7572
}
7573
+
7574
+
// t.References ([]string) (slice)
7575
+
if t.References != nil {
7576
+
7577
+
if len("references") > 1000000 {
7578
+
return xerrors.Errorf("Value in field \"references\" was too long")
7579
+
}
7580
+
7581
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
7582
+
return err
7583
+
}
7584
+
if _, err := cw.WriteString(string("references")); err != nil {
7585
+
return err
7586
+
}
7587
+
7588
+
if len(t.References) > 8192 {
7589
+
return xerrors.Errorf("Slice value in field t.References was too long")
7590
+
}
7591
+
7592
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
7593
+
return err
7594
+
}
7595
+
for _, v := range t.References {
7596
+
if len(v) > 1000000 {
7597
+
return xerrors.Errorf("Value in field v was too long")
7598
+
}
7599
+
7600
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
7601
+
return err
7602
+
}
7603
+
if _, err := cw.WriteString(string(v)); err != nil {
7604
+
return err
7605
+
}
7606
+
7607
+
}
7608
+
}
7326
7609
return nil
7327
7610
}
7328
7611
···
7351
7634
7352
7635
n := extra
7353
7636
7354
-
nameBuf := make([]byte, 9)
7637
+
nameBuf := make([]byte, 10)
7355
7638
for i := uint64(0); i < n; i++ {
7356
7639
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7357
7640
if err != nil {
···
7419
7702
}
7420
7703
7421
7704
t.ReplyTo = (*string)(&sval)
7705
+
}
7706
+
}
7707
+
// t.Mentions ([]string) (slice)
7708
+
case "mentions":
7709
+
7710
+
maj, extra, err = cr.ReadHeader()
7711
+
if err != nil {
7712
+
return err
7713
+
}
7714
+
7715
+
if extra > 8192 {
7716
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
7717
+
}
7718
+
7719
+
if maj != cbg.MajArray {
7720
+
return fmt.Errorf("expected cbor array")
7721
+
}
7722
+
7723
+
if extra > 0 {
7724
+
t.Mentions = make([]string, extra)
7725
+
}
7726
+
7727
+
for i := 0; i < int(extra); i++ {
7728
+
{
7729
+
var maj byte
7730
+
var extra uint64
7731
+
var err error
7732
+
_ = maj
7733
+
_ = extra
7734
+
_ = err
7735
+
7736
+
{
7737
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7738
+
if err != nil {
7739
+
return err
7740
+
}
7741
+
7742
+
t.Mentions[i] = string(sval)
7743
+
}
7744
+
7422
7745
}
7423
7746
}
7424
7747
// t.CreatedAt (string) (string)
···
7431
7754
}
7432
7755
7433
7756
t.CreatedAt = string(sval)
7757
+
}
7758
+
// t.References ([]string) (slice)
7759
+
case "references":
7760
+
7761
+
maj, extra, err = cr.ReadHeader()
7762
+
if err != nil {
7763
+
return err
7764
+
}
7765
+
7766
+
if extra > 8192 {
7767
+
return fmt.Errorf("t.References: array too large (%d)", extra)
7768
+
}
7769
+
7770
+
if maj != cbg.MajArray {
7771
+
return fmt.Errorf("expected cbor array")
7772
+
}
7773
+
7774
+
if extra > 0 {
7775
+
t.References = make([]string, extra)
7776
+
}
7777
+
7778
+
for i := 0; i < int(extra); i++ {
7779
+
{
7780
+
var maj byte
7781
+
var extra uint64
7782
+
var err error
7783
+
_ = maj
7784
+
_ = extra
7785
+
_ = err
7786
+
7787
+
{
7788
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
7789
+
if err != nil {
7790
+
return err
7791
+
}
7792
+
7793
+
t.References[i] = string(sval)
7794
+
}
7795
+
7796
+
}
7434
7797
}
7435
7798
7436
7799
default:
···
7614
7977
}
7615
7978
7616
7979
cw := cbg.NewCborWriter(w)
7617
-
fieldCount := 7
7980
+
fieldCount := 9
7618
7981
7619
7982
if t.Body == nil {
7983
+
fieldCount--
7984
+
}
7985
+
7986
+
if t.Mentions == nil {
7987
+
fieldCount--
7988
+
}
7989
+
7990
+
if t.References == nil {
7620
7991
fieldCount--
7621
7992
}
7622
7993
···
7760
8131
return err
7761
8132
}
7762
8133
8134
+
// t.Mentions ([]string) (slice)
8135
+
if t.Mentions != nil {
8136
+
8137
+
if len("mentions") > 1000000 {
8138
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
8139
+
}
8140
+
8141
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
8142
+
return err
8143
+
}
8144
+
if _, err := cw.WriteString(string("mentions")); err != nil {
8145
+
return err
8146
+
}
8147
+
8148
+
if len(t.Mentions) > 8192 {
8149
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
8150
+
}
8151
+
8152
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
8153
+
return err
8154
+
}
8155
+
for _, v := range t.Mentions {
8156
+
if len(v) > 1000000 {
8157
+
return xerrors.Errorf("Value in field v was too long")
8158
+
}
8159
+
8160
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8161
+
return err
8162
+
}
8163
+
if _, err := cw.WriteString(string(v)); err != nil {
8164
+
return err
8165
+
}
8166
+
8167
+
}
8168
+
}
8169
+
7763
8170
// t.CreatedAt (string) (string)
7764
8171
if len("createdAt") > 1000000 {
7765
8172
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
7782
8189
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
7783
8190
return err
7784
8191
}
8192
+
8193
+
// t.References ([]string) (slice)
8194
+
if t.References != nil {
8195
+
8196
+
if len("references") > 1000000 {
8197
+
return xerrors.Errorf("Value in field \"references\" was too long")
8198
+
}
8199
+
8200
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
8201
+
return err
8202
+
}
8203
+
if _, err := cw.WriteString(string("references")); err != nil {
8204
+
return err
8205
+
}
8206
+
8207
+
if len(t.References) > 8192 {
8208
+
return xerrors.Errorf("Slice value in field t.References was too long")
8209
+
}
8210
+
8211
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
8212
+
return err
8213
+
}
8214
+
for _, v := range t.References {
8215
+
if len(v) > 1000000 {
8216
+
return xerrors.Errorf("Value in field v was too long")
8217
+
}
8218
+
8219
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8220
+
return err
8221
+
}
8222
+
if _, err := cw.WriteString(string(v)); err != nil {
8223
+
return err
8224
+
}
8225
+
8226
+
}
8227
+
}
7785
8228
return nil
7786
8229
}
7787
8230
···
7810
8253
7811
8254
n := extra
7812
8255
7813
-
nameBuf := make([]byte, 9)
8256
+
nameBuf := make([]byte, 10)
7814
8257
for i := uint64(0); i < n; i++ {
7815
8258
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
7816
8259
if err != nil {
···
7919
8362
}
7920
8363
}
7921
8364
8365
+
}
8366
+
// t.Mentions ([]string) (slice)
8367
+
case "mentions":
8368
+
8369
+
maj, extra, err = cr.ReadHeader()
8370
+
if err != nil {
8371
+
return err
8372
+
}
8373
+
8374
+
if extra > 8192 {
8375
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
8376
+
}
8377
+
8378
+
if maj != cbg.MajArray {
8379
+
return fmt.Errorf("expected cbor array")
8380
+
}
8381
+
8382
+
if extra > 0 {
8383
+
t.Mentions = make([]string, extra)
8384
+
}
8385
+
8386
+
for i := 0; i < int(extra); i++ {
8387
+
{
8388
+
var maj byte
8389
+
var extra uint64
8390
+
var err error
8391
+
_ = maj
8392
+
_ = extra
8393
+
_ = err
8394
+
8395
+
{
8396
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8397
+
if err != nil {
8398
+
return err
8399
+
}
8400
+
8401
+
t.Mentions[i] = string(sval)
8402
+
}
8403
+
8404
+
}
7922
8405
}
7923
8406
// t.CreatedAt (string) (string)
7924
8407
case "createdAt":
···
7931
8414
7932
8415
t.CreatedAt = string(sval)
7933
8416
}
8417
+
// t.References ([]string) (slice)
8418
+
case "references":
8419
+
8420
+
maj, extra, err = cr.ReadHeader()
8421
+
if err != nil {
8422
+
return err
8423
+
}
8424
+
8425
+
if extra > 8192 {
8426
+
return fmt.Errorf("t.References: array too large (%d)", extra)
8427
+
}
8428
+
8429
+
if maj != cbg.MajArray {
8430
+
return fmt.Errorf("expected cbor array")
8431
+
}
8432
+
8433
+
if extra > 0 {
8434
+
t.References = make([]string, extra)
8435
+
}
8436
+
8437
+
for i := 0; i < int(extra); i++ {
8438
+
{
8439
+
var maj byte
8440
+
var extra uint64
8441
+
var err error
8442
+
_ = maj
8443
+
_ = extra
8444
+
_ = err
8445
+
8446
+
{
8447
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8448
+
if err != nil {
8449
+
return err
8450
+
}
8451
+
8452
+
t.References[i] = string(sval)
8453
+
}
8454
+
8455
+
}
8456
+
}
7934
8457
7935
8458
default:
7936
8459
// Field doesn't exist on this type, so ignore it
···
7949
8472
}
7950
8473
7951
8474
cw := cbg.NewCborWriter(w)
8475
+
fieldCount := 6
7952
8476
7953
-
if _, err := cw.Write([]byte{164}); err != nil {
8477
+
if t.Mentions == nil {
8478
+
fieldCount--
8479
+
}
8480
+
8481
+
if t.References == nil {
8482
+
fieldCount--
8483
+
}
8484
+
8485
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
7954
8486
return err
7955
8487
}
7956
8488
···
8019
8551
return err
8020
8552
}
8021
8553
8554
+
// t.Mentions ([]string) (slice)
8555
+
if t.Mentions != nil {
8556
+
8557
+
if len("mentions") > 1000000 {
8558
+
return xerrors.Errorf("Value in field \"mentions\" was too long")
8559
+
}
8560
+
8561
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("mentions"))); err != nil {
8562
+
return err
8563
+
}
8564
+
if _, err := cw.WriteString(string("mentions")); err != nil {
8565
+
return err
8566
+
}
8567
+
8568
+
if len(t.Mentions) > 8192 {
8569
+
return xerrors.Errorf("Slice value in field t.Mentions was too long")
8570
+
}
8571
+
8572
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Mentions))); err != nil {
8573
+
return err
8574
+
}
8575
+
for _, v := range t.Mentions {
8576
+
if len(v) > 1000000 {
8577
+
return xerrors.Errorf("Value in field v was too long")
8578
+
}
8579
+
8580
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8581
+
return err
8582
+
}
8583
+
if _, err := cw.WriteString(string(v)); err != nil {
8584
+
return err
8585
+
}
8586
+
8587
+
}
8588
+
}
8589
+
8022
8590
// t.CreatedAt (string) (string)
8023
8591
if len("createdAt") > 1000000 {
8024
8592
return xerrors.Errorf("Value in field \"createdAt\" was too long")
···
8040
8608
}
8041
8609
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
8042
8610
return err
8611
+
}
8612
+
8613
+
// t.References ([]string) (slice)
8614
+
if t.References != nil {
8615
+
8616
+
if len("references") > 1000000 {
8617
+
return xerrors.Errorf("Value in field \"references\" was too long")
8618
+
}
8619
+
8620
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("references"))); err != nil {
8621
+
return err
8622
+
}
8623
+
if _, err := cw.WriteString(string("references")); err != nil {
8624
+
return err
8625
+
}
8626
+
8627
+
if len(t.References) > 8192 {
8628
+
return xerrors.Errorf("Slice value in field t.References was too long")
8629
+
}
8630
+
8631
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.References))); err != nil {
8632
+
return err
8633
+
}
8634
+
for _, v := range t.References {
8635
+
if len(v) > 1000000 {
8636
+
return xerrors.Errorf("Value in field v was too long")
8637
+
}
8638
+
8639
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(v))); err != nil {
8640
+
return err
8641
+
}
8642
+
if _, err := cw.WriteString(string(v)); err != nil {
8643
+
return err
8644
+
}
8645
+
8646
+
}
8043
8647
}
8044
8648
return nil
8045
8649
}
···
8069
8673
8070
8674
n := extra
8071
8675
8072
-
nameBuf := make([]byte, 9)
8676
+
nameBuf := make([]byte, 10)
8073
8677
for i := uint64(0); i < n; i++ {
8074
8678
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
8075
8679
if err != nil {
···
8118
8722
8119
8723
t.LexiconTypeID = string(sval)
8120
8724
}
8725
+
// t.Mentions ([]string) (slice)
8726
+
case "mentions":
8727
+
8728
+
maj, extra, err = cr.ReadHeader()
8729
+
if err != nil {
8730
+
return err
8731
+
}
8732
+
8733
+
if extra > 8192 {
8734
+
return fmt.Errorf("t.Mentions: array too large (%d)", extra)
8735
+
}
8736
+
8737
+
if maj != cbg.MajArray {
8738
+
return fmt.Errorf("expected cbor array")
8739
+
}
8740
+
8741
+
if extra > 0 {
8742
+
t.Mentions = make([]string, extra)
8743
+
}
8744
+
8745
+
for i := 0; i < int(extra); i++ {
8746
+
{
8747
+
var maj byte
8748
+
var extra uint64
8749
+
var err error
8750
+
_ = maj
8751
+
_ = extra
8752
+
_ = err
8753
+
8754
+
{
8755
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8756
+
if err != nil {
8757
+
return err
8758
+
}
8759
+
8760
+
t.Mentions[i] = string(sval)
8761
+
}
8762
+
8763
+
}
8764
+
}
8121
8765
// t.CreatedAt (string) (string)
8122
8766
case "createdAt":
8123
8767
···
8128
8772
}
8129
8773
8130
8774
t.CreatedAt = string(sval)
8775
+
}
8776
+
// t.References ([]string) (slice)
8777
+
case "references":
8778
+
8779
+
maj, extra, err = cr.ReadHeader()
8780
+
if err != nil {
8781
+
return err
8782
+
}
8783
+
8784
+
if extra > 8192 {
8785
+
return fmt.Errorf("t.References: array too large (%d)", extra)
8786
+
}
8787
+
8788
+
if maj != cbg.MajArray {
8789
+
return fmt.Errorf("expected cbor array")
8790
+
}
8791
+
8792
+
if extra > 0 {
8793
+
t.References = make([]string, extra)
8794
+
}
8795
+
8796
+
for i := 0; i < int(extra); i++ {
8797
+
{
8798
+
var maj byte
8799
+
var extra uint64
8800
+
var err error
8801
+
_ = maj
8802
+
_ = extra
8803
+
_ = err
8804
+
8805
+
{
8806
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
8807
+
if err != nil {
8808
+
return err
8809
+
}
8810
+
8811
+
t.References[i] = string(sval)
8812
+
}
8813
+
8814
+
}
8131
8815
}
8132
8816
8133
8817
default:
+7
-5
api/tangled/issuecomment.go
+7
-5
api/tangled/issuecomment.go
···
17
17
} //
18
18
// RECORDTYPE: RepoIssueComment
19
19
type RepoIssueComment struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"`
21
-
Body string `json:"body" cborgen:"body"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Issue string `json:"issue" cborgen:"issue"`
24
-
ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue.comment" cborgen:"$type,const=sh.tangled.repo.issue.comment"`
21
+
Body string `json:"body" cborgen:"body"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Issue string `json:"issue" cborgen:"issue"`
24
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
25
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
26
+
ReplyTo *string `json:"replyTo,omitempty" cborgen:"replyTo,omitempty"`
25
27
}
+6
-4
api/tangled/pullcomment.go
+6
-4
api/tangled/pullcomment.go
···
17
17
} //
18
18
// RECORDTYPE: RepoPullComment
19
19
type RepoPullComment struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"`
21
-
Body string `json:"body" cborgen:"body"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Pull string `json:"pull" cborgen:"pull"`
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull.comment" cborgen:"$type,const=sh.tangled.repo.pull.comment"`
21
+
Body string `json:"body" cborgen:"body"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
+
Pull string `json:"pull" cborgen:"pull"`
25
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
24
26
}
+7
-5
api/tangled/repoissue.go
+7
-5
api/tangled/repoissue.go
···
17
17
} //
18
18
// RECORDTYPE: RepoIssue
19
19
type RepoIssue struct {
20
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"`
21
-
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
-
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
-
Repo string `json:"repo" cborgen:"repo"`
24
-
Title string `json:"title" cborgen:"title"`
20
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.issue" cborgen:"$type,const=sh.tangled.repo.issue"`
21
+
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
24
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
25
+
Repo string `json:"repo" cborgen:"repo"`
26
+
Title string `json:"title" cborgen:"title"`
25
27
}
+2
api/tangled/repopull.go
+2
api/tangled/repopull.go
···
20
20
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
21
21
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
22
22
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
23
+
Mentions []string `json:"mentions,omitempty" cborgen:"mentions,omitempty"`
23
24
Patch string `json:"patch" cborgen:"patch"`
25
+
References []string `json:"references,omitempty" cborgen:"references,omitempty"`
24
26
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
25
27
Target *RepoPull_Target `json:"target" cborgen:"target"`
26
28
Title string `json:"title" cborgen:"title"`
+6
-45
appview/commitverify/verify.go
+6
-45
appview/commitverify/verify.go
···
3
3
import (
4
4
"log"
5
5
6
-
"github.com/go-git/go-git/v5/plumbing/object"
7
6
"tangled.org/core/appview/db"
8
7
"tangled.org/core/appview/models"
9
8
"tangled.org/core/crypto"
···
35
34
return ""
36
35
}
37
36
38
-
func GetVerifiedObjectCommits(e db.Execer, emailToDid map[string]string, commits []*object.Commit) (VerifiedCommits, error) {
39
-
ndCommits := []types.NiceDiff{}
40
-
for _, commit := range commits {
41
-
ndCommits = append(ndCommits, ObjectCommitToNiceDiff(commit))
42
-
}
43
-
return GetVerifiedCommits(e, emailToDid, ndCommits)
44
-
}
45
-
46
-
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.NiceDiff) (VerifiedCommits, error) {
37
+
func GetVerifiedCommits(e db.Execer, emailToDid map[string]string, ndCommits []types.Commit) (VerifiedCommits, error) {
47
38
vcs := VerifiedCommits{}
48
39
49
40
didPubkeyCache := make(map[string][]models.PublicKey)
50
41
51
42
for _, commit := range ndCommits {
52
-
c := commit.Commit
53
-
54
-
committerEmail := c.Committer.Email
43
+
committerEmail := commit.Committer.Email
55
44
if did, exists := emailToDid[committerEmail]; exists {
56
45
// check if we've already fetched public keys for this did
57
46
pubKeys, ok := didPubkeyCache[did]
···
67
56
}
68
57
69
58
// try to verify with any associated pubkeys
59
+
payload := commit.Payload()
60
+
signature := commit.PGPSignature
70
61
for _, pk := range pubKeys {
71
-
if _, ok := crypto.VerifyCommitSignature(pk.Key, commit); ok {
62
+
if _, ok := crypto.VerifySignature([]byte(pk.Key), []byte(signature), []byte(payload)); ok {
72
63
73
64
fp, err := crypto.SSHFingerprint(pk.Key)
74
65
if err != nil {
75
66
log.Println("error computing ssh fingerprint:", err)
76
67
}
77
68
78
-
vc := verifiedCommit{fingerprint: fp, hash: c.This}
69
+
vc := verifiedCommit{fingerprint: fp, hash: commit.This}
79
70
vcs[vc] = struct{}{}
80
71
break
81
72
}
···
86
77
87
78
return vcs, nil
88
79
}
89
-
90
-
// ObjectCommitToNiceDiff is a compatibility function to convert a
91
-
// commit object into a NiceDiff structure.
92
-
func ObjectCommitToNiceDiff(c *object.Commit) types.NiceDiff {
93
-
var niceDiff types.NiceDiff
94
-
95
-
// set commit information
96
-
niceDiff.Commit.Message = c.Message
97
-
niceDiff.Commit.Author = c.Author
98
-
niceDiff.Commit.This = c.Hash.String()
99
-
niceDiff.Commit.Committer = c.Committer
100
-
niceDiff.Commit.Tree = c.TreeHash.String()
101
-
niceDiff.Commit.PGPSignature = c.PGPSignature
102
-
103
-
changeId, ok := c.ExtraHeaders["change-id"]
104
-
if ok {
105
-
niceDiff.Commit.ChangedId = string(changeId)
106
-
}
107
-
108
-
// set parent hash if available
109
-
if len(c.ParentHashes) > 0 {
110
-
niceDiff.Commit.Parent = c.ParentHashes[0].String()
111
-
}
112
-
113
-
// XXX: Stats and Diff fields are typically populated
114
-
// after fetching the actual diff information, which isn't
115
-
// directly available in the commit object itself.
116
-
117
-
return niceDiff
118
-
}
+3
-2
appview/db/artifact.go
+3
-2
appview/db/artifact.go
···
8
8
"github.com/go-git/go-git/v5/plumbing"
9
9
"github.com/ipfs/go-cid"
10
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
11
12
)
12
13
13
14
func AddArtifact(e Execer, artifact models.Artifact) error {
···
37
38
return err
38
39
}
39
40
40
-
func GetArtifact(e Execer, filters ...filter) ([]models.Artifact, error) {
41
+
func GetArtifact(e Execer, filters ...orm.Filter) ([]models.Artifact, error) {
41
42
var artifacts []models.Artifact
42
43
43
44
var conditions []string
···
109
110
return artifacts, nil
110
111
}
111
112
112
-
func DeleteArtifact(e Execer, filters ...filter) error {
113
+
func DeleteArtifact(e Execer, filters ...orm.Filter) error {
113
114
var conditions []string
114
115
var args []any
115
116
for _, filter := range filters {
+4
-3
appview/db/collaborators.go
+4
-3
appview/db/collaborators.go
···
6
6
"time"
7
7
8
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
9
10
)
10
11
11
12
func AddCollaborator(e Execer, c models.Collaborator) error {
···
16
17
return err
17
18
}
18
19
19
-
func DeleteCollaborator(e Execer, filters ...filter) error {
20
+
func DeleteCollaborator(e Execer, filters ...orm.Filter) error {
20
21
var conditions []string
21
22
var args []any
22
23
for _, filter := range filters {
···
58
59
return nil, nil
59
60
}
60
61
61
-
return GetRepos(e, 0, FilterIn("at_uri", repoAts))
62
+
return GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
62
63
}
63
64
64
-
func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) {
65
+
func GetCollaborators(e Execer, filters ...orm.Filter) ([]models.Collaborator, error) {
65
66
var collaborators []models.Collaborator
66
67
var conditions []string
67
68
var args []any
+77
-136
appview/db/db.go
+77
-136
appview/db/db.go
···
3
3
import (
4
4
"context"
5
5
"database/sql"
6
-
"fmt"
7
6
"log/slog"
8
-
"reflect"
9
7
"strings"
10
8
11
9
_ "github.com/mattn/go-sqlite3"
12
10
"tangled.org/core/log"
11
+
"tangled.org/core/orm"
13
12
)
14
13
15
14
type DB struct {
···
261
260
did text not null,
262
261
263
262
-- data
263
+
avatar text,
264
264
description text not null,
265
265
include_bluesky integer not null default 0,
266
266
location text,
···
561
561
email_notifications integer not null default 0
562
562
);
563
563
564
+
create table if not exists reference_links (
565
+
id integer primary key autoincrement,
566
+
from_at text not null,
567
+
to_at text not null,
568
+
unique (from_at, to_at)
569
+
);
570
+
564
571
create table if not exists migrations (
565
572
id integer primary key autoincrement,
566
573
name text unique
···
569
576
-- indexes for better performance
570
577
create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc);
571
578
create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
572
-
create index if not exists idx_stars_created on stars(created);
573
-
create index if not exists idx_stars_repo_at_created on stars(repo_at, created);
579
+
create index if not exists idx_references_from_at on reference_links(from_at);
580
+
create index if not exists idx_references_to_at on reference_links(to_at);
574
581
`)
575
582
if err != nil {
576
583
return nil, err
577
584
}
578
585
579
586
// run migrations
580
-
runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
587
+
orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
581
588
tx.Exec(`
582
589
alter table repos add column description text check (length(description) <= 200);
583
590
`)
584
591
return nil
585
592
})
586
593
587
-
runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
594
+
orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
588
595
// add unconstrained column
589
596
_, err := tx.Exec(`
590
597
alter table public_keys
···
607
614
return nil
608
615
})
609
616
610
-
runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
617
+
orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
611
618
_, err := tx.Exec(`
612
619
alter table comments drop column comment_at;
613
620
alter table comments add column rkey text;
···
615
622
return err
616
623
})
617
624
618
-
runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
625
+
orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
619
626
_, err := tx.Exec(`
620
627
alter table comments add column deleted text; -- timestamp
621
628
alter table comments add column edited text; -- timestamp
···
623
630
return err
624
631
})
625
632
626
-
runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
633
+
orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
627
634
_, err := tx.Exec(`
628
635
alter table pulls add column source_branch text;
629
636
alter table pulls add column source_repo_at text;
···
632
639
return err
633
640
})
634
641
635
-
runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
642
+
orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
636
643
_, err := tx.Exec(`
637
644
alter table repos add column source text;
638
645
`)
···
644
651
//
645
652
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
646
653
conn.ExecContext(ctx, "pragma foreign_keys = off;")
647
-
runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
654
+
orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
648
655
_, err := tx.Exec(`
649
656
create table pulls_new (
650
657
-- identifiers
···
701
708
})
702
709
conn.ExecContext(ctx, "pragma foreign_keys = on;")
703
710
704
-
runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
711
+
orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
705
712
tx.Exec(`
706
713
alter table repos add column spindle text;
707
714
`)
···
711
718
// drop all knot secrets, add unique constraint to knots
712
719
//
713
720
// knots will henceforth use service auth for signed requests
714
-
runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
721
+
orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
715
722
_, err := tx.Exec(`
716
723
create table registrations_new (
717
724
id integer primary key autoincrement,
···
734
741
})
735
742
736
743
// recreate and add rkey + created columns with default constraint
737
-
runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
744
+
orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
738
745
// create new table
739
746
// - repo_at instead of repo integer
740
747
// - rkey field
···
788
795
return err
789
796
})
790
797
791
-
runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
798
+
orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
792
799
_, err := tx.Exec(`
793
800
alter table issues add column rkey text not null default '';
794
801
···
800
807
})
801
808
802
809
// repurpose the read-only column to "needs-upgrade"
803
-
runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
810
+
orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
804
811
_, err := tx.Exec(`
805
812
alter table registrations rename column read_only to needs_upgrade;
806
813
`)
···
808
815
})
809
816
810
817
// require all knots to upgrade after the release of total xrpc
811
-
runMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
818
+
orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
812
819
_, err := tx.Exec(`
813
820
update registrations set needs_upgrade = 1;
814
821
`)
···
816
823
})
817
824
818
825
// require all knots to upgrade after the release of total xrpc
819
-
runMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
826
+
orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
820
827
_, err := tx.Exec(`
821
828
alter table spindles add column needs_upgrade integer not null default 0;
822
829
`)
···
834
841
//
835
842
// disable foreign-keys for the next migration
836
843
conn.ExecContext(ctx, "pragma foreign_keys = off;")
837
-
runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
844
+
orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
838
845
_, err := tx.Exec(`
839
846
create table if not exists issues_new (
840
847
-- identifiers
···
904
911
// - new columns
905
912
// * column "reply_to" which can be any other comment
906
913
// * column "at-uri" which is a generated column
907
-
runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
914
+
orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
908
915
_, err := tx.Exec(`
909
916
create table if not exists issue_comments (
910
917
-- identifiers
···
964
971
//
965
972
// disable foreign-keys for the next migration
966
973
conn.ExecContext(ctx, "pragma foreign_keys = off;")
967
-
runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
974
+
orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
968
975
_, err := tx.Exec(`
969
976
create table if not exists pulls_new (
970
977
-- identifiers
···
1045
1052
//
1046
1053
// disable foreign-keys for the next migration
1047
1054
conn.ExecContext(ctx, "pragma foreign_keys = off;")
1048
-
runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1055
+
orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1049
1056
_, err := tx.Exec(`
1050
1057
create table if not exists pull_submissions_new (
1051
1058
-- identifiers
···
1072
1079
// transfer data, constructing pull_at from pulls table
1073
1080
_, err = tx.Exec(`
1074
1081
insert into pull_submissions_new (id, pull_at, round_number, patch, created)
1075
-
select
1082
+
select
1076
1083
ps.id,
1077
1084
'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey,
1078
1085
ps.round_number,
···
1099
1106
1100
1107
// knots may report the combined patch for a comparison, we can store that on the appview side
1101
1108
// (but not on the pds record), because calculating the combined patch requires a git index
1102
-
runMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1109
+
orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1103
1110
_, err := tx.Exec(`
1104
1111
alter table pull_submissions add column combined text;
1105
1112
`)
1106
1113
return err
1107
1114
})
1108
1115
1109
-
runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1116
+
orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1110
1117
_, err := tx.Exec(`
1111
1118
alter table profile add column pronouns text;
1112
1119
`)
1113
1120
return err
1114
1121
})
1115
1122
1116
-
runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1123
+
orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1117
1124
_, err := tx.Exec(`
1118
1125
alter table repos add column website text;
1119
1126
alter table repos add column topics text;
···
1121
1128
return err
1122
1129
})
1123
1130
1124
-
runMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1131
+
orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1125
1132
_, err := tx.Exec(`
1126
1133
alter table notification_preferences add column user_mentioned integer not null default 1;
1127
1134
`)
1128
1135
return err
1129
1136
})
1130
1137
1131
-
return &DB{
1132
-
db,
1133
-
logger,
1134
-
}, nil
1135
-
}
1138
+
// remove the foreign key constraints from stars.
1139
+
orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1140
+
_, err := tx.Exec(`
1141
+
create table stars_new (
1142
+
id integer primary key autoincrement,
1143
+
did text not null,
1144
+
rkey text not null,
1145
+
1146
+
subject_at text not null,
1147
+
1148
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
1149
+
unique(did, rkey),
1150
+
unique(did, subject_at)
1151
+
);
1136
1152
1137
-
type migrationFn = func(*sql.Tx) error
1153
+
insert into stars_new (
1154
+
id,
1155
+
did,
1156
+
rkey,
1157
+
subject_at,
1158
+
created
1159
+
)
1160
+
select
1161
+
id,
1162
+
starred_by_did,
1163
+
rkey,
1164
+
repo_at,
1165
+
created
1166
+
from stars;
1138
1167
1139
-
func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
1140
-
logger = logger.With("migration", name)
1168
+
drop table stars;
1169
+
alter table stars_new rename to stars;
1141
1170
1142
-
tx, err := c.BeginTx(context.Background(), nil)
1143
-
if err != nil {
1171
+
create index if not exists idx_stars_created on stars(created);
1172
+
create index if not exists idx_stars_subject_at_created on stars(subject_at, created);
1173
+
`)
1144
1174
return err
1145
-
}
1146
-
defer tx.Rollback()
1175
+
})
1147
1176
1148
-
var exists bool
1149
-
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
1150
-
if err != nil {
1177
+
orm.RunMigration(conn, logger, "add-avatar-to-profile", func(tx *sql.Tx) error {
1178
+
_, err := tx.Exec(`
1179
+
alter table profile add column avatar text;
1180
+
`)
1151
1181
return err
1152
-
}
1182
+
})
1153
1183
1154
-
if !exists {
1155
-
// run migration
1156
-
err = migrationFn(tx)
1157
-
if err != nil {
1158
-
logger.Error("failed to run migration", "err", err)
1159
-
return err
1160
-
}
1161
-
1162
-
// mark migration as complete
1163
-
_, err = tx.Exec("insert into migrations (name) values (?)", name)
1164
-
if err != nil {
1165
-
logger.Error("failed to mark migration as complete", "err", err)
1166
-
return err
1167
-
}
1168
-
1169
-
// commit the transaction
1170
-
if err := tx.Commit(); err != nil {
1171
-
return err
1172
-
}
1173
-
1174
-
logger.Info("migration applied successfully")
1175
-
} else {
1176
-
logger.Warn("skipped migration, already applied")
1177
-
}
1178
-
1179
-
return nil
1184
+
return &DB{
1185
+
db,
1186
+
logger,
1187
+
}, nil
1180
1188
}
1181
1189
1182
1190
func (d *DB) Close() error {
1183
1191
return d.DB.Close()
1184
1192
}
1185
-
1186
-
type filter struct {
1187
-
key string
1188
-
arg any
1189
-
cmp string
1190
-
}
1191
-
1192
-
func newFilter(key, cmp string, arg any) filter {
1193
-
return filter{
1194
-
key: key,
1195
-
arg: arg,
1196
-
cmp: cmp,
1197
-
}
1198
-
}
1199
-
1200
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
1201
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
1202
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
1203
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
1204
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
1205
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
1206
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
1207
-
func FilterLike(key string, arg any) filter { return newFilter(key, "like", arg) }
1208
-
func FilterNotLike(key string, arg any) filter { return newFilter(key, "not like", arg) }
1209
-
func FilterContains(key string, arg any) filter {
1210
-
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
1211
-
}
1212
-
1213
-
func (f filter) Condition() string {
1214
-
rv := reflect.ValueOf(f.arg)
1215
-
kind := rv.Kind()
1216
-
1217
-
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
1218
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1219
-
if rv.Len() == 0 {
1220
-
// always false
1221
-
return "1 = 0"
1222
-
}
1223
-
1224
-
placeholders := make([]string, rv.Len())
1225
-
for i := range placeholders {
1226
-
placeholders[i] = "?"
1227
-
}
1228
-
1229
-
return fmt.Sprintf("%s %s (%s)", f.key, f.cmp, strings.Join(placeholders, ", "))
1230
-
}
1231
-
1232
-
return fmt.Sprintf("%s %s ?", f.key, f.cmp)
1233
-
}
1234
-
1235
-
func (f filter) Arg() []any {
1236
-
rv := reflect.ValueOf(f.arg)
1237
-
kind := rv.Kind()
1238
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1239
-
if rv.Len() == 0 {
1240
-
return nil
1241
-
}
1242
-
1243
-
out := make([]any, rv.Len())
1244
-
for i := range rv.Len() {
1245
-
out[i] = rv.Index(i).Interface()
1246
-
}
1247
-
return out
1248
-
}
1249
-
1250
-
return []any{f.arg}
1251
-
}
+4
-3
appview/db/follow.go
+4
-3
appview/db/follow.go
···
7
7
"time"
8
8
9
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
10
11
)
11
12
12
13
func AddFollow(e Execer, follow *models.Follow) error {
···
134
135
return result, nil
135
136
}
136
137
137
-
func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) {
138
+
func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) {
138
139
var follows []models.Follow
139
140
140
141
var conditions []string
···
191
192
}
192
193
193
194
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
194
-
return GetFollows(e, 0, FilterEq("subject_did", did))
195
+
return GetFollows(e, 0, orm.FilterEq("subject_did", did))
195
196
}
196
197
197
198
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
198
-
return GetFollows(e, 0, FilterEq("user_did", did))
199
+
return GetFollows(e, 0, orm.FilterEq("user_did", did))
199
200
}
200
201
201
202
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
+92
-36
appview/db/issues.go
+92
-36
appview/db/issues.go
···
10
10
"time"
11
11
12
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
+
"tangled.org/core/api/tangled"
13
14
"tangled.org/core/appview/models"
14
15
"tangled.org/core/appview/pagination"
16
+
"tangled.org/core/orm"
15
17
)
16
18
17
19
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
26
28
27
29
issues, err := GetIssues(
28
30
tx,
29
-
FilterEq("did", issue.Did),
30
-
FilterEq("rkey", issue.Rkey),
31
+
orm.FilterEq("did", issue.Did),
32
+
orm.FilterEq("rkey", issue.Rkey),
31
33
)
32
34
switch {
33
35
case err != nil:
···
69
71
returning rowid, issue_id
70
72
`, issue.RepoAt, issue.Did, issue.Rkey, newIssueId, issue.Title, issue.Body)
71
73
72
-
return row.Scan(&issue.Id, &issue.IssueId)
74
+
err = row.Scan(&issue.Id, &issue.IssueId)
75
+
if err != nil {
76
+
return fmt.Errorf("scan row: %w", err)
77
+
}
78
+
79
+
if err := putReferences(tx, issue.AtUri(), issue.References); err != nil {
80
+
return fmt.Errorf("put reference_links: %w", err)
81
+
}
82
+
return nil
73
83
}
74
84
75
85
func updateIssue(tx *sql.Tx, issue *models.Issue) error {
···
79
89
set title = ?, body = ?, edited = ?
80
90
where did = ? and rkey = ?
81
91
`, issue.Title, issue.Body, time.Now().Format(time.RFC3339), issue.Did, issue.Rkey)
82
-
return err
92
+
if err != nil {
93
+
return err
94
+
}
95
+
96
+
if err := putReferences(tx, issue.AtUri(), issue.References); err != nil {
97
+
return fmt.Errorf("put reference_links: %w", err)
98
+
}
99
+
return nil
83
100
}
84
101
85
-
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
102
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
86
103
issueMap := make(map[string]*models.Issue) // at-uri -> issue
87
104
88
105
var conditions []string
···
98
115
whereClause = " where " + strings.Join(conditions, " and ")
99
116
}
100
117
101
-
pLower := FilterGte("row_num", page.Offset+1)
102
-
pUpper := FilterLte("row_num", page.Offset+page.Limit)
118
+
pLower := orm.FilterGte("row_num", page.Offset+1)
119
+
pUpper := orm.FilterLte("row_num", page.Offset+page.Limit)
103
120
104
121
pageClause := ""
105
122
if page.Limit > 0 {
···
189
206
repoAts = append(repoAts, string(issue.RepoAt))
190
207
}
191
208
192
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts))
209
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
193
210
if err != nil {
194
211
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
195
212
}
···
212
229
// collect comments
213
230
issueAts := slices.Collect(maps.Keys(issueMap))
214
231
215
-
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
232
+
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
216
233
if err != nil {
217
234
return nil, fmt.Errorf("failed to query comments: %w", err)
218
235
}
···
224
241
}
225
242
226
243
// collect allLabels for each issue
227
-
allLabels, err := GetLabels(e, FilterIn("subject", issueAts))
244
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", issueAts))
228
245
if err != nil {
229
246
return nil, fmt.Errorf("failed to query labels: %w", err)
230
247
}
231
248
for issueAt, labels := range allLabels {
232
249
if issue, ok := issueMap[issueAt.String()]; ok {
233
250
issue.Labels = labels
251
+
}
252
+
}
253
+
254
+
// collect references for each issue
255
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", issueAts))
256
+
if err != nil {
257
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
258
+
}
259
+
for issueAt, references := range allReferencs {
260
+
if issue, ok := issueMap[issueAt.String()]; ok {
261
+
issue.References = references
234
262
}
235
263
}
236
264
···
250
278
issues, err := GetIssuesPaginated(
251
279
e,
252
280
pagination.Page{},
253
-
FilterEq("repo_at", repoAt),
254
-
FilterEq("issue_id", issueId),
281
+
orm.FilterEq("repo_at", repoAt),
282
+
orm.FilterEq("issue_id", issueId),
255
283
)
256
284
if err != nil {
257
285
return nil, err
···
263
291
return &issues[0], nil
264
292
}
265
293
266
-
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
294
+
func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) {
267
295
return GetIssuesPaginated(e, pagination.Page{}, filters...)
268
296
}
269
297
···
271
299
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
272
300
var ids []int64
273
301
274
-
var filters []filter
302
+
var filters []orm.Filter
275
303
openValue := 0
276
304
if opts.IsOpen {
277
305
openValue = 1
278
306
}
279
-
filters = append(filters, FilterEq("open", openValue))
307
+
filters = append(filters, orm.FilterEq("open", openValue))
280
308
if opts.RepoAt != "" {
281
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
282
310
}
283
311
284
312
var conditions []string
···
323
351
return ids, nil
324
352
}
325
353
326
-
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
327
-
result, err := e.Exec(
354
+
func AddIssueComment(tx *sql.Tx, c models.IssueComment) (int64, error) {
355
+
result, err := tx.Exec(
328
356
`insert into issue_comments (
329
357
did,
330
358
rkey,
···
363
391
return 0, err
364
392
}
365
393
394
+
if err := putReferences(tx, c.AtUri(), c.References); err != nil {
395
+
return 0, fmt.Errorf("put reference_links: %w", err)
396
+
}
397
+
366
398
return id, nil
367
399
}
368
400
369
-
func DeleteIssueComments(e Execer, filters ...filter) error {
401
+
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
370
402
var conditions []string
371
403
var args []any
372
404
for _, filter := range filters {
···
385
417
return err
386
418
}
387
419
388
-
func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) {
389
-
var comments []models.IssueComment
420
+
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
421
+
commentMap := make(map[string]*models.IssueComment)
390
422
391
423
var conditions []string
392
424
var args []any
···
465
497
comment.ReplyTo = &replyTo.V
466
498
}
467
499
468
-
comments = append(comments, comment)
500
+
atUri := comment.AtUri().String()
501
+
commentMap[atUri] = &comment
469
502
}
470
503
471
504
if err = rows.Err(); err != nil {
472
505
return nil, err
473
506
}
474
507
508
+
// collect references for each comments
509
+
commentAts := slices.Collect(maps.Keys(commentMap))
510
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
511
+
if err != nil {
512
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
513
+
}
514
+
for commentAt, references := range allReferencs {
515
+
if comment, ok := commentMap[commentAt.String()]; ok {
516
+
comment.References = references
517
+
}
518
+
}
519
+
520
+
var comments []models.IssueComment
521
+
for _, c := range commentMap {
522
+
comments = append(comments, *c)
523
+
}
524
+
525
+
sort.Slice(comments, func(i, j int) bool {
526
+
return comments[i].Created.After(comments[j].Created)
527
+
})
528
+
475
529
return comments, nil
476
530
}
477
531
478
-
func DeleteIssues(e Execer, filters ...filter) error {
479
-
var conditions []string
480
-
var args []any
481
-
for _, filter := range filters {
482
-
conditions = append(conditions, filter.Condition())
483
-
args = append(args, filter.Arg()...)
532
+
func DeleteIssues(tx *sql.Tx, did, rkey string) error {
533
+
_, err := tx.Exec(
534
+
`delete from issues
535
+
where did = ? and rkey = ?`,
536
+
did,
537
+
rkey,
538
+
)
539
+
if err != nil {
540
+
return fmt.Errorf("delete issue: %w", err)
484
541
}
485
542
486
-
whereClause := ""
487
-
if conditions != nil {
488
-
whereClause = " where " + strings.Join(conditions, " and ")
543
+
uri := syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", did, tangled.RepoIssueNSID, rkey))
544
+
err = deleteReferences(tx, uri)
545
+
if err != nil {
546
+
return fmt.Errorf("delete reference_links: %w", err)
489
547
}
490
548
491
-
query := fmt.Sprintf(`delete from issues %s`, whereClause)
492
-
_, err := e.Exec(query, args...)
493
-
return err
549
+
return nil
494
550
}
495
551
496
-
func CloseIssues(e Execer, filters ...filter) error {
552
+
func CloseIssues(e Execer, filters ...orm.Filter) error {
497
553
var conditions []string
498
554
var args []any
499
555
for _, filter := range filters {
···
511
567
return err
512
568
}
513
569
514
-
func ReopenIssues(e Execer, filters ...filter) error {
570
+
func ReopenIssues(e Execer, filters ...orm.Filter) error {
515
571
var conditions []string
516
572
var args []any
517
573
for _, filter := range filters {
+8
-7
appview/db/label.go
+8
-7
appview/db/label.go
···
10
10
11
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
12
"tangled.org/core/appview/models"
13
+
"tangled.org/core/orm"
13
14
)
14
15
15
16
// no updating type for now
···
59
60
return id, nil
60
61
}
61
62
62
-
func DeleteLabelDefinition(e Execer, filters ...filter) error {
63
+
func DeleteLabelDefinition(e Execer, filters ...orm.Filter) error {
63
64
var conditions []string
64
65
var args []any
65
66
for _, filter := range filters {
···
75
76
return err
76
77
}
77
78
78
-
func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
79
+
func GetLabelDefinitions(e Execer, filters ...orm.Filter) ([]models.LabelDefinition, error) {
79
80
var labelDefinitions []models.LabelDefinition
80
81
var conditions []string
81
82
var args []any
···
167
168
}
168
169
169
170
// helper to get exactly one label def
170
-
func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
171
+
func GetLabelDefinition(e Execer, filters ...orm.Filter) (*models.LabelDefinition, error) {
171
172
labels, err := GetLabelDefinitions(e, filters...)
172
173
if err != nil {
173
174
return nil, err
···
227
228
return id, nil
228
229
}
229
230
230
-
func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
231
+
func GetLabelOps(e Execer, filters ...orm.Filter) ([]models.LabelOp, error) {
231
232
var labelOps []models.LabelOp
232
233
var conditions []string
233
234
var args []any
···
302
303
}
303
304
304
305
// get labels for a given list of subject URIs
305
-
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
306
+
func GetLabels(e Execer, filters ...orm.Filter) (map[syntax.ATURI]models.LabelState, error) {
306
307
ops, err := GetLabelOps(e, filters...)
307
308
if err != nil {
308
309
return nil, err
···
322
323
}
323
324
labelAts := slices.Collect(maps.Keys(labelAtSet))
324
325
325
-
actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
326
+
actx, err := NewLabelApplicationCtx(e, orm.FilterIn("at_uri", labelAts))
326
327
if err != nil {
327
328
return nil, err
328
329
}
···
338
339
return results, nil
339
340
}
340
341
341
-
func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
342
+
func NewLabelApplicationCtx(e Execer, filters ...orm.Filter) (*models.LabelApplicationCtx, error) {
342
343
labels, err := GetLabelDefinitions(e, filters...)
343
344
if err != nil {
344
345
return nil, err
+5
-4
appview/db/language.go
+5
-4
appview/db/language.go
···
7
7
8
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
10
11
)
11
12
12
-
func GetRepoLanguages(e Execer, filters ...filter) ([]models.RepoLanguage, error) {
13
+
func GetRepoLanguages(e Execer, filters ...orm.Filter) ([]models.RepoLanguage, error) {
13
14
var conditions []string
14
15
var args []any
15
16
for _, filter := range filters {
···
85
86
return nil
86
87
}
87
88
88
-
func DeleteRepoLanguages(e Execer, filters ...filter) error {
89
+
func DeleteRepoLanguages(e Execer, filters ...orm.Filter) error {
89
90
var conditions []string
90
91
var args []any
91
92
for _, filter := range filters {
···
107
108
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
108
109
err := DeleteRepoLanguages(
109
110
tx,
110
-
FilterEq("repo_at", repoAt),
111
-
FilterEq("ref", ref),
111
+
orm.FilterEq("repo_at", repoAt),
112
+
orm.FilterEq("ref", ref),
112
113
)
113
114
if err != nil {
114
115
return fmt.Errorf("failed to delete existing languages: %w", err)
+14
-13
appview/db/notifications.go
+14
-13
appview/db/notifications.go
···
11
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
12
"tangled.org/core/appview/models"
13
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
14
15
)
15
16
16
17
func CreateNotification(e Execer, notification *models.Notification) error {
···
44
45
}
45
46
46
47
// GetNotificationsPaginated retrieves notifications with filters and pagination
47
-
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) {
48
+
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Notification, error) {
48
49
var conditions []string
49
50
var args []any
50
51
···
113
114
}
114
115
115
116
// GetNotificationsWithEntities retrieves notifications with their related entities
116
-
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) {
117
+
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) {
117
118
var conditions []string
118
119
var args []any
119
120
···
256
257
}
257
258
258
259
// GetNotifications retrieves notifications with filters
259
-
func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) {
260
+
func GetNotifications(e Execer, filters ...orm.Filter) ([]*models.Notification, error) {
260
261
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
261
262
}
262
263
263
-
func CountNotifications(e Execer, filters ...filter) (int64, error) {
264
+
func CountNotifications(e Execer, filters ...orm.Filter) (int64, error) {
264
265
var conditions []string
265
266
var args []any
266
267
for _, filter := range filters {
···
285
286
}
286
287
287
288
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
288
-
idFilter := FilterEq("id", notificationID)
289
-
recipientFilter := FilterEq("recipient_did", userDID)
289
+
idFilter := orm.FilterEq("id", notificationID)
290
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
290
291
291
292
query := fmt.Sprintf(`
292
293
UPDATE notifications
···
314
315
}
315
316
316
317
func MarkAllNotificationsRead(e Execer, userDID string) error {
317
-
recipientFilter := FilterEq("recipient_did", userDID)
318
-
readFilter := FilterEq("read", 0)
318
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
319
+
readFilter := orm.FilterEq("read", 0)
319
320
320
321
query := fmt.Sprintf(`
321
322
UPDATE notifications
···
334
335
}
335
336
336
337
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
337
-
idFilter := FilterEq("id", notificationID)
338
-
recipientFilter := FilterEq("recipient_did", userDID)
338
+
idFilter := orm.FilterEq("id", notificationID)
339
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
339
340
340
341
query := fmt.Sprintf(`
341
342
DELETE FROM notifications
···
362
363
}
363
364
364
365
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
365
-
prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid))
366
+
prefs, err := GetNotificationPreferences(e, orm.FilterEq("user_did", userDid))
366
367
if err != nil {
367
368
return nil, err
368
369
}
···
375
376
return p, nil
376
377
}
377
378
378
-
func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) {
379
+
func GetNotificationPreferences(e Execer, filters ...orm.Filter) (map[syntax.DID]*models.NotificationPreferences, error) {
379
380
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
380
381
381
382
var conditions []string
···
483
484
484
485
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
485
486
cutoff := time.Now().Add(-olderThan)
486
-
createdFilter := FilterLte("created", cutoff)
487
+
createdFilter := orm.FilterLte("created", cutoff)
487
488
488
489
query := fmt.Sprintf(`
489
490
DELETE FROM notifications
+6
-5
appview/db/pipeline.go
+6
-5
appview/db/pipeline.go
···
7
7
"time"
8
8
9
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
10
11
)
11
12
12
-
func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) {
13
+
func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) {
13
14
var pipelines []models.Pipeline
14
15
15
16
var conditions []string
···
168
169
169
170
// this is a mega query, but the most useful one:
170
171
// get N pipelines, for each one get the latest status of its N workflows
171
-
func GetPipelineStatuses(e Execer, limit int, filters ...filter) ([]models.Pipeline, error) {
172
+
func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
172
173
var conditions []string
173
174
var args []any
174
175
for _, filter := range filters {
175
-
filter.key = "p." + filter.key // the table is aliased in the query to `p`
176
+
filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
176
177
conditions = append(conditions, filter.Condition())
177
178
args = append(args, filter.Arg()...)
178
179
}
···
264
265
conditions = nil
265
266
args = nil
266
267
for _, p := range pipelines {
267
-
knotFilter := FilterEq("pipeline_knot", p.Knot)
268
-
rkeyFilter := FilterEq("pipeline_rkey", p.Rkey)
268
+
knotFilter := orm.FilterEq("pipeline_knot", p.Knot)
269
+
rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey)
269
270
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
270
271
args = append(args, p.Knot)
271
272
args = append(args, p.Rkey)
+16
-8
appview/db/profile.go
+16
-8
appview/db/profile.go
···
11
11
12
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
14
15
)
15
16
16
17
const TimeframeMonths = 7
···
44
45
45
46
issues, err := GetIssues(
46
47
e,
47
-
FilterEq("did", forDid),
48
-
FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
48
+
orm.FilterEq("did", forDid),
49
+
orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
49
50
)
50
51
if err != nil {
51
52
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
65
66
*items = append(*items, &issue)
66
67
}
67
68
68
-
repos, err := GetRepos(e, 0, FilterEq("did", forDid))
69
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid))
69
70
if err != nil {
70
71
return nil, fmt.Errorf("error getting all repos by did: %w", err)
71
72
}
···
127
128
_, err = tx.Exec(
128
129
`insert or replace into profile (
129
130
did,
131
+
avatar,
130
132
description,
131
133
include_bluesky,
132
134
location,
133
135
pronouns
134
136
)
135
-
values (?, ?, ?, ?, ?)`,
137
+
values (?, ?, ?, ?, ?, ?)`,
136
138
profile.Did,
139
+
profile.Avatar,
137
140
profile.Description,
138
141
includeBskyValue,
139
142
profile.Location,
···
199
202
return tx.Commit()
200
203
}
201
204
202
-
func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) {
205
+
func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) {
203
206
var conditions []string
204
207
var args []any
205
208
for _, filter := range filters {
···
311
314
func GetProfile(e Execer, did string) (*models.Profile, error) {
312
315
var profile models.Profile
313
316
var pronouns sql.Null[string]
317
+
var avatar sql.Null[string]
314
318
315
319
profile.Did = did
316
320
317
321
includeBluesky := 0
318
322
319
323
err := e.QueryRow(
320
-
`select description, include_bluesky, location, pronouns from profile where did = ?`,
324
+
`select avatar, description, include_bluesky, location, pronouns from profile where did = ?`,
321
325
did,
322
-
).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns)
326
+
).Scan(&avatar, &profile.Description, &includeBluesky, &profile.Location, &pronouns)
323
327
if err == sql.ErrNoRows {
324
328
profile := models.Profile{}
325
329
profile.Did = did
···
338
342
profile.Pronouns = pronouns.V
339
343
}
340
344
345
+
if avatar.Valid {
346
+
profile.Avatar = avatar.V
347
+
}
348
+
341
349
rows, err := e.Query(`select link from profile_links where did = ?`, did)
342
350
if err != nil {
343
351
return nil, err
···
441
449
}
442
450
443
451
// ensure all pinned repos are either own repos or collaborating repos
444
-
repos, err := GetRepos(e, 0, FilterEq("did", profile.Did))
452
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did))
445
453
if err != nil {
446
454
log.Printf("getting repos for %s: %s", profile.Did, err)
447
455
}
+69
-24
appview/db/pulls.go
+69
-24
appview/db/pulls.go
···
13
13
14
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
15
"tangled.org/core/appview/models"
16
+
"tangled.org/core/orm"
16
17
)
17
18
18
19
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
93
94
insert into pull_submissions (pull_at, round_number, patch, combined, source_rev)
94
95
values (?, ?, ?, ?, ?)
95
96
`, pull.AtUri(), 0, pull.Submissions[0].Patch, pull.Submissions[0].Combined, pull.Submissions[0].SourceRev)
96
-
return err
97
+
if err != nil {
98
+
return err
99
+
}
100
+
101
+
if err := putReferences(tx, pull.AtUri(), pull.References); err != nil {
102
+
return fmt.Errorf("put reference_links: %w", err)
103
+
}
104
+
105
+
return nil
97
106
}
98
107
99
108
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (syntax.ATURI, error) {
···
110
119
return pullId - 1, err
111
120
}
112
121
113
-
func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) {
122
+
func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) {
114
123
pulls := make(map[syntax.ATURI]*models.Pull)
115
124
116
125
var conditions []string
···
221
230
for _, p := range pulls {
222
231
pullAts = append(pullAts, p.AtUri())
223
232
}
224
-
submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts))
233
+
submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts))
225
234
if err != nil {
226
235
return nil, fmt.Errorf("failed to get submissions: %w", err)
227
236
}
···
233
242
}
234
243
235
244
// collect allLabels for each issue
236
-
allLabels, err := GetLabels(e, FilterIn("subject", pullAts))
245
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts))
237
246
if err != nil {
238
247
return nil, fmt.Errorf("failed to query labels: %w", err)
239
248
}
···
250
259
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
251
260
}
252
261
}
253
-
sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts))
262
+
sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts))
254
263
if err != nil && !errors.Is(err, sql.ErrNoRows) {
255
264
return nil, fmt.Errorf("failed to get source repos: %w", err)
256
265
}
···
266
275
}
267
276
}
268
277
278
+
allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts))
279
+
if err != nil {
280
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
281
+
}
282
+
for pullAt, references := range allReferences {
283
+
if pull, ok := pulls[pullAt]; ok {
284
+
pull.References = references
285
+
}
286
+
}
287
+
269
288
orderedByPullId := []*models.Pull{}
270
289
for _, p := range pulls {
271
290
orderedByPullId = append(orderedByPullId, p)
···
277
296
return orderedByPullId, nil
278
297
}
279
298
280
-
func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) {
299
+
func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) {
281
300
return GetPullsWithLimit(e, 0, filters...)
282
301
}
283
302
284
303
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
285
304
var ids []int64
286
305
287
-
var filters []filter
288
-
filters = append(filters, FilterEq("state", opts.State))
306
+
var filters []orm.Filter
307
+
filters = append(filters, orm.FilterEq("state", opts.State))
289
308
if opts.RepoAt != "" {
290
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
291
310
}
292
311
293
312
var conditions []string
···
343
362
}
344
363
345
364
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
346
-
pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
365
+
pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId))
347
366
if err != nil {
348
367
return nil, err
349
368
}
···
355
374
}
356
375
357
376
// mapping from pull -> pull submissions
358
-
func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
377
+
func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
359
378
var conditions []string
360
379
var args []any
361
380
for _, filter := range filters {
···
430
449
431
450
// Get comments for all submissions using GetPullComments
432
451
submissionIds := slices.Collect(maps.Keys(submissionMap))
433
-
comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds))
452
+
comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
434
453
if err != nil {
435
-
return nil, err
454
+
return nil, fmt.Errorf("failed to get pull comments: %w", err)
436
455
}
437
456
for _, comment := range comments {
438
457
if submission, ok := submissionMap[comment.SubmissionId]; ok {
···
456
475
return m, nil
457
476
}
458
477
459
-
func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) {
478
+
func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
460
479
var conditions []string
461
480
var args []any
462
481
for _, filter := range filters {
···
492
511
}
493
512
defer rows.Close()
494
513
495
-
var comments []models.PullComment
514
+
commentMap := make(map[string]*models.PullComment)
496
515
for rows.Next() {
497
516
var comment models.PullComment
498
517
var createdAt string
···
514
533
comment.Created = t
515
534
}
516
535
517
-
comments = append(comments, comment)
536
+
atUri := comment.AtUri().String()
537
+
commentMap[atUri] = &comment
518
538
}
519
539
520
540
if err := rows.Err(); err != nil {
521
541
return nil, err
522
542
}
523
543
544
+
// collect references for each comments
545
+
commentAts := slices.Collect(maps.Keys(commentMap))
546
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
547
+
if err != nil {
548
+
return nil, fmt.Errorf("failed to query reference_links: %w", err)
549
+
}
550
+
for commentAt, references := range allReferencs {
551
+
if comment, ok := commentMap[commentAt.String()]; ok {
552
+
comment.References = references
553
+
}
554
+
}
555
+
556
+
var comments []models.PullComment
557
+
for _, c := range commentMap {
558
+
comments = append(comments, *c)
559
+
}
560
+
561
+
sort.Slice(comments, func(i, j int) bool {
562
+
return comments[i].Created.Before(comments[j].Created)
563
+
})
564
+
524
565
return comments, nil
525
566
}
526
567
···
600
641
return pulls, nil
601
642
}
602
643
603
-
func NewPullComment(e Execer, comment *models.PullComment) (int64, error) {
644
+
func NewPullComment(tx *sql.Tx, comment *models.PullComment) (int64, error) {
604
645
query := `insert into pull_comments (owner_did, repo_at, submission_id, comment_at, pull_id, body) values (?, ?, ?, ?, ?, ?)`
605
-
res, err := e.Exec(
646
+
res, err := tx.Exec(
606
647
query,
607
648
comment.OwnerDid,
608
649
comment.RepoAt,
···
618
659
i, err := res.LastInsertId()
619
660
if err != nil {
620
661
return 0, err
662
+
}
663
+
664
+
if err := putReferences(tx, comment.AtUri(), comment.References); err != nil {
665
+
return 0, fmt.Errorf("put reference_links: %w", err)
621
666
}
622
667
623
668
return i, nil
···
664
709
return err
665
710
}
666
711
667
-
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
712
+
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error {
668
713
var conditions []string
669
714
var args []any
670
715
···
688
733
689
734
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
690
735
// otherwise submissions are immutable
691
-
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
736
+
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error {
692
737
var conditions []string
693
738
var args []any
694
739
···
746
791
func GetStack(e Execer, stackId string) (models.Stack, error) {
747
792
unorderedPulls, err := GetPulls(
748
793
e,
749
-
FilterEq("stack_id", stackId),
750
-
FilterNotEq("state", models.PullDeleted),
794
+
orm.FilterEq("stack_id", stackId),
795
+
orm.FilterNotEq("state", models.PullDeleted),
751
796
)
752
797
if err != nil {
753
798
return nil, err
···
791
836
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
792
837
pulls, err := GetPulls(
793
838
e,
794
-
FilterEq("stack_id", stackId),
795
-
FilterEq("state", models.PullDeleted),
839
+
orm.FilterEq("stack_id", stackId),
840
+
orm.FilterEq("state", models.PullDeleted),
796
841
)
797
842
if err != nil {
798
843
return nil, err
+2
-1
appview/db/punchcard.go
+2
-1
appview/db/punchcard.go
···
7
7
"time"
8
8
9
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
10
11
)
11
12
12
13
// this adds to the existing count
···
20
21
return err
21
22
}
22
23
23
-
func MakePunchcard(e Execer, filters ...filter) (*models.Punchcard, error) {
24
+
func MakePunchcard(e Execer, filters ...orm.Filter) (*models.Punchcard, error) {
24
25
punchcard := &models.Punchcard{}
25
26
now := time.Now()
26
27
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+463
appview/db/reference.go
+463
appview/db/reference.go
···
1
+
package db
2
+
3
+
import (
4
+
"database/sql"
5
+
"fmt"
6
+
"strings"
7
+
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
"tangled.org/core/api/tangled"
10
+
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
+
)
13
+
14
+
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
15
+
// It will ignore missing refLinks.
16
+
func ValidateReferenceLinks(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
17
+
var (
18
+
issueRefs []models.ReferenceLink
19
+
pullRefs []models.ReferenceLink
20
+
)
21
+
for _, ref := range refLinks {
22
+
switch ref.Kind {
23
+
case models.RefKindIssue:
24
+
issueRefs = append(issueRefs, ref)
25
+
case models.RefKindPull:
26
+
pullRefs = append(pullRefs, ref)
27
+
}
28
+
}
29
+
issueUris, err := findIssueReferences(e, issueRefs)
30
+
if err != nil {
31
+
return nil, fmt.Errorf("find issue references: %w", err)
32
+
}
33
+
pullUris, err := findPullReferences(e, pullRefs)
34
+
if err != nil {
35
+
return nil, fmt.Errorf("find pull references: %w", err)
36
+
}
37
+
38
+
return append(issueUris, pullUris...), nil
39
+
}
40
+
41
+
func findIssueReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
42
+
if len(refLinks) == 0 {
43
+
return nil, nil
44
+
}
45
+
vals := make([]string, len(refLinks))
46
+
args := make([]any, 0, len(refLinks)*4)
47
+
for i, ref := range refLinks {
48
+
vals[i] = "(?, ?, ?, ?)"
49
+
args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
50
+
}
51
+
query := fmt.Sprintf(
52
+
`with input(owner_did, name, issue_id, comment_id) as (
53
+
values %s
54
+
)
55
+
select
56
+
i.did, i.rkey,
57
+
c.did, c.rkey
58
+
from input inp
59
+
join repos r
60
+
on r.did = inp.owner_did
61
+
and r.name = inp.name
62
+
join issues i
63
+
on i.repo_at = r.at_uri
64
+
and i.issue_id = inp.issue_id
65
+
left join issue_comments c
66
+
on inp.comment_id is not null
67
+
and c.issue_at = i.at_uri
68
+
and c.id = inp.comment_id
69
+
`,
70
+
strings.Join(vals, ","),
71
+
)
72
+
rows, err := e.Query(query, args...)
73
+
if err != nil {
74
+
return nil, err
75
+
}
76
+
defer rows.Close()
77
+
78
+
var uris []syntax.ATURI
79
+
80
+
for rows.Next() {
81
+
// Scan rows
82
+
var issueOwner, issueRkey string
83
+
var commentOwner, commentRkey sql.NullString
84
+
var uri syntax.ATURI
85
+
if err := rows.Scan(&issueOwner, &issueRkey, &commentOwner, &commentRkey); err != nil {
86
+
return nil, err
87
+
}
88
+
if commentOwner.Valid && commentRkey.Valid {
89
+
uri = syntax.ATURI(fmt.Sprintf(
90
+
"at://%s/%s/%s",
91
+
commentOwner.String,
92
+
tangled.RepoIssueCommentNSID,
93
+
commentRkey.String,
94
+
))
95
+
} else {
96
+
uri = syntax.ATURI(fmt.Sprintf(
97
+
"at://%s/%s/%s",
98
+
issueOwner,
99
+
tangled.RepoIssueNSID,
100
+
issueRkey,
101
+
))
102
+
}
103
+
uris = append(uris, uri)
104
+
}
105
+
if err := rows.Err(); err != nil {
106
+
return nil, fmt.Errorf("iterate rows: %w", err)
107
+
}
108
+
109
+
return uris, nil
110
+
}
111
+
112
+
func findPullReferences(e Execer, refLinks []models.ReferenceLink) ([]syntax.ATURI, error) {
113
+
if len(refLinks) == 0 {
114
+
return nil, nil
115
+
}
116
+
vals := make([]string, len(refLinks))
117
+
args := make([]any, 0, len(refLinks)*4)
118
+
for i, ref := range refLinks {
119
+
vals[i] = "(?, ?, ?, ?)"
120
+
args = append(args, ref.Handle, ref.Repo, ref.SubjectId, ref.CommentId)
121
+
}
122
+
query := fmt.Sprintf(
123
+
`with input(owner_did, name, pull_id, comment_id) as (
124
+
values %s
125
+
)
126
+
select
127
+
p.owner_did, p.rkey,
128
+
c.comment_at
129
+
from input inp
130
+
join repos r
131
+
on r.did = inp.owner_did
132
+
and r.name = inp.name
133
+
join pulls p
134
+
on p.repo_at = r.at_uri
135
+
and p.pull_id = inp.pull_id
136
+
left join pull_comments c
137
+
on inp.comment_id is not null
138
+
and c.repo_at = r.at_uri and c.pull_id = p.pull_id
139
+
and c.id = inp.comment_id
140
+
`,
141
+
strings.Join(vals, ","),
142
+
)
143
+
rows, err := e.Query(query, args...)
144
+
if err != nil {
145
+
return nil, err
146
+
}
147
+
defer rows.Close()
148
+
149
+
var uris []syntax.ATURI
150
+
151
+
for rows.Next() {
152
+
// Scan rows
153
+
var pullOwner, pullRkey string
154
+
var commentUri sql.NullString
155
+
var uri syntax.ATURI
156
+
if err := rows.Scan(&pullOwner, &pullRkey, &commentUri); err != nil {
157
+
return nil, err
158
+
}
159
+
if commentUri.Valid {
160
+
// no-op
161
+
uri = syntax.ATURI(commentUri.String)
162
+
} else {
163
+
uri = syntax.ATURI(fmt.Sprintf(
164
+
"at://%s/%s/%s",
165
+
pullOwner,
166
+
tangled.RepoPullNSID,
167
+
pullRkey,
168
+
))
169
+
}
170
+
uris = append(uris, uri)
171
+
}
172
+
return uris, nil
173
+
}
174
+
175
+
func putReferences(tx *sql.Tx, fromAt syntax.ATURI, references []syntax.ATURI) error {
176
+
err := deleteReferences(tx, fromAt)
177
+
if err != nil {
178
+
return fmt.Errorf("delete old reference_links: %w", err)
179
+
}
180
+
if len(references) == 0 {
181
+
return nil
182
+
}
183
+
184
+
values := make([]string, 0, len(references))
185
+
args := make([]any, 0, len(references)*2)
186
+
for _, ref := range references {
187
+
values = append(values, "(?, ?)")
188
+
args = append(args, fromAt, ref)
189
+
}
190
+
_, err = tx.Exec(
191
+
fmt.Sprintf(
192
+
`insert into reference_links (from_at, to_at)
193
+
values %s`,
194
+
strings.Join(values, ","),
195
+
),
196
+
args...,
197
+
)
198
+
if err != nil {
199
+
return fmt.Errorf("insert new reference_links: %w", err)
200
+
}
201
+
return nil
202
+
}
203
+
204
+
func deleteReferences(tx *sql.Tx, fromAt syntax.ATURI) error {
205
+
_, err := tx.Exec(`delete from reference_links where from_at = ?`, fromAt)
206
+
return err
207
+
}
208
+
209
+
func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) {
210
+
var (
211
+
conditions []string
212
+
args []any
213
+
)
214
+
for _, filter := range filters {
215
+
conditions = append(conditions, filter.Condition())
216
+
args = append(args, filter.Arg()...)
217
+
}
218
+
219
+
whereClause := ""
220
+
if conditions != nil {
221
+
whereClause = " where " + strings.Join(conditions, " and ")
222
+
}
223
+
224
+
rows, err := e.Query(
225
+
fmt.Sprintf(
226
+
`select from_at, to_at from reference_links %s`,
227
+
whereClause,
228
+
),
229
+
args...,
230
+
)
231
+
if err != nil {
232
+
return nil, fmt.Errorf("query reference_links: %w", err)
233
+
}
234
+
defer rows.Close()
235
+
236
+
result := make(map[syntax.ATURI][]syntax.ATURI)
237
+
238
+
for rows.Next() {
239
+
var from, to syntax.ATURI
240
+
if err := rows.Scan(&from, &to); err != nil {
241
+
return nil, fmt.Errorf("scan row: %w", err)
242
+
}
243
+
244
+
result[from] = append(result[from], to)
245
+
}
246
+
if err := rows.Err(); err != nil {
247
+
return nil, fmt.Errorf("iterate rows: %w", err)
248
+
}
249
+
250
+
return result, nil
251
+
}
252
+
253
+
func GetBacklinks(e Execer, target syntax.ATURI) ([]models.RichReferenceLink, error) {
254
+
rows, err := e.Query(
255
+
`select from_at from reference_links
256
+
where to_at = ?`,
257
+
target,
258
+
)
259
+
if err != nil {
260
+
return nil, fmt.Errorf("query backlinks: %w", err)
261
+
}
262
+
defer rows.Close()
263
+
264
+
var (
265
+
backlinks []models.RichReferenceLink
266
+
backlinksMap = make(map[string][]syntax.ATURI)
267
+
)
268
+
for rows.Next() {
269
+
var from syntax.ATURI
270
+
if err := rows.Scan(&from); err != nil {
271
+
return nil, fmt.Errorf("scan row: %w", err)
272
+
}
273
+
nsid := from.Collection().String()
274
+
backlinksMap[nsid] = append(backlinksMap[nsid], from)
275
+
}
276
+
if err := rows.Err(); err != nil {
277
+
return nil, fmt.Errorf("iterate rows: %w", err)
278
+
}
279
+
280
+
var ls []models.RichReferenceLink
281
+
ls, err = getIssueBacklinks(e, backlinksMap[tangled.RepoIssueNSID])
282
+
if err != nil {
283
+
return nil, fmt.Errorf("get issue backlinks: %w", err)
284
+
}
285
+
backlinks = append(backlinks, ls...)
286
+
ls, err = getIssueCommentBacklinks(e, backlinksMap[tangled.RepoIssueCommentNSID])
287
+
if err != nil {
288
+
return nil, fmt.Errorf("get issue_comment backlinks: %w", err)
289
+
}
290
+
backlinks = append(backlinks, ls...)
291
+
ls, err = getPullBacklinks(e, backlinksMap[tangled.RepoPullNSID])
292
+
if err != nil {
293
+
return nil, fmt.Errorf("get pull backlinks: %w", err)
294
+
}
295
+
backlinks = append(backlinks, ls...)
296
+
ls, err = getPullCommentBacklinks(e, backlinksMap[tangled.RepoPullCommentNSID])
297
+
if err != nil {
298
+
return nil, fmt.Errorf("get pull_comment backlinks: %w", err)
299
+
}
300
+
backlinks = append(backlinks, ls...)
301
+
302
+
return backlinks, nil
303
+
}
304
+
305
+
func getIssueBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
306
+
if len(aturis) == 0 {
307
+
return nil, nil
308
+
}
309
+
vals := make([]string, len(aturis))
310
+
args := make([]any, 0, len(aturis)*2)
311
+
for i, aturi := range aturis {
312
+
vals[i] = "(?, ?)"
313
+
did := aturi.Authority().String()
314
+
rkey := aturi.RecordKey().String()
315
+
args = append(args, did, rkey)
316
+
}
317
+
rows, err := e.Query(
318
+
fmt.Sprintf(
319
+
`select r.did, r.name, i.issue_id, i.title, i.open
320
+
from issues i
321
+
join repos r
322
+
on r.at_uri = i.repo_at
323
+
where (i.did, i.rkey) in (%s)`,
324
+
strings.Join(vals, ","),
325
+
),
326
+
args...,
327
+
)
328
+
if err != nil {
329
+
return nil, err
330
+
}
331
+
defer rows.Close()
332
+
var refLinks []models.RichReferenceLink
333
+
for rows.Next() {
334
+
var l models.RichReferenceLink
335
+
l.Kind = models.RefKindIssue
336
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
337
+
return nil, err
338
+
}
339
+
refLinks = append(refLinks, l)
340
+
}
341
+
if err := rows.Err(); err != nil {
342
+
return nil, fmt.Errorf("iterate rows: %w", err)
343
+
}
344
+
return refLinks, nil
345
+
}
346
+
347
+
func getIssueCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
348
+
if len(aturis) == 0 {
349
+
return nil, nil
350
+
}
351
+
filter := orm.FilterIn("c.at_uri", aturis)
352
+
rows, err := e.Query(
353
+
fmt.Sprintf(
354
+
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
355
+
from issue_comments c
356
+
join issues i
357
+
on i.at_uri = c.issue_at
358
+
join repos r
359
+
on r.at_uri = i.repo_at
360
+
where %s`,
361
+
filter.Condition(),
362
+
),
363
+
filter.Arg()...,
364
+
)
365
+
if err != nil {
366
+
return nil, err
367
+
}
368
+
defer rows.Close()
369
+
var refLinks []models.RichReferenceLink
370
+
for rows.Next() {
371
+
var l models.RichReferenceLink
372
+
l.Kind = models.RefKindIssue
373
+
l.CommentId = new(int)
374
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
375
+
return nil, err
376
+
}
377
+
refLinks = append(refLinks, l)
378
+
}
379
+
if err := rows.Err(); err != nil {
380
+
return nil, fmt.Errorf("iterate rows: %w", err)
381
+
}
382
+
return refLinks, nil
383
+
}
384
+
385
+
func getPullBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
386
+
if len(aturis) == 0 {
387
+
return nil, nil
388
+
}
389
+
vals := make([]string, len(aturis))
390
+
args := make([]any, 0, len(aturis)*2)
391
+
for i, aturi := range aturis {
392
+
vals[i] = "(?, ?)"
393
+
did := aturi.Authority().String()
394
+
rkey := aturi.RecordKey().String()
395
+
args = append(args, did, rkey)
396
+
}
397
+
rows, err := e.Query(
398
+
fmt.Sprintf(
399
+
`select r.did, r.name, p.pull_id, p.title, p.state
400
+
from pulls p
401
+
join repos r
402
+
on r.at_uri = p.repo_at
403
+
where (p.owner_did, p.rkey) in (%s)`,
404
+
strings.Join(vals, ","),
405
+
),
406
+
args...,
407
+
)
408
+
if err != nil {
409
+
return nil, err
410
+
}
411
+
defer rows.Close()
412
+
var refLinks []models.RichReferenceLink
413
+
for rows.Next() {
414
+
var l models.RichReferenceLink
415
+
l.Kind = models.RefKindPull
416
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, &l.Title, &l.State); err != nil {
417
+
return nil, err
418
+
}
419
+
refLinks = append(refLinks, l)
420
+
}
421
+
if err := rows.Err(); err != nil {
422
+
return nil, fmt.Errorf("iterate rows: %w", err)
423
+
}
424
+
return refLinks, nil
425
+
}
426
+
427
+
func getPullCommentBacklinks(e Execer, aturis []syntax.ATURI) ([]models.RichReferenceLink, error) {
428
+
if len(aturis) == 0 {
429
+
return nil, nil
430
+
}
431
+
filter := orm.FilterIn("c.comment_at", aturis)
432
+
rows, err := e.Query(
433
+
fmt.Sprintf(
434
+
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
435
+
from repos r
436
+
join pulls p
437
+
on r.at_uri = p.repo_at
438
+
join pull_comments c
439
+
on r.at_uri = c.repo_at and p.pull_id = c.pull_id
440
+
where %s`,
441
+
filter.Condition(),
442
+
),
443
+
filter.Arg()...,
444
+
)
445
+
if err != nil {
446
+
return nil, err
447
+
}
448
+
defer rows.Close()
449
+
var refLinks []models.RichReferenceLink
450
+
for rows.Next() {
451
+
var l models.RichReferenceLink
452
+
l.Kind = models.RefKindPull
453
+
l.CommentId = new(int)
454
+
if err := rows.Scan(&l.Handle, &l.Repo, &l.SubjectId, l.CommentId, &l.Title, &l.State); err != nil {
455
+
return nil, err
456
+
}
457
+
refLinks = append(refLinks, l)
458
+
}
459
+
if err := rows.Err(); err != nil {
460
+
return nil, fmt.Errorf("iterate rows: %w", err)
461
+
}
462
+
return refLinks, nil
463
+
}
+4
-3
appview/db/registration.go
+4
-3
appview/db/registration.go
···
7
7
"time"
8
8
9
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
10
11
)
11
12
12
-
func GetRegistrations(e Execer, filters ...filter) ([]models.Registration, error) {
13
+
func GetRegistrations(e Execer, filters ...orm.Filter) ([]models.Registration, error) {
13
14
var registrations []models.Registration
14
15
15
16
var conditions []string
···
69
70
return registrations, nil
70
71
}
71
72
72
-
func MarkRegistered(e Execer, filters ...filter) error {
73
+
func MarkRegistered(e Execer, filters ...orm.Filter) error {
73
74
var conditions []string
74
75
var args []any
75
76
for _, filter := range filters {
···
94
95
return err
95
96
}
96
97
97
-
func DeleteKnot(e Execer, filters ...filter) error {
98
+
func DeleteKnot(e Execer, filters ...orm.Filter) error {
98
99
var conditions []string
99
100
var args []any
100
101
for _, filter := range filters {
+20
-36
appview/db/repos.go
+20
-36
appview/db/repos.go
···
10
10
"time"
11
11
12
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
-
securejoin "github.com/cyphar/filepath-securejoin"
14
-
"tangled.org/core/api/tangled"
15
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
16
15
)
17
16
18
-
type Repo struct {
19
-
Id int64
20
-
Did string
21
-
Name string
22
-
Knot string
23
-
Rkey string
24
-
Created time.Time
25
-
Description string
26
-
Spindle string
27
-
28
-
// optionally, populate this when querying for reverse mappings
29
-
RepoStats *models.RepoStats
30
-
31
-
// optional
32
-
Source string
33
-
}
34
-
35
-
func (r Repo) RepoAt() syntax.ATURI {
36
-
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.RepoNSID, r.Rkey))
37
-
}
38
-
39
-
func (r Repo) DidSlashRepo() string {
40
-
p, _ := securejoin.SecureJoin(r.Did, r.Name)
41
-
return p
42
-
}
43
-
44
-
func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) {
17
+
func GetRepos(e Execer, limit int, filters ...orm.Filter) ([]models.Repo, error) {
45
18
repoMap := make(map[syntax.ATURI]*models.Repo)
46
19
47
20
var conditions []string
···
208
181
209
182
starCountQuery := fmt.Sprintf(
210
183
`select
211
-
repo_at, count(1)
184
+
subject_at, count(1)
212
185
from stars
213
-
where repo_at in (%s)
214
-
group by repo_at`,
186
+
where subject_at in (%s)
187
+
group by subject_at`,
215
188
inClause,
216
189
)
217
190
rows, err = e.Query(starCountQuery, args...)
···
322
295
}
323
296
324
297
// helper to get exactly one repo
325
-
func GetRepo(e Execer, filters ...filter) (*models.Repo, error) {
298
+
func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) {
326
299
repos, err := GetRepos(e, 0, filters...)
327
300
if err != nil {
328
301
return nil, err
···
339
312
return &repos[0], nil
340
313
}
341
314
342
-
func CountRepos(e Execer, filters ...filter) (int64, error) {
315
+
func CountRepos(e Execer, filters ...orm.Filter) (int64, error) {
343
316
var conditions []string
344
317
var args []any
345
318
for _, filter := range filters {
···
439
412
return nullableSource.String, nil
440
413
}
441
414
415
+
func GetRepoSourceRepo(e Execer, repoAt syntax.ATURI) (*models.Repo, error) {
416
+
source, err := GetRepoSource(e, repoAt)
417
+
if source == "" || errors.Is(err, sql.ErrNoRows) {
418
+
return nil, nil
419
+
}
420
+
if err != nil {
421
+
return nil, err
422
+
}
423
+
return GetRepoByAtUri(e, source)
424
+
}
425
+
442
426
func GetForksByDid(e Execer, did string) ([]models.Repo, error) {
443
427
var repos []models.Repo
444
428
···
559
543
return err
560
544
}
561
545
562
-
func UnsubscribeLabel(e Execer, filters ...filter) error {
546
+
func UnsubscribeLabel(e Execer, filters ...orm.Filter) error {
563
547
var conditions []string
564
548
var args []any
565
549
for _, filter := range filters {
···
577
561
return err
578
562
}
579
563
580
-
func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) {
564
+
func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) {
581
565
var conditions []string
582
566
var args []any
583
567
for _, filter := range filters {
+6
-5
appview/db/spindle.go
+6
-5
appview/db/spindle.go
···
7
7
"time"
8
8
9
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
10
11
)
11
12
12
-
func GetSpindles(e Execer, filters ...filter) ([]models.Spindle, error) {
13
+
func GetSpindles(e Execer, filters ...orm.Filter) ([]models.Spindle, error) {
13
14
var spindles []models.Spindle
14
15
15
16
var conditions []string
···
91
92
return err
92
93
}
93
94
94
-
func VerifySpindle(e Execer, filters ...filter) (int64, error) {
95
+
func VerifySpindle(e Execer, filters ...orm.Filter) (int64, error) {
95
96
var conditions []string
96
97
var args []any
97
98
for _, filter := range filters {
···
114
115
return res.RowsAffected()
115
116
}
116
117
117
-
func DeleteSpindle(e Execer, filters ...filter) error {
118
+
func DeleteSpindle(e Execer, filters ...orm.Filter) error {
118
119
var conditions []string
119
120
var args []any
120
121
for _, filter := range filters {
···
144
145
return err
145
146
}
146
147
147
-
func RemoveSpindleMember(e Execer, filters ...filter) error {
148
+
func RemoveSpindleMember(e Execer, filters ...orm.Filter) error {
148
149
var conditions []string
149
150
var args []any
150
151
for _, filter := range filters {
···
163
164
return err
164
165
}
165
166
166
-
func GetSpindleMembers(e Execer, filters ...filter) ([]models.SpindleMember, error) {
167
+
func GetSpindleMembers(e Execer, filters ...orm.Filter) ([]models.SpindleMember, error) {
167
168
var members []models.SpindleMember
168
169
169
170
var conditions []string
+43
-102
appview/db/star.go
+43
-102
appview/db/star.go
···
11
11
12
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
14
15
)
15
16
16
17
func AddStar(e Execer, star *models.Star) error {
17
-
query := `insert or ignore into stars (starred_by_did, repo_at, rkey) values (?, ?, ?)`
18
+
query := `insert or ignore into stars (did, subject_at, rkey) values (?, ?, ?)`
18
19
_, err := e.Exec(
19
20
query,
20
-
star.StarredByDid,
21
+
star.Did,
21
22
star.RepoAt.String(),
22
23
star.Rkey,
23
24
)
···
25
26
}
26
27
27
28
// Get a star record
28
-
func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*models.Star, error) {
29
+
func GetStar(e Execer, did string, subjectAt syntax.ATURI) (*models.Star, error) {
29
30
query := `
30
-
select starred_by_did, repo_at, created, rkey
31
+
select did, subject_at, created, rkey
31
32
from stars
32
-
where starred_by_did = ? and repo_at = ?`
33
-
row := e.QueryRow(query, starredByDid, repoAt)
33
+
where did = ? and subject_at = ?`
34
+
row := e.QueryRow(query, did, subjectAt)
34
35
35
36
var star models.Star
36
37
var created string
37
-
err := row.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
38
+
err := row.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey)
38
39
if err != nil {
39
40
return nil, err
40
41
}
···
51
52
}
52
53
53
54
// Remove a star
54
-
func DeleteStar(e Execer, starredByDid string, repoAt syntax.ATURI) error {
55
-
_, err := e.Exec(`delete from stars where starred_by_did = ? and repo_at = ?`, starredByDid, repoAt)
55
+
func DeleteStar(e Execer, did string, subjectAt syntax.ATURI) error {
56
+
_, err := e.Exec(`delete from stars where did = ? and subject_at = ?`, did, subjectAt)
56
57
return err
57
58
}
58
59
59
60
// Remove a star
60
-
func DeleteStarByRkey(e Execer, starredByDid string, rkey string) error {
61
-
_, err := e.Exec(`delete from stars where starred_by_did = ? and rkey = ?`, starredByDid, rkey)
61
+
func DeleteStarByRkey(e Execer, did string, rkey string) error {
62
+
_, err := e.Exec(`delete from stars where did = ? and rkey = ?`, did, rkey)
62
63
return err
63
64
}
64
65
65
-
func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) {
66
+
func GetStarCount(e Execer, subjectAt syntax.ATURI) (int, error) {
66
67
stars := 0
67
68
err := e.QueryRow(
68
-
`select count(starred_by_did) from stars where repo_at = ?`, repoAt).Scan(&stars)
69
+
`select count(did) from stars where subject_at = ?`, subjectAt).Scan(&stars)
69
70
if err != nil {
70
71
return 0, err
71
72
}
···
89
90
}
90
91
91
92
query := fmt.Sprintf(`
92
-
SELECT repo_at
93
+
SELECT subject_at
93
94
FROM stars
94
-
WHERE starred_by_did = ? AND repo_at IN (%s)
95
+
WHERE did = ? AND subject_at IN (%s)
95
96
`, strings.Join(placeholders, ","))
96
97
97
98
rows, err := e.Query(query, args...)
···
118
119
return result, nil
119
120
}
120
121
121
-
func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool {
122
-
statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{repoAt})
122
+
func GetStarStatus(e Execer, userDid string, subjectAt syntax.ATURI) bool {
123
+
statuses, err := getStarStatuses(e, userDid, []syntax.ATURI{subjectAt})
123
124
if err != nil {
124
125
return false
125
126
}
126
-
return statuses[repoAt.String()]
127
+
return statuses[subjectAt.String()]
127
128
}
128
129
129
130
// GetStarStatuses returns a map of repo URIs to star status for a given user
130
-
func GetStarStatuses(e Execer, userDid string, repoAts []syntax.ATURI) (map[string]bool, error) {
131
-
return getStarStatuses(e, userDid, repoAts)
131
+
func GetStarStatuses(e Execer, userDid string, subjectAts []syntax.ATURI) (map[string]bool, error) {
132
+
return getStarStatuses(e, userDid, subjectAts)
132
133
}
133
-
func GetStars(e Execer, limit int, filters ...filter) ([]models.Star, error) {
134
+
135
+
// GetRepoStars return a list of stars each holding target repository.
136
+
// If there isn't known repo with starred at-uri, those stars will be ignored.
137
+
func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) {
134
138
var conditions []string
135
139
var args []any
136
140
for _, filter := range filters {
···
149
153
}
150
154
151
155
repoQuery := fmt.Sprintf(
152
-
`select starred_by_did, repo_at, created, rkey
156
+
`select did, subject_at, created, rkey
153
157
from stars
154
158
%s
155
159
order by created desc
···
166
170
for rows.Next() {
167
171
var star models.Star
168
172
var created string
169
-
err := rows.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey)
173
+
err := rows.Scan(&star.Did, &star.RepoAt, &created, &star.Rkey)
170
174
if err != nil {
171
175
return nil, err
172
176
}
···
192
196
return nil, nil
193
197
}
194
198
195
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
199
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args))
196
200
if err != nil {
197
201
return nil, err
198
202
}
199
203
204
+
var repoStars []models.RepoStar
200
205
for _, r := range repos {
201
206
if stars, ok := starMap[string(r.RepoAt())]; ok {
202
-
for i := range stars {
203
-
stars[i].Repo = &r
207
+
for _, star := range stars {
208
+
repoStars = append(repoStars, models.RepoStar{
209
+
Star: star,
210
+
Repo: &r,
211
+
})
204
212
}
205
213
}
206
214
}
207
215
208
-
var stars []models.Star
209
-
for _, s := range starMap {
210
-
stars = append(stars, s...)
211
-
}
212
-
213
-
slices.SortFunc(stars, func(a, b models.Star) int {
216
+
slices.SortFunc(repoStars, func(a, b models.RepoStar) int {
214
217
if a.Created.After(b.Created) {
215
218
return -1
216
219
}
···
220
223
return 0
221
224
})
222
225
223
-
return stars, nil
226
+
return repoStars, nil
224
227
}
225
228
226
-
func CountStars(e Execer, filters ...filter) (int64, error) {
229
+
func CountStars(e Execer, filters ...orm.Filter) (int64, error) {
227
230
var conditions []string
228
231
var args []any
229
232
for _, filter := range filters {
···
247
250
return count, nil
248
251
}
249
252
250
-
func GetAllStars(e Execer, limit int) ([]models.Star, error) {
251
-
var stars []models.Star
252
-
253
-
rows, err := e.Query(`
254
-
select
255
-
s.starred_by_did,
256
-
s.repo_at,
257
-
s.rkey,
258
-
s.created,
259
-
r.did,
260
-
r.name,
261
-
r.knot,
262
-
r.rkey,
263
-
r.created
264
-
from stars s
265
-
join repos r on s.repo_at = r.at_uri
266
-
`)
267
-
268
-
if err != nil {
269
-
return nil, err
270
-
}
271
-
defer rows.Close()
272
-
273
-
for rows.Next() {
274
-
var star models.Star
275
-
var repo models.Repo
276
-
var starCreatedAt, repoCreatedAt string
277
-
278
-
if err := rows.Scan(
279
-
&star.StarredByDid,
280
-
&star.RepoAt,
281
-
&star.Rkey,
282
-
&starCreatedAt,
283
-
&repo.Did,
284
-
&repo.Name,
285
-
&repo.Knot,
286
-
&repo.Rkey,
287
-
&repoCreatedAt,
288
-
); err != nil {
289
-
return nil, err
290
-
}
291
-
292
-
star.Created, err = time.Parse(time.RFC3339, starCreatedAt)
293
-
if err != nil {
294
-
star.Created = time.Now()
295
-
}
296
-
repo.Created, err = time.Parse(time.RFC3339, repoCreatedAt)
297
-
if err != nil {
298
-
repo.Created = time.Now()
299
-
}
300
-
star.Repo = &repo
301
-
302
-
stars = append(stars, star)
303
-
}
304
-
305
-
if err := rows.Err(); err != nil {
306
-
return nil, err
307
-
}
308
-
309
-
return stars, nil
310
-
}
311
-
312
253
// GetTopStarredReposLastWeek returns the top 8 most starred repositories from the last week
313
254
func GetTopStarredReposLastWeek(e Execer) ([]models.Repo, error) {
314
255
// first, get the top repo URIs by star count from the last week
315
256
query := `
316
257
with recent_starred_repos as (
317
-
select distinct repo_at
258
+
select distinct subject_at
318
259
from stars
319
260
where created >= datetime('now', '-7 days')
320
261
),
321
262
repo_star_counts as (
322
263
select
323
-
s.repo_at,
264
+
s.subject_at,
324
265
count(*) as stars_gained_last_week
325
266
from stars s
326
-
join recent_starred_repos rsr on s.repo_at = rsr.repo_at
267
+
join recent_starred_repos rsr on s.subject_at = rsr.subject_at
327
268
where s.created >= datetime('now', '-7 days')
328
-
group by s.repo_at
269
+
group by s.subject_at
329
270
)
330
-
select rsc.repo_at
271
+
select rsc.subject_at
331
272
from repo_star_counts rsc
332
273
order by rsc.stars_gained_last_week desc
333
274
limit 8
···
358
299
}
359
300
360
301
// get full repo data
361
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
302
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris))
362
303
if err != nil {
363
304
return nil, err
364
305
}
+4
-3
appview/db/strings.go
+4
-3
appview/db/strings.go
···
8
8
"time"
9
9
10
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
11
12
)
12
13
13
14
func AddString(e Execer, s models.String) error {
···
44
45
return err
45
46
}
46
47
47
-
func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) {
48
+
func GetStrings(e Execer, limit int, filters ...orm.Filter) ([]models.String, error) {
48
49
var all []models.String
49
50
50
51
var conditions []string
···
127
128
return all, nil
128
129
}
129
130
130
-
func CountStrings(e Execer, filters ...filter) (int64, error) {
131
+
func CountStrings(e Execer, filters ...orm.Filter) (int64, error) {
131
132
var conditions []string
132
133
var args []any
133
134
for _, filter := range filters {
···
151
152
return count, nil
152
153
}
153
154
154
-
func DeleteString(e Execer, filters ...filter) error {
155
+
func DeleteString(e Execer, filters ...orm.Filter) error {
155
156
var conditions []string
156
157
var args []any
157
158
for _, filter := range filters {
+11
-20
appview/db/timeline.go
+11
-20
appview/db/timeline.go
···
5
5
6
6
"github.com/bluesky-social/indigo/atproto/syntax"
7
7
"tangled.org/core/appview/models"
8
+
"tangled.org/core/orm"
8
9
)
9
10
10
11
// TODO: this gathers heterogenous events from different sources and aggregates
···
84
85
}
85
86
86
87
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
87
-
filters := make([]filter, 0)
88
+
filters := make([]orm.Filter, 0)
88
89
if userIsFollowing != nil {
89
-
filters = append(filters, FilterIn("did", userIsFollowing))
90
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
90
91
}
91
92
92
93
repos, err := GetRepos(e, limit, filters...)
···
104
105
105
106
var origRepos []models.Repo
106
107
if args != nil {
107
-
origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
108
+
origRepos, err = GetRepos(e, 0, orm.FilterIn("at_uri", args))
108
109
}
109
110
if err != nil {
110
111
return nil, err
···
144
145
}
145
146
146
147
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
147
-
filters := make([]filter, 0)
148
+
filters := make([]orm.Filter, 0)
148
149
if userIsFollowing != nil {
149
-
filters = append(filters, FilterIn("starred_by_did", userIsFollowing))
150
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
150
151
}
151
152
152
-
stars, err := GetStars(e, limit, filters...)
153
+
stars, err := GetRepoStars(e, limit, filters...)
153
154
if err != nil {
154
155
return nil, err
155
156
}
156
157
157
-
// filter star records without a repo
158
-
n := 0
159
-
for _, s := range stars {
160
-
if s.Repo != nil {
161
-
stars[n] = s
162
-
n++
163
-
}
164
-
}
165
-
stars = stars[:n]
166
-
167
158
var repos []models.Repo
168
159
for _, s := range stars {
169
160
repos = append(repos, *s.Repo)
···
179
170
isStarred, starCount := getRepoStarInfo(s.Repo, starStatuses)
180
171
181
172
events = append(events, models.TimelineEvent{
182
-
Star: &s,
173
+
RepoStar: &s,
183
174
EventAt: s.Created,
184
175
IsStarred: isStarred,
185
176
StarCount: starCount,
···
190
181
}
191
182
192
183
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
193
-
filters := make([]filter, 0)
184
+
filters := make([]orm.Filter, 0)
194
185
if userIsFollowing != nil {
195
-
filters = append(filters, FilterIn("user_did", userIsFollowing))
186
+
filters = append(filters, orm.FilterIn("user_did", userIsFollowing))
196
187
}
197
188
198
189
follows, err := GetFollows(e, limit, filters...)
···
209
200
return nil, nil
210
201
}
211
202
212
-
profiles, err := GetProfiles(e, FilterIn("did", subjects))
203
+
profiles, err := GetProfiles(e, orm.FilterIn("did", subjects))
213
204
if err != nil {
214
205
return nil, err
215
206
}
+7
-12
appview/email/email.go
+7
-12
appview/email/email.go
···
3
3
import (
4
4
"fmt"
5
5
"net"
6
-
"regexp"
6
+
"net/mail"
7
7
"strings"
8
8
9
9
"github.com/resend/resend-go/v2"
···
34
34
}
35
35
36
36
func IsValidEmail(email string) bool {
37
-
// Basic length check
38
-
if len(email) < 3 || len(email) > 254 {
37
+
// Reject whitespace (ParseAddress normalizes it away)
38
+
if strings.ContainsAny(email, " \t\n\r") {
39
39
return false
40
40
}
41
41
42
-
// Regular expression for email validation (RFC 5322 compliant)
43
-
pattern := `^[a-zA-Z0-9.!#$%&'*+/=?^_\x60{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`
44
-
45
-
// Compile regex
46
-
regex := regexp.MustCompile(pattern)
47
-
48
-
// Check if email matches regex pattern
49
-
if !regex.MatchString(email) {
42
+
// Use stdlib RFC 5322 parser
43
+
addr, err := mail.ParseAddress(email)
44
+
if err != nil {
50
45
return false
51
46
}
52
47
53
48
// Split email into local and domain parts
54
-
parts := strings.Split(email, "@")
49
+
parts := strings.Split(addr.Address, "@")
55
50
domain := parts[1]
56
51
57
52
mx, err := net.LookupMX(domain)
+53
appview/email/email_test.go
+53
appview/email/email_test.go
···
1
+
package email
2
+
3
+
import (
4
+
"testing"
5
+
)
6
+
7
+
func TestIsValidEmail(t *testing.T) {
8
+
tests := []struct {
9
+
name string
10
+
email string
11
+
want bool
12
+
}{
13
+
// Valid emails using RFC 2606 reserved domains
14
+
{"standard email", "user@example.com", true},
15
+
{"single char local", "a@example.com", true},
16
+
{"dot in middle", "first.last@example.com", true},
17
+
{"multiple dots", "a.b.c@example.com", true},
18
+
{"plus tag", "user+tag@example.com", true},
19
+
{"numbers", "user123@example.com", true},
20
+
{"example.org", "user@example.org", true},
21
+
{"example.net", "user@example.net", true},
22
+
23
+
// Invalid format - rejected by mail.ParseAddress
24
+
{"empty string", "", false},
25
+
{"no at sign", "userexample.com", false},
26
+
{"no domain", "user@", false},
27
+
{"no local part", "@example.com", false},
28
+
{"double at", "user@@example.com", false},
29
+
{"just at sign", "@", false},
30
+
{"leading dot", ".user@example.com", false},
31
+
{"trailing dot", "user.@example.com", false},
32
+
{"consecutive dots", "user..name@example.com", false},
33
+
34
+
// Whitespace - rejected before parsing
35
+
{"space in local", "user @example.com", false},
36
+
{"space in domain", "user@ example.com", false},
37
+
{"tab", "user\t@example.com", false},
38
+
{"newline", "user\n@example.com", false},
39
+
40
+
// MX lookup - using RFC 2606 reserved TLDs (guaranteed no MX)
41
+
{"invalid TLD", "user@example.invalid", false},
42
+
{"test TLD", "user@mail.test", false},
43
+
}
44
+
45
+
for _, tt := range tests {
46
+
t.Run(tt.name, func(t *testing.T) {
47
+
got := IsValidEmail(tt.email)
48
+
if got != tt.want {
49
+
t.Errorf("IsValidEmail(%q) = %v, want %v", tt.email, got, tt.want)
50
+
}
51
+
})
52
+
}
53
+
}
+56
-32
appview/ingester.go
+56
-32
appview/ingester.go
···
21
21
"tangled.org/core/appview/serververify"
22
22
"tangled.org/core/appview/validator"
23
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
24
25
"tangled.org/core/rbac"
25
26
)
26
27
···
121
122
return err
122
123
}
123
124
err = db.AddStar(i.Db, &models.Star{
124
-
StarredByDid: did,
125
-
RepoAt: subjectUri,
126
-
Rkey: e.Commit.RKey,
125
+
Did: did,
126
+
RepoAt: subjectUri,
127
+
Rkey: e.Commit.RKey,
127
128
})
128
129
case jmodels.CommitOperationDelete:
129
130
err = db.DeleteStarByRkey(i.Db, did, e.Commit.RKey)
···
253
254
254
255
err = db.AddArtifact(i.Db, artifact)
255
256
case jmodels.CommitOperationDelete:
256
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
257
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
257
258
}
258
259
259
260
if err != nil {
···
284
285
return err
285
286
}
286
287
288
+
avatar := ""
289
+
if record.Avatar != nil {
290
+
avatar = record.Avatar.Ref.String()
291
+
}
292
+
287
293
description := ""
288
294
if record.Description != nil {
289
295
description = *record.Description
···
324
330
325
331
profile := models.Profile{
326
332
Did: did,
333
+
Avatar: avatar,
327
334
Description: description,
328
335
IncludeBluesky: includeBluesky,
329
336
Location: location,
···
350
357
351
358
err = db.UpsertProfile(tx, &profile)
352
359
case jmodels.CommitOperationDelete:
353
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
360
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
354
361
}
355
362
356
363
if err != nil {
···
424
431
// get record from db first
425
432
members, err := db.GetSpindleMembers(
426
433
ddb,
427
-
db.FilterEq("did", did),
428
-
db.FilterEq("rkey", rkey),
434
+
orm.FilterEq("did", did),
435
+
orm.FilterEq("rkey", rkey),
429
436
)
430
437
if err != nil || len(members) != 1 {
431
438
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
440
447
// remove record by rkey && update enforcer
441
448
if err = db.RemoveSpindleMember(
442
449
tx,
443
-
db.FilterEq("did", did),
444
-
db.FilterEq("rkey", rkey),
450
+
orm.FilterEq("did", did),
451
+
orm.FilterEq("rkey", rkey),
445
452
); err != nil {
446
453
return fmt.Errorf("failed to remove from db: %w", err)
447
454
}
···
523
530
// get record from db first
524
531
spindles, err := db.GetSpindles(
525
532
ddb,
526
-
db.FilterEq("owner", did),
527
-
db.FilterEq("instance", instance),
533
+
orm.FilterEq("owner", did),
534
+
orm.FilterEq("instance", instance),
528
535
)
529
536
if err != nil || len(spindles) != 1 {
530
537
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
543
550
// remove spindle members first
544
551
err = db.RemoveSpindleMember(
545
552
tx,
546
-
db.FilterEq("owner", did),
547
-
db.FilterEq("instance", instance),
553
+
orm.FilterEq("owner", did),
554
+
orm.FilterEq("instance", instance),
548
555
)
549
556
if err != nil {
550
557
return err
···
552
559
553
560
err = db.DeleteSpindle(
554
561
tx,
555
-
db.FilterEq("owner", did),
556
-
db.FilterEq("instance", instance),
562
+
orm.FilterEq("owner", did),
563
+
orm.FilterEq("instance", instance),
557
564
)
558
565
if err != nil {
559
566
return err
···
621
628
case jmodels.CommitOperationDelete:
622
629
if err := db.DeleteString(
623
630
ddb,
624
-
db.FilterEq("did", did),
625
-
db.FilterEq("rkey", rkey),
631
+
orm.FilterEq("did", did),
632
+
orm.FilterEq("rkey", rkey),
626
633
); err != nil {
627
634
l.Error("failed to delete", "err", err)
628
635
return fmt.Errorf("failed to delete string record: %w", err)
···
740
747
// get record from db first
741
748
registrations, err := db.GetRegistrations(
742
749
ddb,
743
-
db.FilterEq("domain", domain),
744
-
db.FilterEq("did", did),
750
+
orm.FilterEq("domain", domain),
751
+
orm.FilterEq("did", did),
745
752
)
746
753
if err != nil {
747
754
return fmt.Errorf("failed to get registration: %w", err)
···
762
769
763
770
err = db.DeleteKnot(
764
771
tx,
765
-
db.FilterEq("did", did),
766
-
db.FilterEq("domain", domain),
772
+
orm.FilterEq("did", did),
773
+
orm.FilterEq("domain", domain),
767
774
)
768
775
if err != nil {
769
776
return err
···
841
848
return nil
842
849
843
850
case jmodels.CommitOperationDelete:
851
+
tx, err := ddb.BeginTx(ctx, nil)
852
+
if err != nil {
853
+
l.Error("failed to begin transaction", "err", err)
854
+
return err
855
+
}
856
+
defer tx.Rollback()
857
+
844
858
if err := db.DeleteIssues(
845
-
ddb,
846
-
db.FilterEq("did", did),
847
-
db.FilterEq("rkey", rkey),
859
+
tx,
860
+
did,
861
+
rkey,
848
862
); err != nil {
849
863
l.Error("failed to delete", "err", err)
850
864
return fmt.Errorf("failed to delete issue record: %w", err)
865
+
}
866
+
if err := tx.Commit(); err != nil {
867
+
l.Error("failed to commit txn", "err", err)
868
+
return err
851
869
}
852
870
853
871
return nil
···
888
906
return fmt.Errorf("failed to validate comment: %w", err)
889
907
}
890
908
891
-
_, err = db.AddIssueComment(ddb, *comment)
909
+
tx, err := ddb.Begin()
910
+
if err != nil {
911
+
return fmt.Errorf("failed to start transaction: %w", err)
912
+
}
913
+
defer tx.Rollback()
914
+
915
+
_, err = db.AddIssueComment(tx, *comment)
892
916
if err != nil {
893
917
return fmt.Errorf("failed to create issue comment: %w", err)
894
918
}
895
919
896
-
return nil
920
+
return tx.Commit()
897
921
898
922
case jmodels.CommitOperationDelete:
899
923
if err := db.DeleteIssueComments(
900
924
ddb,
901
-
db.FilterEq("did", did),
902
-
db.FilterEq("rkey", rkey),
925
+
orm.FilterEq("did", did),
926
+
orm.FilterEq("rkey", rkey),
903
927
); err != nil {
904
928
return fmt.Errorf("failed to delete issue comment record: %w", err)
905
929
}
···
952
976
case jmodels.CommitOperationDelete:
953
977
if err := db.DeleteLabelDefinition(
954
978
ddb,
955
-
db.FilterEq("did", did),
956
-
db.FilterEq("rkey", rkey),
979
+
orm.FilterEq("did", did),
980
+
orm.FilterEq("rkey", rkey),
957
981
); err != nil {
958
982
return fmt.Errorf("failed to delete labeldef record: %w", err)
959
983
}
···
993
1017
var repo *models.Repo
994
1018
switch collection {
995
1019
case tangled.RepoIssueNSID:
996
-
i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject))
1020
+
i, err := db.GetIssues(ddb, orm.FilterEq("at_uri", subject))
997
1021
if err != nil || len(i) != 1 {
998
1022
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
999
1023
}
···
1002
1026
return fmt.Errorf("unsupport label subject: %s", collection)
1003
1027
}
1004
1028
1005
-
actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels))
1029
+
actx, err := db.NewLabelApplicationCtx(ddb, orm.FilterIn("at_uri", repo.Labels))
1006
1030
if err != nil {
1007
1031
return fmt.Errorf("failed to build label application ctx: %w", err)
1008
1032
}
+152
-135
appview/issues/issues.go
+152
-135
appview/issues/issues.go
···
7
7
"fmt"
8
8
"log/slog"
9
9
"net/http"
10
-
"slices"
11
10
"time"
12
11
13
12
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
20
19
"tangled.org/core/appview/config"
21
20
"tangled.org/core/appview/db"
22
21
issues_indexer "tangled.org/core/appview/indexer/issues"
22
+
"tangled.org/core/appview/mentions"
23
23
"tangled.org/core/appview/models"
24
24
"tangled.org/core/appview/notify"
25
25
"tangled.org/core/appview/oauth"
26
26
"tangled.org/core/appview/pages"
27
-
"tangled.org/core/appview/pages/markup"
27
+
"tangled.org/core/appview/pages/repoinfo"
28
28
"tangled.org/core/appview/pagination"
29
29
"tangled.org/core/appview/reporesolver"
30
30
"tangled.org/core/appview/validator"
31
31
"tangled.org/core/idresolver"
32
+
"tangled.org/core/orm"
33
+
"tangled.org/core/rbac"
32
34
"tangled.org/core/tid"
33
35
)
34
36
35
37
type Issues struct {
36
-
oauth *oauth.OAuth
37
-
repoResolver *reporesolver.RepoResolver
38
-
pages *pages.Pages
39
-
idResolver *idresolver.Resolver
40
-
db *db.DB
41
-
config *config.Config
42
-
notifier notify.Notifier
43
-
logger *slog.Logger
44
-
validator *validator.Validator
45
-
indexer *issues_indexer.Indexer
38
+
oauth *oauth.OAuth
39
+
repoResolver *reporesolver.RepoResolver
40
+
enforcer *rbac.Enforcer
41
+
pages *pages.Pages
42
+
idResolver *idresolver.Resolver
43
+
mentionsResolver *mentions.Resolver
44
+
db *db.DB
45
+
config *config.Config
46
+
notifier notify.Notifier
47
+
logger *slog.Logger
48
+
validator *validator.Validator
49
+
indexer *issues_indexer.Indexer
46
50
}
47
51
48
52
func New(
49
53
oauth *oauth.OAuth,
50
54
repoResolver *reporesolver.RepoResolver,
55
+
enforcer *rbac.Enforcer,
51
56
pages *pages.Pages,
52
57
idResolver *idresolver.Resolver,
58
+
mentionsResolver *mentions.Resolver,
53
59
db *db.DB,
54
60
config *config.Config,
55
61
notifier notify.Notifier,
···
58
64
logger *slog.Logger,
59
65
) *Issues {
60
66
return &Issues{
61
-
oauth: oauth,
62
-
repoResolver: repoResolver,
63
-
pages: pages,
64
-
idResolver: idResolver,
65
-
db: db,
66
-
config: config,
67
-
notifier: notifier,
68
-
logger: logger,
69
-
validator: validator,
70
-
indexer: indexer,
67
+
oauth: oauth,
68
+
repoResolver: repoResolver,
69
+
enforcer: enforcer,
70
+
pages: pages,
71
+
idResolver: idResolver,
72
+
mentionsResolver: mentionsResolver,
73
+
db: db,
74
+
config: config,
75
+
notifier: notifier,
76
+
logger: logger,
77
+
validator: validator,
78
+
indexer: indexer,
71
79
}
72
80
}
73
81
···
97
105
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
98
106
}
99
107
108
+
backlinks, err := db.GetBacklinks(rp.db, issue.AtUri())
109
+
if err != nil {
110
+
l.Error("failed to fetch backlinks", "err", err)
111
+
rp.pages.Error503(w)
112
+
return
113
+
}
114
+
100
115
labelDefs, err := db.GetLabelDefinitions(
101
116
rp.db,
102
-
db.FilterIn("at_uri", f.Repo.Labels),
103
-
db.FilterContains("scope", tangled.RepoIssueNSID),
117
+
orm.FilterIn("at_uri", f.Labels),
118
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
104
119
)
105
120
if err != nil {
106
121
l.Error("failed to fetch labels", "err", err)
···
115
130
116
131
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
117
132
LoggedInUser: user,
118
-
RepoInfo: f.RepoInfo(user),
133
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
119
134
Issue: issue,
120
135
CommentList: issue.CommentList(),
136
+
Backlinks: backlinks,
121
137
OrderedReactionKinds: models.OrderedReactionKinds,
122
138
Reactions: reactionMap,
123
139
UserReacted: userReactions,
···
128
144
func (rp *Issues) EditIssue(w http.ResponseWriter, r *http.Request) {
129
145
l := rp.logger.With("handler", "EditIssue")
130
146
user := rp.oauth.GetUser(r)
131
-
f, err := rp.repoResolver.Resolve(r)
132
-
if err != nil {
133
-
l.Error("failed to get repo and knot", "err", err)
134
-
return
135
-
}
136
147
137
148
issue, ok := r.Context().Value("issue").(*models.Issue)
138
149
if !ok {
···
145
156
case http.MethodGet:
146
157
rp.pages.EditIssueFragment(w, pages.EditIssueParams{
147
158
LoggedInUser: user,
148
-
RepoInfo: f.RepoInfo(user),
159
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
149
160
Issue: issue,
150
161
})
151
162
case http.MethodPost:
···
153
164
newIssue := issue
154
165
newIssue.Title = r.FormValue("title")
155
166
newIssue.Body = r.FormValue("body")
167
+
newIssue.Mentions, newIssue.References = rp.mentionsResolver.Resolve(r.Context(), newIssue.Body)
156
168
157
169
if err := rp.validator.ValidateIssue(newIssue); err != nil {
158
170
l.Error("validation error", "err", err)
···
222
234
l := rp.logger.With("handler", "DeleteIssue")
223
235
noticeId := "issue-actions-error"
224
236
225
-
user := rp.oauth.GetUser(r)
226
-
227
237
f, err := rp.repoResolver.Resolve(r)
228
238
if err != nil {
229
239
l.Error("failed to get repo and knot", "err", err)
···
238
248
}
239
249
l = l.With("did", issue.Did, "rkey", issue.Rkey)
240
250
251
+
tx, err := rp.db.Begin()
252
+
if err != nil {
253
+
l.Error("failed to start transaction", "err", err)
254
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
255
+
return
256
+
}
257
+
defer tx.Rollback()
258
+
241
259
// delete from PDS
242
260
client, err := rp.oauth.AuthorizedClient(r)
243
261
if err != nil {
···
258
276
}
259
277
260
278
// delete from db
261
-
if err := db.DeleteIssues(rp.db, db.FilterEq("id", issue.Id)); err != nil {
279
+
if err := db.DeleteIssues(tx, issue.Did, issue.Rkey); err != nil {
262
280
l.Error("failed to delete issue", "err", err)
263
281
rp.pages.Notice(w, noticeId, "Failed to delete issue.")
264
282
return
265
283
}
284
+
tx.Commit()
266
285
267
286
rp.notifier.DeleteIssue(r.Context(), issue)
268
287
269
288
// return to all issues page
270
-
rp.pages.HxRedirect(w, "/"+f.RepoInfo(user).FullName()+"/issues")
289
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
290
+
rp.pages.HxRedirect(w, "/"+ownerSlashRepo+"/issues")
271
291
}
272
292
273
293
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
···
286
306
return
287
307
}
288
308
289
-
collaborators, err := f.Collaborators(r.Context())
290
-
if err != nil {
291
-
l.Error("failed to fetch repo collaborators", "err", err)
292
-
}
293
-
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
294
-
return user.Did == collab.Did
295
-
})
309
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
310
+
isRepoOwner := roles.IsOwner()
311
+
isCollaborator := roles.IsCollaborator()
296
312
isIssueOwner := user.Did == issue.Did
297
313
298
314
// TODO: make this more granular
299
-
if isIssueOwner || isCollaborator {
315
+
if isIssueOwner || isRepoOwner || isCollaborator {
300
316
err = db.CloseIssues(
301
317
rp.db,
302
-
db.FilterEq("id", issue.Id),
318
+
orm.FilterEq("id", issue.Id),
303
319
)
304
320
if err != nil {
305
321
l.Error("failed to close issue", "err", err)
···
312
328
// notify about the issue closure
313
329
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
314
330
315
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
331
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
332
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
316
333
return
317
334
} else {
318
335
l.Error("user is not permitted to close issue")
···
337
354
return
338
355
}
339
356
340
-
collaborators, err := f.Collaborators(r.Context())
341
-
if err != nil {
342
-
l.Error("failed to fetch repo collaborators", "err", err)
343
-
}
344
-
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
345
-
return user.Did == collab.Did
346
-
})
357
+
roles := repoinfo.RolesInRepo{Roles: rp.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
358
+
isRepoOwner := roles.IsOwner()
359
+
isCollaborator := roles.IsCollaborator()
347
360
isIssueOwner := user.Did == issue.Did
348
361
349
-
if isCollaborator || isIssueOwner {
362
+
if isCollaborator || isRepoOwner || isIssueOwner {
350
363
err := db.ReopenIssues(
351
364
rp.db,
352
-
db.FilterEq("id", issue.Id),
365
+
orm.FilterEq("id", issue.Id),
353
366
)
354
367
if err != nil {
355
368
l.Error("failed to reopen issue", "err", err)
···
362
375
// notify about the issue reopen
363
376
rp.notifier.NewIssueState(r.Context(), syntax.DID(user.Did), issue)
364
377
365
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
378
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
379
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
366
380
return
367
381
} else {
368
382
l.Error("user is not the owner of the repo")
···
398
412
if replyToUri != "" {
399
413
replyTo = &replyToUri
400
414
}
415
+
416
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
401
417
402
418
comment := models.IssueComment{
403
-
Did: user.Did,
404
-
Rkey: tid.TID(),
405
-
IssueAt: issue.AtUri().String(),
406
-
ReplyTo: replyTo,
407
-
Body: body,
408
-
Created: time.Now(),
419
+
Did: user.Did,
420
+
Rkey: tid.TID(),
421
+
IssueAt: issue.AtUri().String(),
422
+
ReplyTo: replyTo,
423
+
Body: body,
424
+
Created: time.Now(),
425
+
Mentions: mentions,
426
+
References: references,
409
427
}
410
428
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
411
429
l.Error("failed to validate comment", "err", err)
···
442
460
}
443
461
}()
444
462
445
-
commentId, err := db.AddIssueComment(rp.db, comment)
463
+
tx, err := rp.db.Begin()
464
+
if err != nil {
465
+
l.Error("failed to start transaction", "err", err)
466
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
467
+
return
468
+
}
469
+
defer tx.Rollback()
470
+
471
+
commentId, err := db.AddIssueComment(tx, comment)
446
472
if err != nil {
447
473
l.Error("failed to create comment", "err", err)
448
474
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
449
475
return
450
476
}
477
+
err = tx.Commit()
478
+
if err != nil {
479
+
l.Error("failed to commit transaction", "err", err)
480
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment, try again later.")
481
+
return
482
+
}
451
483
452
484
// reset atUri to make rollback a no-op
453
485
atUri = ""
···
455
487
// notify about the new comment
456
488
comment.Id = commentId
457
489
458
-
rawMentions := markup.FindUserMentions(comment.Body)
459
-
idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions)
460
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
461
-
var mentions []syntax.DID
462
-
for _, ident := range idents {
463
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
464
-
mentions = append(mentions, ident.DID)
465
-
}
466
-
}
467
490
rp.notifier.NewIssueComment(r.Context(), &comment, mentions)
468
491
469
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId))
492
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
493
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", ownerSlashRepo, issue.IssueId, commentId))
470
494
}
471
495
472
496
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
473
497
l := rp.logger.With("handler", "IssueComment")
474
498
user := rp.oauth.GetUser(r)
475
-
f, err := rp.repoResolver.Resolve(r)
476
-
if err != nil {
477
-
l.Error("failed to get repo and knot", "err", err)
478
-
return
479
-
}
480
499
481
500
issue, ok := r.Context().Value("issue").(*models.Issue)
482
501
if !ok {
···
488
507
commentId := chi.URLParam(r, "commentId")
489
508
comments, err := db.GetIssueComments(
490
509
rp.db,
491
-
db.FilterEq("id", commentId),
510
+
orm.FilterEq("id", commentId),
492
511
)
493
512
if err != nil {
494
513
l.Error("failed to fetch comment", "id", commentId)
···
504
523
505
524
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
506
525
LoggedInUser: user,
507
-
RepoInfo: f.RepoInfo(user),
526
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
508
527
Issue: issue,
509
528
Comment: &comment,
510
529
})
···
513
532
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
514
533
l := rp.logger.With("handler", "EditIssueComment")
515
534
user := rp.oauth.GetUser(r)
516
-
f, err := rp.repoResolver.Resolve(r)
517
-
if err != nil {
518
-
l.Error("failed to get repo and knot", "err", err)
519
-
return
520
-
}
521
535
522
536
issue, ok := r.Context().Value("issue").(*models.Issue)
523
537
if !ok {
···
529
543
commentId := chi.URLParam(r, "commentId")
530
544
comments, err := db.GetIssueComments(
531
545
rp.db,
532
-
db.FilterEq("id", commentId),
546
+
orm.FilterEq("id", commentId),
533
547
)
534
548
if err != nil {
535
549
l.Error("failed to fetch comment", "id", commentId)
···
553
567
case http.MethodGet:
554
568
rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
555
569
LoggedInUser: user,
556
-
RepoInfo: f.RepoInfo(user),
570
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
557
571
Issue: issue,
558
572
Comment: &comment,
559
573
})
···
571
585
newComment := comment
572
586
newComment.Body = newBody
573
587
newComment.Edited = &now
588
+
newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody)
589
+
574
590
record := newComment.AsRecord()
575
591
576
-
_, err = db.AddIssueComment(rp.db, newComment)
592
+
tx, err := rp.db.Begin()
593
+
if err != nil {
594
+
l.Error("failed to start transaction", "err", err)
595
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
596
+
return
597
+
}
598
+
defer tx.Rollback()
599
+
600
+
_, err = db.AddIssueComment(tx, newComment)
577
601
if err != nil {
578
602
l.Error("failed to perferom update-description query", "err", err)
579
603
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
580
604
return
581
605
}
606
+
tx.Commit()
582
607
583
608
// rkey is optional, it was introduced later
584
609
if newComment.Rkey != "" {
···
607
632
// return new comment body with htmx
608
633
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
609
634
LoggedInUser: user,
610
-
RepoInfo: f.RepoInfo(user),
635
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
611
636
Issue: issue,
612
637
Comment: &newComment,
613
638
})
···
617
642
func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
618
643
l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
619
644
user := rp.oauth.GetUser(r)
620
-
f, err := rp.repoResolver.Resolve(r)
621
-
if err != nil {
622
-
l.Error("failed to get repo and knot", "err", err)
623
-
return
624
-
}
625
645
626
646
issue, ok := r.Context().Value("issue").(*models.Issue)
627
647
if !ok {
···
633
653
commentId := chi.URLParam(r, "commentId")
634
654
comments, err := db.GetIssueComments(
635
655
rp.db,
636
-
db.FilterEq("id", commentId),
656
+
orm.FilterEq("id", commentId),
637
657
)
638
658
if err != nil {
639
659
l.Error("failed to fetch comment", "id", commentId)
···
649
669
650
670
rp.pages.ReplyIssueCommentPlaceholderFragment(w, pages.ReplyIssueCommentPlaceholderParams{
651
671
LoggedInUser: user,
652
-
RepoInfo: f.RepoInfo(user),
672
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
653
673
Issue: issue,
654
674
Comment: &comment,
655
675
})
···
658
678
func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
659
679
l := rp.logger.With("handler", "ReplyIssueComment")
660
680
user := rp.oauth.GetUser(r)
661
-
f, err := rp.repoResolver.Resolve(r)
662
-
if err != nil {
663
-
l.Error("failed to get repo and knot", "err", err)
664
-
return
665
-
}
666
681
667
682
issue, ok := r.Context().Value("issue").(*models.Issue)
668
683
if !ok {
···
674
689
commentId := chi.URLParam(r, "commentId")
675
690
comments, err := db.GetIssueComments(
676
691
rp.db,
677
-
db.FilterEq("id", commentId),
692
+
orm.FilterEq("id", commentId),
678
693
)
679
694
if err != nil {
680
695
l.Error("failed to fetch comment", "id", commentId)
···
690
705
691
706
rp.pages.ReplyIssueCommentFragment(w, pages.ReplyIssueCommentParams{
692
707
LoggedInUser: user,
693
-
RepoInfo: f.RepoInfo(user),
708
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
694
709
Issue: issue,
695
710
Comment: &comment,
696
711
})
···
699
714
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
700
715
l := rp.logger.With("handler", "DeleteIssueComment")
701
716
user := rp.oauth.GetUser(r)
702
-
f, err := rp.repoResolver.Resolve(r)
703
-
if err != nil {
704
-
l.Error("failed to get repo and knot", "err", err)
705
-
return
706
-
}
707
717
708
718
issue, ok := r.Context().Value("issue").(*models.Issue)
709
719
if !ok {
···
715
725
commentId := chi.URLParam(r, "commentId")
716
726
comments, err := db.GetIssueComments(
717
727
rp.db,
718
-
db.FilterEq("id", commentId),
728
+
orm.FilterEq("id", commentId),
719
729
)
720
730
if err != nil {
721
731
l.Error("failed to fetch comment", "id", commentId)
···
742
752
743
753
// optimistic deletion
744
754
deleted := time.Now()
745
-
err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id))
755
+
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
746
756
if err != nil {
747
757
l.Error("failed to delete comment", "err", err)
748
758
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
774
784
// htmx fragment of comment after deletion
775
785
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
776
786
LoggedInUser: user,
777
-
RepoInfo: f.RepoInfo(user),
787
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
778
788
Issue: issue,
779
789
Comment: &comment,
780
790
})
···
804
814
return
805
815
}
806
816
817
+
totalIssues := 0
818
+
if isOpen {
819
+
totalIssues = f.RepoStats.IssueCount.Open
820
+
} else {
821
+
totalIssues = f.RepoStats.IssueCount.Closed
822
+
}
823
+
807
824
keyword := params.Get("q")
808
825
809
826
var issues []models.Issue
···
820
837
return
821
838
}
822
839
l.Debug("searched issues with indexer", "count", len(res.Hits))
840
+
totalIssues = int(res.Total)
823
841
824
842
issues, err = db.GetIssues(
825
843
rp.db,
826
-
db.FilterIn("id", res.Hits),
844
+
orm.FilterIn("id", res.Hits),
827
845
)
828
846
if err != nil {
829
847
l.Error("failed to get issues", "err", err)
···
839
857
issues, err = db.GetIssuesPaginated(
840
858
rp.db,
841
859
page,
842
-
db.FilterEq("repo_at", f.RepoAt()),
843
-
db.FilterEq("open", openInt),
860
+
orm.FilterEq("repo_at", f.RepoAt()),
861
+
orm.FilterEq("open", openInt),
844
862
)
845
863
if err != nil {
846
864
l.Error("failed to get issues", "err", err)
···
851
869
852
870
labelDefs, err := db.GetLabelDefinitions(
853
871
rp.db,
854
-
db.FilterIn("at_uri", f.Repo.Labels),
855
-
db.FilterContains("scope", tangled.RepoIssueNSID),
872
+
orm.FilterIn("at_uri", f.Labels),
873
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
856
874
)
857
875
if err != nil {
858
876
l.Error("failed to fetch labels", "err", err)
···
867
885
868
886
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
869
887
LoggedInUser: rp.oauth.GetUser(r),
870
-
RepoInfo: f.RepoInfo(user),
888
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
871
889
Issues: issues,
890
+
IssueCount: totalIssues,
872
891
LabelDefs: defs,
873
892
FilteringByOpen: isOpen,
874
893
FilterQuery: keyword,
···
890
909
case http.MethodGet:
891
910
rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
892
911
LoggedInUser: user,
893
-
RepoInfo: f.RepoInfo(user),
912
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
894
913
})
895
914
case http.MethodPost:
915
+
body := r.FormValue("body")
916
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
917
+
896
918
issue := &models.Issue{
897
-
RepoAt: f.RepoAt(),
898
-
Rkey: tid.TID(),
899
-
Title: r.FormValue("title"),
900
-
Body: r.FormValue("body"),
901
-
Open: true,
902
-
Did: user.Did,
903
-
Created: time.Now(),
904
-
Repo: &f.Repo,
919
+
RepoAt: f.RepoAt(),
920
+
Rkey: tid.TID(),
921
+
Title: r.FormValue("title"),
922
+
Body: body,
923
+
Open: true,
924
+
Did: user.Did,
925
+
Created: time.Now(),
926
+
Mentions: mentions,
927
+
References: references,
928
+
Repo: f,
905
929
}
906
930
907
931
if err := rp.validator.ValidateIssue(issue); err != nil {
···
969
993
// everything is successful, do not rollback the atproto record
970
994
atUri = ""
971
995
972
-
rawMentions := markup.FindUserMentions(issue.Body)
973
-
idents := rp.idResolver.ResolveIdents(r.Context(), rawMentions)
974
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
975
-
var mentions []syntax.DID
976
-
for _, ident := range idents {
977
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
978
-
mentions = append(mentions, ident.DID)
979
-
}
980
-
}
981
996
rp.notifier.NewIssue(r.Context(), issue, mentions)
982
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
997
+
998
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
999
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", ownerSlashRepo, issue.IssueId))
983
1000
return
984
1001
}
985
1002
}
+3
-3
appview/issues/opengraph.go
+3
-3
appview/issues/opengraph.go
···
232
232
233
233
// Get owner handle for avatar
234
234
var ownerHandle string
235
-
owner, err := rp.idResolver.ResolveIdent(r.Context(), f.Repo.Did)
235
+
owner, err := rp.idResolver.ResolveIdent(r.Context(), f.Did)
236
236
if err != nil {
237
-
ownerHandle = f.Repo.Did
237
+
ownerHandle = f.Did
238
238
} else {
239
239
ownerHandle = "@" + owner.Handle.String()
240
240
}
241
241
242
-
card, err := rp.drawIssueSummaryCard(issue, &f.Repo, commentCount, ownerHandle)
242
+
card, err := rp.drawIssueSummaryCard(issue, f, commentCount, ownerHandle)
243
243
if err != nil {
244
244
log.Println("failed to draw issue summary card", err)
245
245
http.Error(w, "failed to draw issue summary card", http.StatusInternalServerError)
+37
-19
appview/knots/knots.go
+37
-19
appview/knots/knots.go
···
21
21
"tangled.org/core/appview/xrpcclient"
22
22
"tangled.org/core/eventconsumer"
23
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
24
25
"tangled.org/core/rbac"
25
26
"tangled.org/core/tid"
26
27
···
39
40
Knotstream *eventconsumer.Consumer
40
41
}
41
42
43
+
type tab = map[string]any
44
+
45
+
var (
46
+
knotsTabs []tab = []tab{
47
+
{"Name": "profile", "Icon": "user"},
48
+
{"Name": "keys", "Icon": "key"},
49
+
{"Name": "emails", "Icon": "mail"},
50
+
{"Name": "notifications", "Icon": "bell"},
51
+
{"Name": "knots", "Icon": "volleyball"},
52
+
{"Name": "spindles", "Icon": "spool"},
53
+
}
54
+
)
55
+
42
56
func (k *Knots) Router() http.Handler {
43
57
r := chi.NewRouter()
44
58
···
59
73
user := k.OAuth.GetUser(r)
60
74
registrations, err := db.GetRegistrations(
61
75
k.Db,
62
-
db.FilterEq("did", user.Did),
76
+
orm.FilterEq("did", user.Did),
63
77
)
64
78
if err != nil {
65
79
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
70
84
k.Pages.Knots(w, pages.KnotsParams{
71
85
LoggedInUser: user,
72
86
Registrations: registrations,
87
+
Tabs: knotsTabs,
88
+
Tab: "knots",
73
89
})
74
90
}
75
91
···
87
103
88
104
registrations, err := db.GetRegistrations(
89
105
k.Db,
90
-
db.FilterEq("did", user.Did),
91
-
db.FilterEq("domain", domain),
106
+
orm.FilterEq("did", user.Did),
107
+
orm.FilterEq("domain", domain),
92
108
)
93
109
if err != nil {
94
110
l.Error("failed to get registrations", "err", err)
···
112
128
repos, err := db.GetRepos(
113
129
k.Db,
114
130
0,
115
-
db.FilterEq("knot", domain),
131
+
orm.FilterEq("knot", domain),
116
132
)
117
133
if err != nil {
118
134
l.Error("failed to get knot repos", "err", err)
···
132
148
Members: members,
133
149
Repos: repoMap,
134
150
IsOwner: true,
151
+
Tabs: knotsTabs,
152
+
Tab: "knots",
135
153
})
136
154
}
137
155
···
276
294
// get record from db first
277
295
registrations, err := db.GetRegistrations(
278
296
k.Db,
279
-
db.FilterEq("did", user.Did),
280
-
db.FilterEq("domain", domain),
297
+
orm.FilterEq("did", user.Did),
298
+
orm.FilterEq("domain", domain),
281
299
)
282
300
if err != nil {
283
301
l.Error("failed to get registration", "err", err)
···
304
322
305
323
err = db.DeleteKnot(
306
324
tx,
307
-
db.FilterEq("did", user.Did),
308
-
db.FilterEq("domain", domain),
325
+
orm.FilterEq("did", user.Did),
326
+
orm.FilterEq("domain", domain),
309
327
)
310
328
if err != nil {
311
329
l.Error("failed to delete registration", "err", err)
···
385
403
// get record from db first
386
404
registrations, err := db.GetRegistrations(
387
405
k.Db,
388
-
db.FilterEq("did", user.Did),
389
-
db.FilterEq("domain", domain),
406
+
orm.FilterEq("did", user.Did),
407
+
orm.FilterEq("domain", domain),
390
408
)
391
409
if err != nil {
392
410
l.Error("failed to get registration", "err", err)
···
476
494
// Get updated registration to show
477
495
registrations, err = db.GetRegistrations(
478
496
k.Db,
479
-
db.FilterEq("did", user.Did),
480
-
db.FilterEq("domain", domain),
497
+
orm.FilterEq("did", user.Did),
498
+
orm.FilterEq("domain", domain),
481
499
)
482
500
if err != nil {
483
501
l.Error("failed to get registration", "err", err)
···
512
530
513
531
registrations, err := db.GetRegistrations(
514
532
k.Db,
515
-
db.FilterEq("did", user.Did),
516
-
db.FilterEq("domain", domain),
517
-
db.FilterIsNot("registered", "null"),
533
+
orm.FilterEq("did", user.Did),
534
+
orm.FilterEq("domain", domain),
535
+
orm.FilterIsNot("registered", "null"),
518
536
)
519
537
if err != nil {
520
538
l.Error("failed to get registration", "err", err)
···
596
614
}
597
615
598
616
// success
599
-
k.Pages.HxRedirect(w, fmt.Sprintf("/knots/%s", domain))
617
+
k.Pages.HxRedirect(w, fmt.Sprintf("/settings/knots/%s", domain))
600
618
}
601
619
602
620
func (k *Knots) removeMember(w http.ResponseWriter, r *http.Request) {
···
620
638
621
639
registrations, err := db.GetRegistrations(
622
640
k.Db,
623
-
db.FilterEq("did", user.Did),
624
-
db.FilterEq("domain", domain),
625
-
db.FilterIsNot("registered", "null"),
641
+
orm.FilterEq("did", user.Did),
642
+
orm.FilterEq("domain", domain),
643
+
orm.FilterIsNot("registered", "null"),
626
644
)
627
645
if err != nil {
628
646
l.Error("failed to get registration", "err", err)
+5
-4
appview/labels/labels.go
+5
-4
appview/labels/labels.go
···
16
16
"tangled.org/core/appview/oauth"
17
17
"tangled.org/core/appview/pages"
18
18
"tangled.org/core/appview/validator"
19
+
"tangled.org/core/orm"
19
20
"tangled.org/core/rbac"
20
21
"tangled.org/core/tid"
21
22
···
88
89
repoAt := r.Form.Get("repo")
89
90
subjectUri := r.Form.Get("subject")
90
91
91
-
repo, err := db.GetRepo(l.db, db.FilterEq("at_uri", repoAt))
92
+
repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt))
92
93
if err != nil {
93
94
fail("Failed to get repository.", err)
94
95
return
95
96
}
96
97
97
98
// find all the labels that this repo subscribes to
98
-
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
99
+
repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt))
99
100
if err != nil {
100
101
fail("Failed to get labels for this repository.", err)
101
102
return
···
106
107
labelAts = append(labelAts, rl.LabelAt.String())
107
108
}
108
109
109
-
actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts))
110
+
actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts))
110
111
if err != nil {
111
112
fail("Invalid form data.", err)
112
113
return
113
114
}
114
115
115
116
// calculate the start state by applying already known labels
116
-
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
117
+
existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri))
117
118
if err != nil {
118
119
fail("Invalid form data.", err)
119
120
return
+67
appview/mentions/resolver.go
+67
appview/mentions/resolver.go
···
1
+
package mentions
2
+
3
+
import (
4
+
"context"
5
+
"log/slog"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"tangled.org/core/appview/config"
9
+
"tangled.org/core/appview/db"
10
+
"tangled.org/core/appview/models"
11
+
"tangled.org/core/appview/pages/markup"
12
+
"tangled.org/core/idresolver"
13
+
)
14
+
15
+
type Resolver struct {
16
+
config *config.Config
17
+
idResolver *idresolver.Resolver
18
+
execer db.Execer
19
+
logger *slog.Logger
20
+
}
21
+
22
+
func New(
23
+
config *config.Config,
24
+
idResolver *idresolver.Resolver,
25
+
execer db.Execer,
26
+
logger *slog.Logger,
27
+
) *Resolver {
28
+
return &Resolver{
29
+
config,
30
+
idResolver,
31
+
execer,
32
+
logger,
33
+
}
34
+
}
35
+
36
+
func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) {
37
+
l := r.logger.With("method", "Resolve")
38
+
39
+
rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source)
40
+
l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs)
41
+
42
+
idents := r.idResolver.ResolveIdents(ctx, rawMentions)
43
+
var mentions []syntax.DID
44
+
for _, ident := range idents {
45
+
if ident != nil && !ident.Handle.IsInvalidHandle() {
46
+
mentions = append(mentions, ident.DID)
47
+
}
48
+
}
49
+
l.Debug("found mentions", "mentions", mentions)
50
+
51
+
var resolvedRefs []models.ReferenceLink
52
+
for _, rawRef := range rawRefs {
53
+
ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle)
54
+
if err != nil || ident == nil || ident.Handle.IsInvalidHandle() {
55
+
continue
56
+
}
57
+
rawRef.Handle = string(ident.DID)
58
+
resolvedRefs = append(resolvedRefs, rawRef)
59
+
}
60
+
aturiRefs, err := db.ValidateReferenceLinks(r.execer, resolvedRefs)
61
+
if err != nil {
62
+
l.Error("failed running query", "err", err)
63
+
}
64
+
l.Debug("found references", "refs", aturiRefs)
65
+
66
+
return mentions, aturiRefs
67
+
}
+5
-4
appview/middleware/middleware.go
+5
-4
appview/middleware/middleware.go
···
18
18
"tangled.org/core/appview/pagination"
19
19
"tangled.org/core/appview/reporesolver"
20
20
"tangled.org/core/idresolver"
21
+
"tangled.org/core/orm"
21
22
"tangled.org/core/rbac"
22
23
)
23
24
···
164
165
ok, err := mw.enforcer.E.Enforce(actor.Did, f.Knot, f.DidSlashRepo(), requiredPerm)
165
166
if err != nil || !ok {
166
167
// we need a logged in user
167
-
log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.OwnerSlashRepo())
168
+
log.Printf("%s does not have perms of a %s in repo %s", actor.Did, requiredPerm, f.DidSlashRepo())
168
169
http.Error(w, "Forbiden", http.StatusUnauthorized)
169
170
return
170
171
}
···
217
218
218
219
repo, err := db.GetRepo(
219
220
mw.db,
220
-
db.FilterEq("did", id.DID.String()),
221
-
db.FilterEq("name", repoName),
221
+
orm.FilterEq("did", id.DID.String()),
222
+
orm.FilterEq("name", repoName),
222
223
)
223
224
if err != nil {
224
225
log.Println("failed to resolve repo", "err", err)
···
327
328
return
328
329
}
329
330
330
-
fullName := f.OwnerHandle() + "/" + f.Name
331
+
fullName := reporesolver.GetBaseRepoPath(r, f)
331
332
332
333
if r.Header.Get("User-Agent") == "Go-http-client/1.1" {
333
334
if r.URL.Query().Get("go-get") == "1" {
+70
-34
appview/models/issue.go
+70
-34
appview/models/issue.go
···
10
10
)
11
11
12
12
type Issue struct {
13
-
Id int64
14
-
Did string
15
-
Rkey string
16
-
RepoAt syntax.ATURI
17
-
IssueId int
18
-
Created time.Time
19
-
Edited *time.Time
20
-
Deleted *time.Time
21
-
Title string
22
-
Body string
23
-
Open bool
13
+
Id int64
14
+
Did string
15
+
Rkey string
16
+
RepoAt syntax.ATURI
17
+
IssueId int
18
+
Created time.Time
19
+
Edited *time.Time
20
+
Deleted *time.Time
21
+
Title string
22
+
Body string
23
+
Open bool
24
+
Mentions []syntax.DID
25
+
References []syntax.ATURI
24
26
25
27
// optionally, populate this when querying for reverse mappings
26
28
// like comment counts, parent repo etc.
···
34
36
}
35
37
36
38
func (i *Issue) AsRecord() tangled.RepoIssue {
39
+
mentions := make([]string, len(i.Mentions))
40
+
for i, did := range i.Mentions {
41
+
mentions[i] = string(did)
42
+
}
43
+
references := make([]string, len(i.References))
44
+
for i, uri := range i.References {
45
+
references[i] = string(uri)
46
+
}
37
47
return tangled.RepoIssue{
38
-
Repo: i.RepoAt.String(),
39
-
Title: i.Title,
40
-
Body: &i.Body,
41
-
CreatedAt: i.Created.Format(time.RFC3339),
48
+
Repo: i.RepoAt.String(),
49
+
Title: i.Title,
50
+
Body: &i.Body,
51
+
Mentions: mentions,
52
+
References: references,
53
+
CreatedAt: i.Created.Format(time.RFC3339),
42
54
}
43
55
}
44
56
···
161
173
}
162
174
163
175
type IssueComment struct {
164
-
Id int64
165
-
Did string
166
-
Rkey string
167
-
IssueAt string
168
-
ReplyTo *string
169
-
Body string
170
-
Created time.Time
171
-
Edited *time.Time
172
-
Deleted *time.Time
176
+
Id int64
177
+
Did string
178
+
Rkey string
179
+
IssueAt string
180
+
ReplyTo *string
181
+
Body string
182
+
Created time.Time
183
+
Edited *time.Time
184
+
Deleted *time.Time
185
+
Mentions []syntax.DID
186
+
References []syntax.ATURI
173
187
}
174
188
175
189
func (i *IssueComment) AtUri() syntax.ATURI {
···
177
191
}
178
192
179
193
func (i *IssueComment) AsRecord() tangled.RepoIssueComment {
194
+
mentions := make([]string, len(i.Mentions))
195
+
for i, did := range i.Mentions {
196
+
mentions[i] = string(did)
197
+
}
198
+
references := make([]string, len(i.References))
199
+
for i, uri := range i.References {
200
+
references[i] = string(uri)
201
+
}
180
202
return tangled.RepoIssueComment{
181
-
Body: i.Body,
182
-
Issue: i.IssueAt,
183
-
CreatedAt: i.Created.Format(time.RFC3339),
184
-
ReplyTo: i.ReplyTo,
203
+
Body: i.Body,
204
+
Issue: i.IssueAt,
205
+
CreatedAt: i.Created.Format(time.RFC3339),
206
+
ReplyTo: i.ReplyTo,
207
+
Mentions: mentions,
208
+
References: references,
185
209
}
186
210
}
187
211
···
205
229
return nil, err
206
230
}
207
231
232
+
i := record
233
+
mentions := make([]syntax.DID, len(record.Mentions))
234
+
for i, did := range record.Mentions {
235
+
mentions[i] = syntax.DID(did)
236
+
}
237
+
references := make([]syntax.ATURI, len(record.References))
238
+
for i, uri := range i.References {
239
+
references[i] = syntax.ATURI(uri)
240
+
}
241
+
208
242
comment := IssueComment{
209
-
Did: ownerDid,
210
-
Rkey: rkey,
211
-
Body: record.Body,
212
-
IssueAt: record.Issue,
213
-
ReplyTo: record.ReplyTo,
214
-
Created: created,
243
+
Did: ownerDid,
244
+
Rkey: rkey,
245
+
Body: record.Body,
246
+
IssueAt: record.Issue,
247
+
ReplyTo: record.ReplyTo,
248
+
Created: created,
249
+
Mentions: mentions,
250
+
References: references,
215
251
}
216
252
217
253
return &comment, nil
+4
-1
appview/models/profile.go
+4
-1
appview/models/profile.go
···
13
13
Did string
14
14
15
15
// data
16
+
Avatar string // CID of the avatar blob
16
17
Description string
17
18
IncludeBluesky bool
18
19
Location string
···
111
112
}
112
113
113
114
type ByMonth struct {
115
+
Commits int
114
116
RepoEvents []RepoEvent
115
117
IssueEvents IssueEvents
116
118
PullEvents PullEvents
···
119
121
func (b ByMonth) IsEmpty() bool {
120
122
return len(b.RepoEvents) == 0 &&
121
123
len(b.IssueEvents.Items) == 0 &&
122
-
len(b.PullEvents.Items) == 0
124
+
len(b.PullEvents.Items) == 0 &&
125
+
b.Commits == 0
123
126
}
124
127
125
128
type IssueEvents struct {
+41
-3
appview/models/pull.go
+41
-3
appview/models/pull.go
···
66
66
TargetBranch string
67
67
State PullState
68
68
Submissions []*PullSubmission
69
+
Mentions []syntax.DID
70
+
References []syntax.ATURI
69
71
70
72
// stacking
71
73
StackId string // nullable string
···
92
94
source.Repo = &s
93
95
}
94
96
}
97
+
mentions := make([]string, len(p.Mentions))
98
+
for i, did := range p.Mentions {
99
+
mentions[i] = string(did)
100
+
}
101
+
references := make([]string, len(p.References))
102
+
for i, uri := range p.References {
103
+
references[i] = string(uri)
104
+
}
95
105
96
106
record := tangled.RepoPull{
97
-
Title: p.Title,
98
-
Body: &p.Body,
99
-
CreatedAt: p.Created.Format(time.RFC3339),
107
+
Title: p.Title,
108
+
Body: &p.Body,
109
+
Mentions: mentions,
110
+
References: references,
111
+
CreatedAt: p.Created.Format(time.RFC3339),
100
112
Target: &tangled.RepoPull_Target{
101
113
Repo: p.RepoAt.String(),
102
114
Branch: p.TargetBranch,
···
146
158
147
159
// content
148
160
Body string
161
+
162
+
// meta
163
+
Mentions []syntax.DID
164
+
References []syntax.ATURI
149
165
150
166
// meta
151
167
Created time.Time
152
168
}
169
+
170
+
func (p *PullComment) AtUri() syntax.ATURI {
171
+
return syntax.ATURI(p.CommentAt)
172
+
}
173
+
174
+
// func (p *PullComment) AsRecord() tangled.RepoPullComment {
175
+
// mentions := make([]string, len(p.Mentions))
176
+
// for i, did := range p.Mentions {
177
+
// mentions[i] = string(did)
178
+
// }
179
+
// references := make([]string, len(p.References))
180
+
// for i, uri := range p.References {
181
+
// references[i] = string(uri)
182
+
// }
183
+
// return tangled.RepoPullComment{
184
+
// Pull: p.PullAt,
185
+
// Body: p.Body,
186
+
// Mentions: mentions,
187
+
// References: references,
188
+
// CreatedAt: p.Created.Format(time.RFC3339),
189
+
// }
190
+
// }
153
191
154
192
func (p *Pull) LastRoundNumber() int {
155
193
return len(p.Submissions) - 1
+49
appview/models/reference.go
+49
appview/models/reference.go
···
1
+
package models
2
+
3
+
import "fmt"
4
+
5
+
type RefKind int
6
+
7
+
const (
8
+
RefKindIssue RefKind = iota
9
+
RefKindPull
10
+
)
11
+
12
+
func (k RefKind) String() string {
13
+
if k == RefKindIssue {
14
+
return "issues"
15
+
} else {
16
+
return "pulls"
17
+
}
18
+
}
19
+
20
+
// /@alice.com/cool-proj/issues/123
21
+
// /@alice.com/cool-proj/issues/123#comment-321
22
+
type ReferenceLink struct {
23
+
Handle string
24
+
Repo string
25
+
Kind RefKind
26
+
SubjectId int
27
+
CommentId *int
28
+
}
29
+
30
+
func (l ReferenceLink) String() string {
31
+
comment := ""
32
+
if l.CommentId != nil {
33
+
comment = fmt.Sprintf("#comment-%d", *l.CommentId)
34
+
}
35
+
return fmt.Sprintf("/%s/%s/%s/%d%s",
36
+
l.Handle,
37
+
l.Repo,
38
+
l.Kind.String(),
39
+
l.SubjectId,
40
+
comment,
41
+
)
42
+
}
43
+
44
+
type RichReferenceLink struct {
45
+
ReferenceLink
46
+
Title string
47
+
// reusing PullState for both issue & PR
48
+
State PullState
49
+
}
+14
-5
appview/models/star.go
+14
-5
appview/models/star.go
···
7
7
)
8
8
9
9
type Star struct {
10
-
StarredByDid string
11
-
RepoAt syntax.ATURI
12
-
Created time.Time
13
-
Rkey string
10
+
Did string
11
+
RepoAt syntax.ATURI
12
+
Created time.Time
13
+
Rkey string
14
+
}
14
15
15
-
// optionally, populate this when querying for reverse mappings
16
+
// RepoStar is used for reverse mapping to repos
17
+
type RepoStar struct {
18
+
Star
16
19
Repo *Repo
17
20
}
21
+
22
+
// StringStar is used for reverse mapping to strings
23
+
type StringStar struct {
24
+
Star
25
+
String *String
26
+
}
+1
-1
appview/models/string.go
+1
-1
appview/models/string.go
+1
-1
appview/models/timeline.go
+1
-1
appview/models/timeline.go
+5
-4
appview/notifications/notifications.go
+5
-4
appview/notifications/notifications.go
···
11
11
"tangled.org/core/appview/oauth"
12
12
"tangled.org/core/appview/pages"
13
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
14
15
)
15
16
16
17
type Notifications struct {
···
53
54
54
55
total, err := db.CountNotifications(
55
56
n.db,
56
-
db.FilterEq("recipient_did", user.Did),
57
+
orm.FilterEq("recipient_did", user.Did),
57
58
)
58
59
if err != nil {
59
60
l.Error("failed to get total notifications", "err", err)
···
64
65
notifications, err := db.GetNotificationsWithEntities(
65
66
n.db,
66
67
page,
67
-
db.FilterEq("recipient_did", user.Did),
68
+
orm.FilterEq("recipient_did", user.Did),
68
69
)
69
70
if err != nil {
70
71
l.Error("failed to get notifications", "err", err)
···
96
97
97
98
count, err := db.CountNotifications(
98
99
n.db,
99
-
db.FilterEq("recipient_did", user.Did),
100
-
db.FilterEq("read", 0),
100
+
orm.FilterEq("recipient_did", user.Did),
101
+
orm.FilterEq("read", 0),
101
102
)
102
103
if err != nil {
103
104
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
+17
-11
appview/notify/db/db.go
+17
-11
appview/notify/db/db.go
···
7
7
"slices"
8
8
9
9
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
"tangled.org/core/api/tangled"
10
11
"tangled.org/core/appview/db"
11
12
"tangled.org/core/appview/models"
12
13
"tangled.org/core/appview/notify"
13
14
"tangled.org/core/idresolver"
15
+
"tangled.org/core/orm"
14
16
)
15
17
16
18
const (
···
36
38
}
37
39
38
40
func (n *databaseNotifier) NewStar(ctx context.Context, star *models.Star) {
41
+
if star.RepoAt.Collection().String() != tangled.RepoNSID {
42
+
// skip string stars for now
43
+
return
44
+
}
39
45
var err error
40
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
46
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(star.RepoAt)))
41
47
if err != nil {
42
48
log.Printf("NewStar: failed to get repos: %v", err)
43
49
return
44
50
}
45
51
46
-
actorDid := syntax.DID(star.StarredByDid)
52
+
actorDid := syntax.DID(star.Did)
47
53
recipients := []syntax.DID{syntax.DID(repo.Did)}
48
54
eventType := models.NotificationTypeRepoStarred
49
55
entityType := "repo"
···
75
81
// - collaborators in the repo
76
82
var recipients []syntax.DID
77
83
recipients = append(recipients, syntax.DID(issue.Repo.Did))
78
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
84
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
79
85
if err != nil {
80
86
log.Printf("failed to fetch collaborators: %v", err)
81
87
return
···
114
120
}
115
121
116
122
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
117
-
issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt))
123
+
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
118
124
if err != nil {
119
125
log.Printf("NewIssueComment: failed to get issues: %v", err)
120
126
return
···
202
208
}
203
209
204
210
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
205
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
211
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
206
212
if err != nil {
207
213
log.Printf("NewPull: failed to get repos: %v", err)
208
214
return
···
213
219
// - collaborators in the repo
214
220
var recipients []syntax.DID
215
221
recipients = append(recipients, syntax.DID(repo.Did))
216
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
222
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
217
223
if err != nil {
218
224
log.Printf("failed to fetch collaborators: %v", err)
219
225
return
···
253
259
return
254
260
}
255
261
256
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
262
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt))
257
263
if err != nil {
258
264
log.Printf("NewPullComment: failed to get repos: %v", err)
259
265
return
···
322
328
// - all issue participants
323
329
var recipients []syntax.DID
324
330
recipients = append(recipients, syntax.DID(issue.Repo.Did))
325
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
331
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
326
332
if err != nil {
327
333
log.Printf("failed to fetch collaborators: %v", err)
328
334
return
···
361
367
362
368
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
363
369
// Get repo details
364
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
370
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
365
371
if err != nil {
366
372
log.Printf("NewPullState: failed to get repos: %v", err)
367
373
return
···
372
378
// - all pull participants
373
379
var recipients []syntax.DID
374
380
recipients = append(recipients, syntax.DID(repo.Did))
375
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
381
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
376
382
if err != nil {
377
383
log.Printf("failed to fetch collaborators: %v", err)
378
384
return
···
438
444
439
445
prefMap, err := db.GetNotificationPreferences(
440
446
n.db,
441
-
db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
447
+
orm.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
442
448
)
443
449
if err != nil {
444
450
// failed to get prefs for users
+2
-2
appview/notify/posthog/notifier.go
+2
-2
appview/notify/posthog/notifier.go
···
37
37
38
38
func (n *posthogNotifier) NewStar(ctx context.Context, star *models.Star) {
39
39
err := n.client.Enqueue(posthog.Capture{
40
-
DistinctId: star.StarredByDid,
40
+
DistinctId: star.Did,
41
41
Event: "star",
42
42
Properties: posthog.Properties{"repo_at": star.RepoAt.String()},
43
43
})
···
48
48
49
49
func (n *posthogNotifier) DeleteStar(ctx context.Context, star *models.Star) {
50
50
err := n.client.Enqueue(posthog.Capture{
51
-
DistinctId: star.StarredByDid,
51
+
DistinctId: star.Did,
52
52
Event: "unstar",
53
53
Properties: posthog.Properties{"repo_at": star.RepoAt.String()},
54
54
})
+3
-2
appview/oauth/handler.go
+3
-2
appview/oauth/handler.go
···
16
16
"tangled.org/core/api/tangled"
17
17
"tangled.org/core/appview/db"
18
18
"tangled.org/core/consts"
19
+
"tangled.org/core/orm"
19
20
"tangled.org/core/tid"
20
21
)
21
22
···
97
98
// and create an sh.tangled.spindle.member record with that
98
99
spindleMembers, err := db.GetSpindleMembers(
99
100
o.Db,
100
-
db.FilterEq("instance", "spindle.tangled.sh"),
101
-
db.FilterEq("subject", did),
101
+
orm.FilterEq("instance", "spindle.tangled.sh"),
102
+
orm.FilterEq("subject", did),
102
103
)
103
104
if err != nil {
104
105
l.Error("failed to get spindle members", "err", err)
+15
-2
appview/oauth/oauth.go
+15
-2
appview/oauth/oauth.go
···
202
202
exp int64
203
203
lxm string
204
204
dev bool
205
+
timeout time.Duration
205
206
}
206
207
207
208
type ServiceClientOpt func(*ServiceClientOpts)
209
+
210
+
func DefaultServiceClientOpts() ServiceClientOpts {
211
+
return ServiceClientOpts{
212
+
timeout: time.Second * 5,
213
+
}
214
+
}
208
215
209
216
func WithService(service string) ServiceClientOpt {
210
217
return func(s *ServiceClientOpts) {
···
233
240
}
234
241
}
235
242
243
+
func WithTimeout(timeout time.Duration) ServiceClientOpt {
244
+
return func(s *ServiceClientOpts) {
245
+
s.timeout = timeout
246
+
}
247
+
}
248
+
236
249
func (s *ServiceClientOpts) Audience() string {
237
250
return fmt.Sprintf("did:web:%s", s.service)
238
251
}
···
247
260
}
248
261
249
262
func (o *OAuth) ServiceClient(r *http.Request, os ...ServiceClientOpt) (*xrpc.Client, error) {
250
-
opts := ServiceClientOpts{}
263
+
opts := DefaultServiceClientOpts()
251
264
for _, o := range os {
252
265
o(&opts)
253
266
}
···
274
287
},
275
288
Host: opts.Host(),
276
289
Client: &http.Client{
277
-
Timeout: time.Second * 5,
290
+
Timeout: opts.timeout,
278
291
},
279
292
}, nil
280
293
}
+59
-9
appview/pages/funcmap.go
+59
-9
appview/pages/funcmap.go
···
22
22
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
23
23
"github.com/alecthomas/chroma/v2/lexers"
24
24
"github.com/alecthomas/chroma/v2/styles"
25
-
"github.com/bluesky-social/indigo/atproto/syntax"
26
25
"github.com/dustin/go-humanize"
27
26
"github.com/go-enry/go-enry/v2"
28
27
"github.com/yuin/goldmark"
29
28
"tangled.org/core/appview/filetree"
29
+
"tangled.org/core/appview/models"
30
30
"tangled.org/core/appview/pages/markup"
31
31
"tangled.org/core/crypto"
32
32
)
···
72
72
73
73
return identity.Handle.String()
74
74
},
75
+
"ownerSlashRepo": func(repo *models.Repo) string {
76
+
ownerId, err := p.resolver.ResolveIdent(context.Background(), repo.Did)
77
+
if err != nil {
78
+
return repo.DidSlashRepo()
79
+
}
80
+
handle := ownerId.Handle
81
+
if handle != "" && !handle.IsInvalidHandle() {
82
+
return string(handle) + "/" + repo.Name
83
+
}
84
+
return repo.DidSlashRepo()
85
+
},
75
86
"truncateAt30": func(s string) string {
76
87
if len(s) <= 30 {
77
88
return s
···
100
111
"sub": func(a, b int) int {
101
112
return a - b
102
113
},
114
+
"mul": func(a, b int) int {
115
+
return a * b
116
+
},
117
+
"div": func(a, b int) int {
118
+
return a / b
119
+
},
120
+
"mod": func(a, b int) int {
121
+
return a % b
122
+
},
103
123
"f64": func(a int) float64 {
104
124
return float64(a)
105
125
},
···
132
152
133
153
return b
134
154
},
135
-
"didOrHandle": func(did, handle string) string {
136
-
if handle != "" && handle != syntax.HandleInvalid.String() {
137
-
return handle
138
-
} else {
139
-
return did
140
-
}
141
-
},
142
155
"assoc": func(values ...string) ([][]string, error) {
143
156
if len(values)%2 != 0 {
144
157
return nil, fmt.Errorf("invalid assoc call, must have an even number of arguments")
···
149
162
}
150
163
return pairs, nil
151
164
},
152
-
"append": func(s []string, values ...string) []string {
165
+
"append": func(s []any, values ...any) []any {
153
166
s = append(s, values...)
154
167
return s
155
168
},
···
347
360
"fullAvatar": func(handle string) string {
348
361
return p.AvatarUrl(handle, "")
349
362
},
363
+
"placeholderAvatar": func(size string) template.HTML {
364
+
sizeClass := "size-6"
365
+
iconSize := "size-4"
366
+
if size == "tiny" {
367
+
sizeClass = "size-6"
368
+
iconSize = "size-4"
369
+
} else if size == "small" {
370
+
sizeClass = "size-8"
371
+
iconSize = "size-5"
372
+
} else {
373
+
sizeClass = "size-12"
374
+
iconSize = "size-8"
375
+
}
376
+
icon, _ := p.icon("user-round", []string{iconSize, "text-gray-400", "dark:text-gray-500"})
377
+
return template.HTML(fmt.Sprintf(`<div class="%s rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">%s</div>`, sizeClass, icon))
378
+
},
379
+
"profileAvatarUrl": func(profile *models.Profile, size string) string {
380
+
if profile != nil {
381
+
return p.AvatarUrl(profile.Did, size)
382
+
}
383
+
return ""
384
+
},
350
385
"langColor": enry.GetColor,
351
386
"layoutSide": func() string {
352
387
return "col-span-1 md:col-span-2 lg:col-span-3"
···
370
405
}
371
406
}
372
407
408
+
func (p *Pages) resolveDid(did string) string {
409
+
identity, err := p.resolver.ResolveIdent(context.Background(), did)
410
+
411
+
if err != nil {
412
+
return did
413
+
}
414
+
415
+
if identity.Handle.IsInvalidHandle() {
416
+
return "handle.invalid"
417
+
}
418
+
419
+
return identity.Handle.String()
420
+
}
421
+
373
422
func (p *Pages) AvatarUrl(handle, size string) string {
374
423
handle = strings.TrimPrefix(handle, "@")
424
+
handle = p.resolveDid(handle)
375
425
376
426
secret := p.avatar.SharedSecret
377
427
h := hmac.New(sha256.New, []byte(secret))
+1
-1
appview/pages/markup/extension/atlink.go
+1
-1
appview/pages/markup/extension/atlink.go
+1
-27
appview/pages/markup/markdown.go
+1
-27
appview/pages/markup/markdown.go
···
12
12
13
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
14
"github.com/alecthomas/chroma/v2/styles"
15
-
treeblood "github.com/wyatt915/goldmark-treeblood"
16
15
"github.com/yuin/goldmark"
17
16
highlighting "github.com/yuin/goldmark-highlighting/v2"
18
17
"github.com/yuin/goldmark/ast"
···
65
64
extension.NewFootnote(
66
65
extension.WithFootnoteIDPrefix([]byte("footnote")),
67
66
),
68
-
treeblood.MathML(),
69
67
callout.CalloutExtention,
70
68
textension.AtExt,
71
69
),
···
249
247
repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name)
250
248
251
249
query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true",
252
-
url.PathEscape(repoName), url.PathEscape(rctx.RepoInfo.Ref), actualPath)
250
+
url.QueryEscape(repoName), url.QueryEscape(rctx.RepoInfo.Ref), actualPath)
253
251
254
252
parsedURL := &url.URL{
255
253
Scheme: scheme,
···
302
300
}
303
301
304
302
return path.Join(rctx.CurrentDir, dst)
305
-
}
306
-
307
-
// FindUserMentions returns Set of user handles from given markup soruce.
308
-
// It doesn't guarntee unique DIDs
309
-
func FindUserMentions(source string) []string {
310
-
var (
311
-
mentions []string
312
-
mentionsSet = make(map[string]struct{})
313
-
md = NewMarkdown()
314
-
sourceBytes = []byte(source)
315
-
root = md.Parser().Parse(text.NewReader(sourceBytes))
316
-
)
317
-
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
318
-
if entering && n.Kind() == textension.KindAt {
319
-
handle := n.(*textension.AtNode).Handle
320
-
mentionsSet[handle] = struct{}{}
321
-
return ast.WalkSkipChildren, nil
322
-
}
323
-
return ast.WalkContinue, nil
324
-
})
325
-
for handle := range mentionsSet {
326
-
mentions = append(mentions, handle)
327
-
}
328
-
return mentions
329
303
}
330
304
331
305
func isAbsoluteUrl(link string) bool {
+124
appview/pages/markup/reference_link.go
+124
appview/pages/markup/reference_link.go
···
1
+
package markup
2
+
3
+
import (
4
+
"maps"
5
+
"net/url"
6
+
"path"
7
+
"slices"
8
+
"strconv"
9
+
"strings"
10
+
11
+
"github.com/yuin/goldmark/ast"
12
+
"github.com/yuin/goldmark/text"
13
+
"tangled.org/core/appview/models"
14
+
textension "tangled.org/core/appview/pages/markup/extension"
15
+
)
16
+
17
+
// FindReferences collects all links referencing tangled-related objects
18
+
// like issues, PRs, comments or even @-mentions
19
+
// This funciton doesn't actually check for the existence of records in the DB
20
+
// or the PDS; it merely returns a list of what are presumed to be references.
21
+
func FindReferences(baseUrl string, source string) ([]string, []models.ReferenceLink) {
22
+
var (
23
+
refLinkSet = make(map[models.ReferenceLink]struct{})
24
+
mentionsSet = make(map[string]struct{})
25
+
md = NewMarkdown()
26
+
sourceBytes = []byte(source)
27
+
root = md.Parser().Parse(text.NewReader(sourceBytes))
28
+
)
29
+
// trim url scheme. the SSL shouldn't matter
30
+
baseUrl = strings.TrimPrefix(baseUrl, "https://")
31
+
baseUrl = strings.TrimPrefix(baseUrl, "http://")
32
+
33
+
ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
34
+
if !entering {
35
+
return ast.WalkContinue, nil
36
+
}
37
+
switch n.Kind() {
38
+
case textension.KindAt:
39
+
handle := n.(*textension.AtNode).Handle
40
+
mentionsSet[handle] = struct{}{}
41
+
return ast.WalkSkipChildren, nil
42
+
case ast.KindLink:
43
+
dest := string(n.(*ast.Link).Destination)
44
+
ref := parseTangledLink(baseUrl, dest)
45
+
if ref != nil {
46
+
refLinkSet[*ref] = struct{}{}
47
+
}
48
+
return ast.WalkSkipChildren, nil
49
+
case ast.KindAutoLink:
50
+
an := n.(*ast.AutoLink)
51
+
if an.AutoLinkType == ast.AutoLinkURL {
52
+
dest := string(an.URL(sourceBytes))
53
+
ref := parseTangledLink(baseUrl, dest)
54
+
if ref != nil {
55
+
refLinkSet[*ref] = struct{}{}
56
+
}
57
+
}
58
+
return ast.WalkSkipChildren, nil
59
+
}
60
+
return ast.WalkContinue, nil
61
+
})
62
+
mentions := slices.Collect(maps.Keys(mentionsSet))
63
+
references := slices.Collect(maps.Keys(refLinkSet))
64
+
return mentions, references
65
+
}
66
+
67
+
func parseTangledLink(baseHost string, urlStr string) *models.ReferenceLink {
68
+
u, err := url.Parse(urlStr)
69
+
if err != nil {
70
+
return nil
71
+
}
72
+
73
+
if u.Host != "" && !strings.EqualFold(u.Host, baseHost) {
74
+
return nil
75
+
}
76
+
77
+
p := path.Clean(u.Path)
78
+
parts := strings.FieldsFunc(p, func(r rune) bool { return r == '/' })
79
+
if len(parts) < 4 {
80
+
// need at least: handle / repo / kind / id
81
+
return nil
82
+
}
83
+
84
+
var (
85
+
handle = parts[0]
86
+
repo = parts[1]
87
+
kindSeg = parts[2]
88
+
subjectSeg = parts[3]
89
+
)
90
+
91
+
handle = strings.TrimPrefix(handle, "@")
92
+
93
+
var kind models.RefKind
94
+
switch kindSeg {
95
+
case "issues":
96
+
kind = models.RefKindIssue
97
+
case "pulls":
98
+
kind = models.RefKindPull
99
+
default:
100
+
return nil
101
+
}
102
+
103
+
subjectId, err := strconv.Atoi(subjectSeg)
104
+
if err != nil {
105
+
return nil
106
+
}
107
+
var commentId *int
108
+
if u.Fragment != "" {
109
+
if strings.HasPrefix(u.Fragment, "comment-") {
110
+
commentIdStr := u.Fragment[len("comment-"):]
111
+
if id, err := strconv.Atoi(commentIdStr); err == nil {
112
+
commentId = &id
113
+
}
114
+
}
115
+
}
116
+
117
+
return &models.ReferenceLink{
118
+
Handle: handle,
119
+
Repo: repo,
120
+
Kind: kind,
121
+
SubjectId: subjectId,
122
+
CommentId: commentId,
123
+
}
124
+
}
+42
appview/pages/markup/reference_link_test.go
+42
appview/pages/markup/reference_link_test.go
···
1
+
package markup_test
2
+
3
+
import (
4
+
"testing"
5
+
6
+
"github.com/stretchr/testify/assert"
7
+
"tangled.org/core/appview/models"
8
+
"tangled.org/core/appview/pages/markup"
9
+
)
10
+
11
+
func TestMarkupParsing(t *testing.T) {
12
+
tests := []struct {
13
+
name string
14
+
source string
15
+
wantHandles []string
16
+
wantRefLinks []models.ReferenceLink
17
+
}{
18
+
{
19
+
name: "normal link",
20
+
source: `[link](http://127.0.0.1:3000/alice.pds.tngl.boltless.dev/coolproj/issues/1)`,
21
+
wantHandles: make([]string, 0),
22
+
wantRefLinks: []models.ReferenceLink{
23
+
{Handle: "alice.pds.tngl.boltless.dev", Repo: "coolproj", Kind: models.RefKindIssue, SubjectId: 1, CommentId: nil},
24
+
},
25
+
},
26
+
{
27
+
name: "commonmark style autolink",
28
+
source: `<http://127.0.0.1:3000/alice.pds.tngl.boltless.dev/coolproj/issues/1>`,
29
+
wantHandles: make([]string, 0),
30
+
wantRefLinks: []models.ReferenceLink{
31
+
{Handle: "alice.pds.tngl.boltless.dev", Repo: "coolproj", Kind: models.RefKindIssue, SubjectId: 1, CommentId: nil},
32
+
},
33
+
},
34
+
}
35
+
for _, tt := range tests {
36
+
t.Run(tt.name, func(t *testing.T) {
37
+
handles, refLinks := markup.FindReferences("http://127.0.0.1:3000", tt.source)
38
+
assert.ElementsMatch(t, tt.wantHandles, handles)
39
+
assert.ElementsMatch(t, tt.wantRefLinks, refLinks)
40
+
})
41
+
}
42
+
}
+29
-18
appview/pages/pages.go
+29
-18
appview/pages/pages.go
···
31
31
"github.com/bluesky-social/indigo/atproto/identity"
32
32
"github.com/bluesky-social/indigo/atproto/syntax"
33
33
"github.com/go-git/go-git/v5/plumbing"
34
-
"github.com/go-git/go-git/v5/plumbing/object"
35
34
)
36
35
37
36
//go:embed templates/* static legal
···
407
406
type KnotsParams struct {
408
407
LoggedInUser *oauth.User
409
408
Registrations []models.Registration
409
+
Tabs []map[string]any
410
+
Tab string
410
411
}
411
412
412
413
func (p *Pages) Knots(w io.Writer, params KnotsParams) error {
···
419
420
Members []string
420
421
Repos map[string][]models.Repo
421
422
IsOwner bool
423
+
Tabs []map[string]any
424
+
Tab string
422
425
}
423
426
424
427
func (p *Pages) Knot(w io.Writer, params KnotParams) error {
···
436
439
type SpindlesParams struct {
437
440
LoggedInUser *oauth.User
438
441
Spindles []models.Spindle
442
+
Tabs []map[string]any
443
+
Tab string
439
444
}
440
445
441
446
func (p *Pages) Spindles(w io.Writer, params SpindlesParams) error {
···
444
449
445
450
type SpindleListingParams struct {
446
451
models.Spindle
452
+
Tabs []map[string]any
453
+
Tab string
447
454
}
448
455
449
456
func (p *Pages) SpindleListing(w io.Writer, params SpindleListingParams) error {
···
455
462
Spindle models.Spindle
456
463
Members []string
457
464
Repos map[string][]models.Repo
465
+
Tabs []map[string]any
466
+
Tab string
458
467
}
459
468
460
469
func (p *Pages) SpindleDashboard(w io.Writer, params SpindleDashboardParams) error {
···
482
491
483
492
type ProfileCard struct {
484
493
UserDid string
485
-
UserHandle string
486
494
FollowStatus models.FollowStatus
487
495
Punchcard *models.Punchcard
488
496
Profile *models.Profile
···
625
633
return p.executePlain("user/fragments/editPins", w, params)
626
634
}
627
635
628
-
type RepoStarFragmentParams struct {
636
+
type StarBtnFragmentParams struct {
629
637
IsStarred bool
630
-
RepoAt syntax.ATURI
631
-
Stats models.RepoStats
638
+
SubjectAt syntax.ATURI
639
+
StarCount int
632
640
}
633
641
634
-
func (p *Pages) RepoStarFragment(w io.Writer, params RepoStarFragmentParams) error {
635
-
return p.executePlain("repo/fragments/repoStar", w, params)
642
+
func (p *Pages) StarBtnFragment(w io.Writer, params StarBtnFragmentParams) error {
643
+
return p.executePlain("fragments/starBtn", w, params)
636
644
}
637
645
638
646
type RepoIndexParams struct {
···
640
648
RepoInfo repoinfo.RepoInfo
641
649
Active string
642
650
TagMap map[string][]string
643
-
CommitsTrunc []*object.Commit
651
+
CommitsTrunc []types.Commit
644
652
TagsTrunc []*types.TagReference
645
653
BranchesTrunc []types.Branch
646
654
// ForkInfo *types.ForkInfo
···
831
839
}
832
840
833
841
type Collaborator struct {
834
-
Did string
835
-
Handle string
836
-
Role string
842
+
Did string
843
+
Role string
837
844
}
838
845
839
846
type RepoSettingsParams struct {
···
908
915
RepoInfo repoinfo.RepoInfo
909
916
Active string
910
917
Issues []models.Issue
918
+
IssueCount int
911
919
LabelDefs map[string]*models.LabelDefinition
912
920
Page pagination.Page
913
921
FilteringByOpen bool
···
925
933
Active string
926
934
Issue *models.Issue
927
935
CommentList []models.CommentListItem
936
+
Backlinks []models.RichReferenceLink
928
937
LabelDefs map[string]*models.LabelDefinition
929
938
930
939
OrderedReactionKinds []models.ReactionKind
···
1078
1087
Pull *models.Pull
1079
1088
Stack models.Stack
1080
1089
AbandonedPulls []*models.Pull
1090
+
Backlinks []models.RichReferenceLink
1081
1091
BranchDeleteStatus *models.BranchDeleteStatus
1082
1092
MergeCheck types.MergeCheckResponse
1083
1093
ResubmitCheck ResubmitResult
···
1249
1259
return p.executePlain("repo/fragments/compareAllowPull", w, params)
1250
1260
}
1251
1261
1252
-
type RepoCompareDiffParams struct {
1253
-
LoggedInUser *oauth.User
1254
-
RepoInfo repoinfo.RepoInfo
1255
-
Diff types.NiceDiff
1262
+
type RepoCompareDiffFragmentParams struct {
1263
+
Diff types.NiceDiff
1264
+
DiffOpts types.DiffOpts
1256
1265
}
1257
1266
1258
-
func (p *Pages) RepoCompareDiff(w io.Writer, params RepoCompareDiffParams) error {
1259
-
return p.executePlain("repo/fragments/diff", w, []any{params.RepoInfo.FullName, ¶ms.Diff})
1267
+
func (p *Pages) RepoCompareDiffFragment(w io.Writer, params RepoCompareDiffFragmentParams) error {
1268
+
return p.executePlain("repo/fragments/diff", w, []any{¶ms.Diff, ¶ms.DiffOpts})
1260
1269
}
1261
1270
1262
1271
type LabelPanelParams struct {
···
1376
1385
ShowRendered bool
1377
1386
RenderToggle bool
1378
1387
RenderedContents template.HTML
1379
-
String models.String
1388
+
String *models.String
1380
1389
Stats models.StringStats
1390
+
IsStarred bool
1391
+
StarCount int
1381
1392
Owner identity.Identity
1382
1393
}
1383
1394
+25
-22
appview/pages/repoinfo/repoinfo.go
+25
-22
appview/pages/repoinfo/repoinfo.go
···
1
1
package repoinfo
2
2
3
3
import (
4
+
"fmt"
4
5
"path"
5
6
"slices"
6
7
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
"tangled.org/core/api/tangled"
8
10
"tangled.org/core/appview/models"
9
11
"tangled.org/core/appview/state/userutil"
10
12
)
11
13
12
-
func (r RepoInfo) Owner() string {
14
+
func (r RepoInfo) owner() string {
13
15
if r.OwnerHandle != "" {
14
16
return r.OwnerHandle
15
17
} else {
···
18
20
}
19
21
20
22
func (r RepoInfo) FullName() string {
21
-
return path.Join(r.Owner(), r.Name)
23
+
return path.Join(r.owner(), r.Name)
22
24
}
23
25
24
-
func (r RepoInfo) OwnerWithoutAt() string {
26
+
func (r RepoInfo) ownerWithoutAt() string {
25
27
if r.OwnerHandle != "" {
26
28
return r.OwnerHandle
27
29
} else {
···
30
32
}
31
33
32
34
func (r RepoInfo) FullNameWithoutAt() string {
33
-
return path.Join(r.OwnerWithoutAt(), r.Name)
35
+
return path.Join(r.ownerWithoutAt(), r.Name)
34
36
}
35
37
36
38
func (r RepoInfo) GetTabs() [][]string {
···
48
50
return tabs
49
51
}
50
52
53
+
func (r RepoInfo) RepoAt() syntax.ATURI {
54
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.OwnerDid, tangled.RepoNSID, r.Rkey))
55
+
}
56
+
51
57
type RepoInfo struct {
52
-
Name string
53
-
Rkey string
54
-
OwnerDid string
55
-
OwnerHandle string
56
-
Description string
57
-
Website string
58
-
Topics []string
59
-
Knot string
60
-
Spindle string
61
-
RepoAt syntax.ATURI
62
-
IsStarred bool
63
-
Stats models.RepoStats
64
-
Roles RolesInRepo
65
-
Source *models.Repo
66
-
SourceHandle string
67
-
Ref string
68
-
DisableFork bool
69
-
CurrentDir string
58
+
Name string
59
+
Rkey string
60
+
OwnerDid string
61
+
OwnerHandle string
62
+
Description string
63
+
Website string
64
+
Topics []string
65
+
Knot string
66
+
Spindle string
67
+
IsStarred bool
68
+
Stats models.RepoStats
69
+
Roles RolesInRepo
70
+
Source *models.Repo
71
+
Ref string
72
+
CurrentDir string
70
73
}
71
74
72
75
// each tab on a repo could have some metadata:
+28
appview/pages/templates/fragments/starBtn.html
+28
appview/pages/templates/fragments/starBtn.html
···
1
+
{{ define "fragments/starBtn" }}
2
+
<button
3
+
id="starBtn"
4
+
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
5
+
data-star-subject-at="{{ .SubjectAt }}"
6
+
{{ if .IsStarred }}
7
+
hx-delete="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}"
8
+
{{ else }}
9
+
hx-post="/star?subject={{ .SubjectAt }}&countHint={{ .StarCount }}"
10
+
{{ end }}
11
+
12
+
hx-trigger="click"
13
+
hx-target="this"
14
+
hx-swap="outerHTML"
15
+
hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'
16
+
hx-disabled-elt="#starBtn"
17
+
>
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
+
{{ .StarCount }}
25
+
</span>
26
+
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
27
+
</button>
28
+
{{ end }}
+8
appview/pages/templates/fragments/tabSelector.html
+8
appview/pages/templates/fragments/tabSelector.html
···
2
2
{{ $name := .Name }}
3
3
{{ $all := .Values }}
4
4
{{ $active := .Active }}
5
+
{{ $include := .Include }}
5
6
<div class="flex justify-between divide-x divide-gray-200 dark:divide-gray-700 rounded border border-gray-200 dark:border-gray-700 overflow-hidden">
6
7
{{ $activeTab := "bg-white dark:bg-gray-700 shadow-sm" }}
7
8
{{ $inactiveTab := "bg-gray-100 dark:bg-gray-800 shadow-inner" }}
8
9
{{ range $index, $value := $all }}
9
10
{{ $isActive := eq $value.Key $active }}
10
11
<a href="?{{ $name }}={{ $value.Key }}"
12
+
{{ if $include }}
13
+
hx-get="?{{ $name }}={{ $value.Key }}"
14
+
hx-include="{{ $include }}"
15
+
hx-push-url="true"
16
+
hx-target="body"
17
+
hx-on:htmx:config-request="if(!event.detail.parameters.q) delete event.detail.parameters.q"
18
+
{{ end }}
11
19
class="p-2 whitespace-nowrap flex justify-center items-center gap-2 text-sm w-full block hover:no-underline text-center {{ if $isActive }} {{$activeTab }} {{ else }} {{ $inactiveTab }} {{ end }}">
12
20
{{ if $value.Icon }}
13
21
{{ i $value.Icon "size-4" }}
+22
appview/pages/templates/fragments/tinyAvatarList.html
+22
appview/pages/templates/fragments/tinyAvatarList.html
···
1
+
{{ define "fragments/tinyAvatarList" }}
2
+
{{ $all := .all }}
3
+
{{ $classes := .classes }}
4
+
{{ $ps := take $all 5 }}
5
+
<div class="inline-flex items-center -space-x-3">
6
+
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
7
+
{{ range $i, $p := $ps }}
8
+
<img
9
+
src="{{ tinyAvatar . }}"
10
+
alt=""
11
+
class="rounded-full size-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0 {{ $classes }}"
12
+
/>
13
+
{{ end }}
14
+
15
+
{{ if gt (len $all) 5 }}
16
+
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
17
+
+{{ sub (len $all) 5 }}
18
+
</span>
19
+
{{ end }}
20
+
</div>
21
+
{{ end }}
22
+
+23
-7
appview/pages/templates/knots/dashboard.html
+23
-7
appview/pages/templates/knots/dashboard.html
···
1
-
{{ define "title" }}{{ .Registration.Domain }} · knots{{ end }}
1
+
{{ define "title" }}{{ .Registration.Domain }} · {{ .Tab }} settings{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="px-6 py-4">
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "knotDash" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "knotDash" }}
20
+
<div>
5
21
<div class="flex justify-between items-center">
6
-
<h1 class="text-xl font-bold dark:text-white">{{ .Registration.Domain }}</h1>
22
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} · {{ .Registration.Domain }}</h2>
7
23
<div id="right-side" class="flex gap-2">
8
24
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
9
25
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Registration.ByDid) }}
···
35
51
</div>
36
52
37
53
{{ if .Members }}
38
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
54
+
<section class="bg-white dark:bg-gray-800 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
39
55
<div class="flex flex-col gap-2">
40
56
{{ block "member" . }} {{ end }}
41
57
</div>
···
79
95
<button
80
96
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
81
97
title="Delete knot"
82
-
hx-delete="/knots/{{ .Domain }}"
98
+
hx-delete="/settings/knots/{{ .Domain }}"
83
99
hx-swap="outerHTML"
84
100
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
85
101
hx-headers='{"shouldRedirect": "true"}'
···
95
111
<button
96
112
class="btn gap-2 group"
97
113
title="Retry knot verification"
98
-
hx-post="/knots/{{ .Domain }}/retry"
114
+
hx-post="/settings/knots/{{ .Domain }}/retry"
99
115
hx-swap="none"
100
116
hx-headers='{"shouldRefresh": "true"}'
101
117
>
···
113
129
<button
114
130
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
115
131
title="Remove member"
116
-
hx-post="/knots/{{ $root.Registration.Domain }}/remove"
132
+
hx-post="/settings/knots/{{ $root.Registration.Domain }}/remove"
117
133
hx-swap="none"
118
134
hx-vals='{"member": "{{$member}}" }'
119
135
hx-confirm="Are you sure you want to remove {{ $memberHandle }} from this knot?"
+1
-1
appview/pages/templates/knots/fragments/addMemberModal.html
+1
-1
appview/pages/templates/knots/fragments/addMemberModal.html
+3
-3
appview/pages/templates/knots/fragments/knotListing.html
+3
-3
appview/pages/templates/knots/fragments/knotListing.html
···
7
7
8
8
{{ define "knotLeftSide" }}
9
9
{{ if .Registered }}
10
-
<a href="/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
10
+
<a href="/settings/knots/{{ .Domain }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
11
{{ i "hard-drive" "w-4 h-4" }}
12
12
<span class="hover:underline">
13
13
{{ .Domain }}
···
56
56
<button
57
57
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
58
58
title="Delete knot"
59
-
hx-delete="/knots/{{ .Domain }}"
59
+
hx-delete="/settings/knots/{{ .Domain }}"
60
60
hx-swap="outerHTML"
61
61
hx-target="#knot-{{.Id}}"
62
62
hx-confirm="Are you sure you want to delete the knot '{{ .Domain }}'?"
···
72
72
<button
73
73
class="btn gap-2 group"
74
74
title="Retry knot verification"
75
-
hx-post="/knots/{{ .Domain }}/retry"
75
+
hx-post="/settings/knots/{{ .Domain }}/retry"
76
76
hx-swap="none"
77
77
hx-target="#knot-{{.Id}}"
78
78
>
+42
-11
appview/pages/templates/knots/index.html
+42
-11
appview/pages/templates/knots/index.html
···
1
-
{{ define "title" }}knots{{ end }}
1
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
-
<h1 class="text-xl font-bold dark:text-white">Knots</h1>
6
-
<span class="flex items-center gap-1">
7
-
{{ i "book" "w-3 h-3" }}
8
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/knot-hosting.md">docs</a>
9
-
</span>
10
-
</div>
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "knotsList" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "knotsList" }}
20
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
21
+
<div class="col-span-1 md:col-span-2">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">Knots</h2>
23
+
{{ block "about" . }} {{ end }}
24
+
</div>
25
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
26
+
{{ template "docsButton" . }}
27
+
</div>
28
+
</div>
11
29
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
30
+
<section>
13
31
<div class="flex flex-col gap-6">
14
-
{{ block "about" . }} {{ end }}
15
32
{{ block "list" . }} {{ end }}
16
33
{{ block "register" . }} {{ end }}
17
34
</div>
···
50
67
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a knot</h2>
51
68
<p class="mb-2 dark:text-gray-300">Enter the hostname of your knot to get started.</p>
52
69
<form
53
-
hx-post="/knots/register"
70
+
hx-post="/settings/knots/register"
54
71
class="max-w-2xl mb-2 space-y-4"
55
72
hx-indicator="#register-button"
56
73
hx-swap="none"
···
84
101
85
102
</section>
86
103
{{ end }}
104
+
105
+
{{ define "docsButton" }}
106
+
<a
107
+
class="btn flex items-center gap-2"
108
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
109
+
{{ i "book" "size-4" }}
110
+
docs
111
+
</a>
112
+
<div
113
+
id="add-email-modal"
114
+
popover
115
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
116
+
</div>
117
+
{{ end }}
-2
appview/pages/templates/layouts/fragments/topbar.html
-2
appview/pages/templates/layouts/fragments/topbar.html
···
61
61
<a href="/{{ $user }}">profile</a>
62
62
<a href="/{{ $user }}?tab=repos">repositories</a>
63
63
<a href="/{{ $user }}?tab=strings">strings</a>
64
-
<a href="/knots">knots</a>
65
-
<a href="/spindles">spindles</a>
66
64
<a href="/settings">settings</a>
67
65
<a href="#"
68
66
hx-post="/logout"
+8
-7
appview/pages/templates/layouts/profilebase.html
+8
-7
appview/pages/templates/layouts/profilebase.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
1
+
{{ define "title" }}{{ resolve .Card.UserDid }}{{ end }}
2
2
3
3
{{ define "extrameta" }}
4
-
{{ $avatarUrl := fullAvatar .Card.UserHandle }}
5
-
<meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}" />
4
+
{{ $handle := resolve .Card.UserDid }}
5
+
{{ $avatarUrl := profileAvatarUrl .Card.Profile "" }}
6
+
<meta property="og:title" content="{{ $handle }}" />
6
7
<meta property="og:type" content="profile" />
7
-
<meta property="og:url" content="https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}?tab={{ .Active }}" />
8
-
<meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" />
8
+
<meta property="og:url" content="https://tangled.org/{{ $handle }}?tab={{ .Active }}" />
9
+
<meta property="og:description" content="{{ or .Card.Profile.Description $handle }}" />
9
10
<meta property="og:image" content="{{ $avatarUrl }}" />
10
11
<meta property="og:image:width" content="512" />
11
12
<meta property="og:image:height" content="512" />
12
13
13
14
<meta name="twitter:card" content="summary" />
14
-
<meta name="twitter:title" content="{{ or .Card.UserHandle .Card.UserDid }}" />
15
-
<meta name="twitter:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" />
15
+
<meta name="twitter:title" content="{{ $handle }}" />
16
+
<meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" />
16
17
<meta name="twitter:image" content="{{ $avatarUrl }}" />
17
18
{{ end }}
18
19
+4
-1
appview/pages/templates/layouts/repobase.html
+4
-1
appview/pages/templates/layouts/repobase.html
···
49
49
</div>
50
50
51
51
<div class="w-full sm:w-fit grid grid-cols-3 gap-2 z-auto">
52
-
{{ template "repo/fragments/repoStar" .RepoInfo }}
52
+
{{ template "fragments/starBtn"
53
+
(dict "SubjectAt" .RepoInfo.RepoAt
54
+
"IsStarred" .RepoInfo.IsStarred
55
+
"StarCount" .RepoInfo.Stats.StarCount) }}
53
56
<a
54
57
class="btn text-sm no-underline hover:no-underline flex items-center gap-2 group"
55
58
hx-boost="true"
+38
-10
appview/pages/templates/repo/commit.html
+38
-10
appview/pages/templates/repo/commit.html
···
25
25
</div>
26
26
27
27
<div class="flex flex-wrap items-center space-x-2">
28
-
<p class="flex flex-wrap items-center gap-2 text-sm text-gray-500 dark:text-gray-300">
29
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
30
-
31
-
{{ if $did }}
32
-
{{ template "user/fragments/picHandleLink" $did }}
33
-
{{ else }}
34
-
<a href="mailto:{{ $commit.Author.Email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $commit.Author.Name }}</a>
35
-
{{ end }}
28
+
<p class="flex flex-wrap items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
29
+
{{ template "attribution" . }}
36
30
37
31
<span class="px-1 select-none before:content-['\00B7']"></span>
38
-
{{ template "repo/fragments/time" $commit.Author.When }}
32
+
{{ template "repo/fragments/time" $commit.Committer.When }}
39
33
<span class="px-1 select-none before:content-['\00B7']"></span>
40
34
41
35
<a href="/{{ $repo }}/commit/{{ $commit.This }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ slice $commit.This 0 8 }}</a>
···
79
73
</section>
80
74
{{end}}
81
75
76
+
{{ define "attribution" }}
77
+
{{ $commit := .Diff.Commit }}
78
+
{{ $showCommitter := true }}
79
+
{{ if eq $commit.Author.Email $commit.Committer.Email }}
80
+
{{ $showCommitter = false }}
81
+
{{ end }}
82
+
83
+
{{ if $showCommitter }}
84
+
authored by {{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid) }}
85
+
{{ range $commit.CoAuthors }}
86
+
{{ template "attributedUser" (list .Email .Name $.EmailToDid) }}
87
+
{{ end }}
88
+
and committed by {{ template "attributedUser" (list $commit.Committer.Email $commit.Committer.Name $.EmailToDid) }}
89
+
{{ else }}
90
+
{{ template "attributedUser" (list $commit.Author.Email $commit.Author.Name $.EmailToDid )}}
91
+
{{ end }}
92
+
{{ end }}
93
+
94
+
{{ define "attributedUser" }}
95
+
{{ $email := index . 0 }}
96
+
{{ $name := index . 1 }}
97
+
{{ $map := index . 2 }}
98
+
{{ $did := index $map $email }}
99
+
100
+
{{ if $did }}
101
+
{{ template "user/fragments/picHandleLink" $did }}
102
+
{{ else }}
103
+
<span class="flex items-center gap-1">
104
+
{{ placeholderAvatar "tiny" }}
105
+
<a href="mailto:{{ $email }}" class="no-underline hover:underline text-gray-500 dark:text-gray-300">{{ $name }}</a>
106
+
</span>
107
+
{{ end }}
108
+
{{ end }}
109
+
82
110
{{ define "topbarLayout" }}
83
111
<header class="col-span-full" style="z-index: 20;">
84
112
{{ template "layouts/fragments/topbar" . }}
···
111
139
{{ end }}
112
140
113
141
{{ define "contentAfter" }}
114
-
{{ template "repo/fragments/diff" (list .RepoInfo.FullName .Diff .DiffOpts) }}
142
+
{{ template "repo/fragments/diff" (list .Diff .DiffOpts) }}
115
143
{{end}}
116
144
117
145
{{ define "contentAfterLeft" }}
+1
-1
appview/pages/templates/repo/compare/compare.html
+1
-1
appview/pages/templates/repo/compare/compare.html
+1
-1
appview/pages/templates/repo/empty.html
+1
-1
appview/pages/templates/repo/empty.html
···
35
35
36
36
<p><span class="{{$bullet}}">1</span>First, generate a new <a href="https://git-scm.com/book/en/v2/Git-on-the-Server-Generating-Your-SSH-Public-Key" class="underline">SSH key pair</a>.</p>
37
37
<p><span class="{{$bullet}}">2</span>Then add the public key to your account from the <a href="/settings" class="underline">settings</a> page.</p>
38
-
<p><span class="{{$bullet}}">3</span>Configure your remote to <code>git@{{ $knot | stripPort }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code></p>
38
+
<p><span class="{{$bullet}}">3</span>Configure your remote to <code>git@{{ $knot | stripPort }}:{{ resolve .RepoInfo.OwnerDid }}/{{ .RepoInfo.Name }}</code></p>
39
39
<p><span class="{{$bullet}}">4</span>Push!</p>
40
40
</div>
41
41
</div>
+2
-1
appview/pages/templates/repo/fork.html
+2
-1
appview/pages/templates/repo/fork.html
···
25
25
value="{{ . }}"
26
26
class="mr-2"
27
27
id="domain-{{ . }}"
28
+
{{if eq (len $.Knots) 1}}checked{{end}}
28
29
/>
29
30
<label for="domain-{{ . }}" class="dark:text-white">{{ . }}</label>
30
31
</div>
···
33
34
{{ end }}
34
35
</div>
35
36
</div>
36
-
<p class="text-sm text-gray-500 dark:text-gray-400">A knot hosts repository data. <a href="/knots" class="underline">Learn how to register your own knot.</a></p>
37
+
<p class="text-sm text-gray-500 dark:text-gray-400">A knot hosts repository data. <a href="/settings/knots" class="underline">Learn how to register your own knot.</a></p>
37
38
</fieldset>
38
39
39
40
<div class="space-y-2">
+49
appview/pages/templates/repo/fragments/backlinks.html
+49
appview/pages/templates/repo/fragments/backlinks.html
···
1
+
{{ define "repo/fragments/backlinks" }}
2
+
{{ if .Backlinks }}
3
+
<div id="at-uri-panel" class="px-2 md:px-0">
4
+
<div>
5
+
<span class="text-sm py-1 font-bold text-gray-500 dark:text-gray-400">Referenced by</span>
6
+
</div>
7
+
<ul>
8
+
{{ range .Backlinks }}
9
+
<li>
10
+
{{ $repoOwner := resolve .Handle }}
11
+
{{ $repoName := .Repo }}
12
+
{{ $repoUrl := printf "%s/%s" $repoOwner $repoName }}
13
+
<div class="flex flex-col">
14
+
<div class="flex gap-2 items-center">
15
+
{{ if .State.IsClosed }}
16
+
<span class="text-gray-500 dark:text-gray-400">
17
+
{{ i "ban" "w-4 h-4" }}
18
+
</span>
19
+
{{ else if eq .Kind.String "issues" }}
20
+
<span class="text-green-600 dark:text-green-500">
21
+
{{ i "circle-dot" "w-4 h-4" }}
22
+
</span>
23
+
{{ else if .State.IsOpen }}
24
+
<span class="text-green-600 dark:text-green-500">
25
+
{{ i "git-pull-request" "w-4 h-4" }}
26
+
</span>
27
+
{{ else if .State.IsMerged }}
28
+
<span class="text-purple-600 dark:text-purple-500">
29
+
{{ i "git-merge" "w-4 h-4" }}
30
+
</span>
31
+
{{ else }}
32
+
<span class="text-gray-600 dark:text-gray-300">
33
+
{{ i "git-pull-request-closed" "w-4 h-4" }}
34
+
</span>
35
+
{{ end }}
36
+
<a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
37
+
</div>
38
+
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
39
+
<div>
40
+
<span>on <a href="/{{ $repoUrl }}">{{ $repoUrl }}</a></span>
41
+
</div>
42
+
{{ end }}
43
+
</div>
44
+
</li>
45
+
{{ end }}
46
+
</ul>
47
+
</div>
48
+
{{ end }}
49
+
{{ end }}
+3
-2
appview/pages/templates/repo/fragments/cloneDropdown.html
+3
-2
appview/pages/templates/repo/fragments/cloneDropdown.html
···
43
43
44
44
<!-- SSH Clone -->
45
45
<div class="mb-3">
46
+
{{ $repoOwnerHandle := resolve .RepoInfo.OwnerDid }}
46
47
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">SSH</label>
47
48
<div class="flex items-center border border-gray-300 dark:border-gray-600 rounded">
48
49
<code
49
50
class="flex-1 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-l select-all cursor-pointer whitespace-nowrap overflow-x-auto"
50
51
onclick="window.getSelection().selectAllChildren(this)"
51
-
data-url="git@{{ $knot | stripPort }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}"
52
-
>git@{{ $knot | stripPort }}:{{ .RepoInfo.OwnerHandle }}/{{ .RepoInfo.Name }}</code>
52
+
data-url="git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}"
53
+
>git@{{ $knot | stripPort }}:{{ $repoOwnerHandle }}/{{ .RepoInfo.Name }}</code>
53
54
<button
54
55
onclick="copyToClipboard(this, this.previousElementSibling.getAttribute('data-url'))"
55
56
class="px-3 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 border-l border-gray-300 dark:border-gray-600"
+2
-3
appview/pages/templates/repo/fragments/diff.html
+2
-3
appview/pages/templates/repo/fragments/diff.html
+15
-1
appview/pages/templates/repo/fragments/editLabelPanel.html
+15
-1
appview/pages/templates/repo/fragments/editLabelPanel.html
···
170
170
{{ $fieldName := $def.AtUri }}
171
171
{{ $valueType := $def.ValueType }}
172
172
{{ $value := .value }}
173
+
173
174
{{ if $valueType.IsDidFormat }}
174
175
{{ $value = trimPrefix (resolve .value) "@" }}
176
+
<actor-typeahead>
177
+
<input
178
+
autocapitalize="none"
179
+
autocorrect="off"
180
+
autocomplete="off"
181
+
placeholder="user.tngl.sh"
182
+
value="{{$value}}"
183
+
name="{{$fieldName}}"
184
+
type="text"
185
+
class="p-1 w-full text-sm"
186
+
/>
187
+
</actor-typeahead>
188
+
{{ else }}
189
+
<input class="p-1 w-full" type="text" name="{{$fieldName}}" value="{{$value}}">
175
190
{{ end }}
176
-
<input class="p-1 w-full" type="text" name="{{$fieldName}}" value="{{$value}}">
177
191
{{ end }}
178
192
179
193
{{ define "nullTypeInput" }}
+1
-16
appview/pages/templates/repo/fragments/participants.html
+1
-16
appview/pages/templates/repo/fragments/participants.html
···
6
6
<span class="font-bold text-gray-500 dark:text-gray-400 capitalize">Participants</span>
7
7
<span class="bg-gray-200 dark:bg-gray-700 rounded py-1/2 px-1 ml-1">{{ len $all }}</span>
8
8
</div>
9
-
<div class="flex items-center -space-x-3 mt-2">
10
-
{{ $c := "z-50 z-40 z-30 z-20 z-10" }}
11
-
{{ range $i, $p := $ps }}
12
-
<img
13
-
src="{{ tinyAvatar . }}"
14
-
alt=""
15
-
class="rounded-full h-8 w-8 mr-1 border-2 border-gray-100 dark:border-gray-900 z-{{sub 5 $i}}0"
16
-
/>
17
-
{{ end }}
18
-
19
-
{{ if gt (len $all) 5 }}
20
-
<span class="pl-4 text-gray-500 dark:text-gray-400 text-sm">
21
-
+{{ sub (len $all) 5 }}
22
-
</span>
23
-
{{ end }}
24
-
</div>
9
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "w-8 h-8") }}
25
10
</div>
26
11
{{ end }}
-26
appview/pages/templates/repo/fragments/repoStar.html
-26
appview/pages/templates/repo/fragments/repoStar.html
···
1
-
{{ define "repo/fragments/repoStar" }}
2
-
<button
3
-
id="starBtn"
4
-
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
5
-
{{ if .IsStarred }}
6
-
hx-delete="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}"
7
-
{{ else }}
8
-
hx-post="/star?subject={{ .RepoAt }}&countHint={{ .Stats.StarCount }}"
9
-
{{ end }}
10
-
11
-
hx-trigger="click"
12
-
hx-target="this"
13
-
hx-swap="outerHTML"
14
-
hx-disabled-elt="#starBtn"
15
-
>
16
-
{{ if .IsStarred }}
17
-
{{ i "star" "w-4 h-4 fill-current" }}
18
-
{{ else }}
19
-
{{ i "star" "w-4 h-4" }}
20
-
{{ end }}
21
-
<span class="text-sm">
22
-
{{ .Stats.StarCount }}
23
-
</span>
24
-
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
25
-
</button>
26
-
{{ end }}
+35
-9
appview/pages/templates/repo/index.html
+35
-9
appview/pages/templates/repo/index.html
···
14
14
{{ end }}
15
15
<div class="flex items-center justify-between pb-5">
16
16
{{ block "branchSelector" . }}{{ end }}
17
-
<div class="flex md:hidden items-center gap-2">
17
+
<div class="flex md:hidden items-center gap-3">
18
18
<a href="/{{ .RepoInfo.FullName }}/commits/{{ .Ref | urlquery }}" class="inline-flex items-center text-sm gap-1 font-bold">
19
19
{{ i "git-commit-horizontal" "w-4" "h-4" }} {{ .TotalCommits }}
20
20
</a>
···
47
47
<div class="px-4 py-2 border-b border-gray-200 dark:border-gray-600 flex items-center gap-4 flex-wrap">
48
48
{{ range $value := .Languages }}
49
49
<div
50
-
class="flex flex-grow items-center gap-2 text-xs align-items-center justify-center"
50
+
class="flex items-center gap-2 text-xs align-items-center justify-center"
51
51
>
52
52
{{ template "repo/fragments/colorBall" (dict "color" (langColor $value.Name)) }}
53
53
<div>{{ or $value.Name "Other" }}
···
66
66
67
67
{{ define "branchSelector" }}
68
68
<div class="flex gap-2 items-center justify-between w-full">
69
-
<div class="flex gap-2 items-center">
69
+
<div class="flex gap-2 items-stretch">
70
70
<select
71
71
onchange="window.location.href = '/{{ .RepoInfo.FullName }}/tree/' + encodeURIComponent(this.value)"
72
72
class="p-1 border max-w-32 border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"
···
228
228
<span
229
229
class="mx-1 before:content-['ยท'] before:select-none"
230
230
></span>
231
-
<span>
232
-
{{ $did := index $.EmailToDid .Author.Email }}
233
-
<a href="{{ if $did }}/{{ resolve $did }}{{ else }}mailto:{{ .Author.Email }}{{ end }}"
234
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline"
235
-
>{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ .Author.Name }}{{ end }}</a>
236
-
</span>
231
+
{{ template "attribution" (list . $.EmailToDid) }}
237
232
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
238
233
{{ template "repo/fragments/time" .Committer.When }}
239
234
···
259
254
{{ end }}
260
255
</div>
261
256
</div>
257
+
{{ end }}
258
+
259
+
{{ define "attribution" }}
260
+
{{ $commit := index . 0 }}
261
+
{{ $map := index . 1 }}
262
+
<span class="flex items-center gap-1">
263
+
{{ $author := index $map $commit.Author.Email }}
264
+
{{ $coauthors := $commit.CoAuthors }}
265
+
{{ $all := list }}
266
+
267
+
{{ if $author }}
268
+
{{ $all = append $all $author }}
269
+
{{ end }}
270
+
{{ range $coauthors }}
271
+
{{ $co := index $map .Email }}
272
+
{{ if $co }}
273
+
{{ $all = append $all $co }}
274
+
{{ end }}
275
+
{{ end }}
276
+
277
+
{{ if $author }}
278
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
279
+
{{ else }}
280
+
{{ placeholderAvatar "tiny" }}
281
+
{{ end }}
282
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
283
+
class="no-underline hover:underline">
284
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
285
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
286
+
</a>
287
+
</span>
262
288
{{ end }}
263
289
264
290
{{ define "branchList" }}
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
+2
-2
appview/pages/templates/repo/issues/fragments/issueCommentHeader.html
···
19
19
{{ end }}
20
20
21
21
{{ define "timestamp" }}
22
-
<a href="#{{ .Comment.Id }}"
22
+
<a href="#comment-{{ .Comment.Id }}"
23
23
class="text-gray-500 dark:text-gray-400 hover:text-gray-500 dark:hover:text-gray-400 hover:underline no-underline"
24
-
id="{{ .Comment.Id }}">
24
+
id="comment-{{ .Comment.Id }}">
25
25
{{ if .Comment.Deleted }}
26
26
{{ template "repo/fragments/shortTimeAgo" .Comment.Deleted }}
27
27
{{ else if .Comment.Edited }}
+3
appview/pages/templates/repo/issues/issue.html
+3
appview/pages/templates/repo/issues/issue.html
···
20
20
"Subject" $.Issue.AtUri
21
21
"State" $.Issue.Labels) }}
22
22
{{ template "repo/fragments/participants" $.Issue.Participants }}
23
+
{{ template "repo/fragments/backlinks"
24
+
(dict "RepoInfo" $.RepoInfo
25
+
"Backlinks" $.Backlinks) }}
23
26
{{ template "repo/fragments/externalLinkPanel" $.Issue.AtUri }}
24
27
</div>
25
28
</div>
+116
-35
appview/pages/templates/repo/issues/issues.html
+116
-35
appview/pages/templates/repo/issues/issues.html
···
30
30
<div class="grid gap-2 grid-cols-[auto_1fr_auto] grid-row-2">
31
31
<form class="flex relative col-span-3 sm:col-span-1 sm:col-start-2" method="GET">
32
32
<input type="hidden" name="state" value="{{ if .FilteringByOpen }}open{{ else }}closed{{ end }}">
33
-
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
34
-
{{ i "search" "w-4 h-4" }}
33
+
<div class="flex-1 flex relative">
34
+
<input
35
+
id="search-q"
36
+
class="flex-1 py-1 pl-2 pr-10 mr-[-1px] rounded-r-none focus:border-0 focus:outline-none focus:ring focus:ring-blue-400 ring-inset peer"
37
+
type="text"
38
+
name="q"
39
+
value="{{ .FilterQuery }}"
40
+
placeholder=" "
41
+
>
42
+
<a
43
+
href="?state={{ if .FilteringByOpen }}open{{ else }}closed{{ end }}"
44
+
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hidden peer-[:not(:placeholder-shown)]:block"
45
+
>
46
+
{{ i "x" "w-4 h-4" }}
47
+
</a>
35
48
</div>
36
-
<input class="flex-1 p-1 pl-10 pr-10 peer" type="text" name="q" value="{{ .FilterQuery }}" placeholder=" ">
37
-
<a
38
-
href="?state={{ if .FilteringByOpen }}open{{ else }}closed{{ end }}"
39
-
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hidden peer-[:not(:placeholder-shown)]:block"
49
+
<button
50
+
type="submit"
51
+
class="p-2 text-gray-400 border rounded-r border-gray-400 dark:border-gray-600"
40
52
>
41
-
{{ i "x" "w-4 h-4" }}
42
-
</a>
53
+
{{ i "search" "w-4 h-4" }}
54
+
</button>
43
55
</form>
44
56
<div class="sm:row-start-1">
45
-
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active) }}
57
+
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active "Include" "#search-q") }}
46
58
</div>
47
59
<a
48
60
href="/{{ .RepoInfo.FullName }}/issues/new"
···
59
71
<div class="mt-2">
60
72
{{ template "repo/issues/fragments/issueListing" (dict "Issues" .Issues "RepoPrefix" .RepoInfo.FullName "LabelDefs" .LabelDefs) }}
61
73
</div>
62
-
{{ block "pagination" . }} {{ end }}
74
+
{{if gt .IssueCount .Page.Limit }}
75
+
{{ block "pagination" . }} {{ end }}
76
+
{{ end }}
63
77
{{ end }}
64
78
65
79
{{ define "pagination" }}
66
-
<div class="flex justify-end mt-4 gap-2">
67
-
{{ $currentState := "closed" }}
68
-
{{ if .FilteringByOpen }}
69
-
{{ $currentState = "open" }}
70
-
{{ end }}
80
+
<div class="flex justify-center items-center mt-4 gap-2">
81
+
{{ $currentState := "closed" }}
82
+
{{ if .FilteringByOpen }}
83
+
{{ $currentState = "open" }}
84
+
{{ end }}
85
+
86
+
{{ $prev := .Page.Previous.Offset }}
87
+
{{ $next := .Page.Next.Offset }}
88
+
{{ $lastPage := sub .IssueCount (mod .IssueCount .Page.Limit) }}
71
89
90
+
<a
91
+
class="
92
+
btn flex items-center gap-2 no-underline hover:no-underline
93
+
dark:text-white dark:hover:bg-gray-700
94
+
{{ if le .Page.Offset 0 }}
95
+
cursor-not-allowed opacity-50
96
+
{{ end }}
97
+
"
72
98
{{ if gt .Page.Offset 0 }}
73
-
{{ $prev := .Page.Previous }}
74
-
<a
75
-
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
76
-
hx-boost="true"
77
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev.Offset }}&limit={{ $prev.Limit }}"
78
-
>
79
-
{{ i "chevron-left" "w-4 h-4" }}
80
-
previous
81
-
</a>
82
-
{{ else }}
83
-
<div></div>
99
+
hx-boost="true"
100
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev }}&limit={{ .Page.Limit }}"
84
101
{{ end }}
102
+
>
103
+
{{ i "chevron-left" "w-4 h-4" }}
104
+
previous
105
+
</a>
85
106
107
+
<!-- dont show first page if current page is first page -->
108
+
{{ if gt .Page.Offset 0 }}
109
+
<a
110
+
hx-boost="true"
111
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset=0&limit={{ .Page.Limit }}"
112
+
>
113
+
1
114
+
</a>
115
+
{{ end }}
116
+
117
+
<!-- if previous page is not first or second page (prev > limit) -->
118
+
{{ if gt $prev .Page.Limit }}
119
+
<span>...</span>
120
+
{{ end }}
121
+
122
+
<!-- if previous page is not the first page -->
123
+
{{ if gt $prev 0 }}
124
+
<a
125
+
hx-boost="true"
126
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $prev }}&limit={{ .Page.Limit }}"
127
+
>
128
+
{{ add (div $prev .Page.Limit) 1 }}
129
+
</a>
130
+
{{ end }}
131
+
132
+
<!-- current page. this is always visible -->
133
+
<span class="font-bold">
134
+
{{ add (div .Page.Offset .Page.Limit) 1 }}
135
+
</span>
136
+
137
+
<!-- if next page is not last page -->
138
+
{{ if lt $next $lastPage }}
139
+
<a
140
+
hx-boost="true"
141
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next }}&limit={{ .Page.Limit }}"
142
+
>
143
+
{{ add (div $next .Page.Limit) 1 }}
144
+
</a>
145
+
{{ end }}
146
+
147
+
<!-- if next page is not second last or last page (next < issues - 2 * limit) -->
148
+
{{ if lt ($next) (sub .IssueCount (mul (2) .Page.Limit)) }}
149
+
<span>...</span>
150
+
{{ end }}
151
+
152
+
<!-- if its not the last page -->
153
+
{{ if lt .Page.Offset $lastPage }}
154
+
<a
155
+
hx-boost="true"
156
+
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $lastPage }}&limit={{ .Page.Limit }}"
157
+
>
158
+
{{ add (div $lastPage .Page.Limit) 1 }}
159
+
</a>
160
+
{{ end }}
161
+
162
+
<a
163
+
class="
164
+
btn flex items-center gap-2 no-underline hover:no-underline
165
+
dark:text-white dark:hover:bg-gray-700
166
+
{{ if ne (len .Issues) .Page.Limit }}
167
+
cursor-not-allowed opacity-50
168
+
{{ end }}
169
+
"
86
170
{{ if eq (len .Issues) .Page.Limit }}
87
-
{{ $next := .Page.Next }}
88
-
<a
89
-
class="btn flex items-center gap-2 no-underline hover:no-underline dark:text-white dark:hover:bg-gray-700"
90
-
hx-boost="true"
91
-
href = "/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next.Offset }}&limit={{ $next.Limit }}"
92
-
>
93
-
next
94
-
{{ i "chevron-right" "w-4 h-4" }}
95
-
</a>
171
+
hx-boost="true"
172
+
href="/{{ $.RepoInfo.FullName }}/issues?state={{ $currentState }}&q={{ .FilterQuery }}&offset={{ $next }}&limit={{ .Page.Limit }}"
96
173
{{ end }}
174
+
>
175
+
next
176
+
{{ i "chevron-right" "w-4 h-4" }}
177
+
</a>
97
178
</div>
98
179
{{ end }}
+44
-23
appview/pages/templates/repo/log.html
+44
-23
appview/pages/templates/repo/log.html
···
17
17
<div class="hidden md:flex md:flex-col divide-y divide-gray-200 dark:divide-gray-700">
18
18
{{ $grid := "grid grid-cols-14 gap-4" }}
19
19
<div class="{{ $grid }}">
20
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2">Author</div>
20
+
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Author</div>
21
21
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-3">Commit</div>
22
22
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-6">Message</div>
23
-
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-1"></div>
24
23
<div class="py-2 text-sm text-left text-gray-700 dark:text-gray-300 uppercase font-bold col-span-2 justify-self-end">Date</div>
25
24
</div>
26
25
{{ range $index, $commit := .Commits }}
27
26
{{ $messageParts := splitN $commit.Message "\n\n" 2 }}
28
27
<div class="{{ $grid }} py-3">
29
-
<div class="align-top truncate col-span-2">
30
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
31
-
{{ if $did }}
32
-
{{ template "user/fragments/picHandleLink" $did }}
33
-
{{ else }}
34
-
<a href="mailto:{{ $commit.Author.Email }}" class="text-gray-700 dark:text-gray-300 no-underline hover:underline">{{ $commit.Author.Name }}</a>
35
-
{{ end }}
28
+
<div class="align-top col-span-3">
29
+
{{ template "attribution" (list $commit $.EmailToDid) }}
36
30
</div>
37
31
<div class="align-top font-mono flex items-start col-span-3">
38
32
{{ $verified := $.VerifiedCommits.IsVerified $commit.Hash.String }}
···
61
55
<div class="align-top col-span-6">
62
56
<div>
63
57
<a href="/{{ $.RepoInfo.FullName }}/commit/{{ $commit.Hash.String }}" class="dark:text-white no-underline hover:underline">{{ index $messageParts 0 }}</a>
58
+
64
59
{{ if gt (len $messageParts) 1 }}
65
60
<button class="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>
66
61
{{ end }}
···
72
67
</span>
73
68
{{ end }}
74
69
{{ end }}
70
+
71
+
<!-- ci status -->
72
+
<span class="text-xs">
73
+
{{ $pipeline := index $.Pipelines .Hash.String }}
74
+
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
75
+
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
76
+
{{ end }}
77
+
</span>
75
78
</div>
76
79
77
80
{{ if gt (len $messageParts) 1 }}
78
81
<p class="hidden mt-1 text-sm text-gray-600 dark:text-gray-400">{{ nl2br (index $messageParts 1) }}</p>
79
82
{{ end }}
80
-
</div>
81
-
<div class="align-top col-span-1">
82
-
<!-- ci status -->
83
-
{{ $pipeline := index $.Pipelines .Hash.String }}
84
-
{{ if and $pipeline (gt (len $pipeline.Statuses) 0) }}
85
-
{{ template "repo/pipelines/fragments/pipelineSymbolLong" (dict "Pipeline" $pipeline "RepoInfo" $.RepoInfo) }}
86
-
{{ end }}
87
83
</div>
88
84
<div class="align-top justify-self-end text-gray-500 dark:text-gray-400 col-span-2">{{ template "repo/fragments/shortTimeAgo" $commit.Committer.When }}</div>
89
85
</div>
···
152
148
</a>
153
149
</span>
154
150
<span class="mx-2 before:content-['ยท'] before:select-none"></span>
155
-
<span>
156
-
{{ $did := index $.EmailToDid $commit.Author.Email }}
157
-
<a href="{{ if $did }}/{{ $did }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
158
-
class="text-gray-500 dark:text-gray-400 no-underline hover:underline">
159
-
{{ if $did }}{{ template "user/fragments/picHandleLink" $did }}{{ else }}{{ $commit.Author.Name }}{{ end }}
160
-
</a>
161
-
</span>
151
+
{{ template "attribution" (list $commit $.EmailToDid) }}
162
152
<div class="inline-block px-1 select-none after:content-['ยท']"></div>
163
153
<span>{{ template "repo/fragments/shortTime" $commit.Committer.When }}</span>
164
154
···
176
166
</div>
177
167
</section>
178
168
169
+
{{ end }}
170
+
171
+
{{ define "attribution" }}
172
+
{{ $commit := index . 0 }}
173
+
{{ $map := index . 1 }}
174
+
<span class="flex items-center gap-1">
175
+
{{ $author := index $map $commit.Author.Email }}
176
+
{{ $coauthors := $commit.CoAuthors }}
177
+
{{ $all := list }}
178
+
179
+
{{ if $author }}
180
+
{{ $all = append $all $author }}
181
+
{{ end }}
182
+
{{ range $coauthors }}
183
+
{{ $co := index $map .Email }}
184
+
{{ if $co }}
185
+
{{ $all = append $all $co }}
186
+
{{ end }}
187
+
{{ end }}
188
+
189
+
{{ if $author }}
190
+
{{ template "fragments/tinyAvatarList" (dict "all" $all "classes" "size-6") }}
191
+
{{ else }}
192
+
{{ placeholderAvatar "tiny" }}
193
+
{{ end }}
194
+
<a href="{{ if $author }}/{{ $author }}{{ else }}mailto:{{ $commit.Author.Email }}{{ end }}"
195
+
class="no-underline hover:underline">
196
+
{{ if $author }}{{ resolve $author }}{{ else }}{{ $commit.Author.Name }}{{ end }}
197
+
{{ if $coauthors }} +{{ length $coauthors }}{{ end }}
198
+
</a>
199
+
</span>
179
200
{{ end }}
180
201
181
202
{{ define "repoAfter" }}
+2
-1
appview/pages/templates/repo/new.html
+2
-1
appview/pages/templates/repo/new.html
···
155
155
class="mr-2"
156
156
id="domain-{{ . }}"
157
157
required
158
+
{{if eq (len $.Knots) 1}}checked{{end}}
158
159
/>
159
160
<label for="domain-{{ . }}" class="dark:text-white lowercase">{{ . }}</label>
160
161
</div>
···
164
165
</div>
165
166
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
166
167
A knot hosts repository data and handles Git operations.
167
-
You can also <a href="/knots" class="underline">register your own knot</a>.
168
+
You can also <a href="/settings/knots" class="underline">register your own knot</a>.
168
169
</p>
169
170
</div>
170
171
{{ end }}
+1
-1
appview/pages/templates/repo/pulls/patch.html
+1
-1
appview/pages/templates/repo/pulls/patch.html
+3
appview/pages/templates/repo/pulls/pull.html
+3
appview/pages/templates/repo/pulls/pull.html
···
21
21
"Subject" $.Pull.AtUri
22
22
"State" $.Pull.Labels) }}
23
23
{{ template "repo/fragments/participants" $.Pull.Participants }}
24
+
{{ template "repo/fragments/backlinks"
25
+
(dict "RepoInfo" $.RepoInfo
26
+
"Backlinks" $.Backlinks) }}
24
27
{{ template "repo/fragments/externalLinkPanel" $.Pull.AtUri }}
25
28
</div>
26
29
</div>
+21
-9
appview/pages/templates/repo/pulls/pulls.html
+21
-9
appview/pages/templates/repo/pulls/pulls.html
···
36
36
<div class="grid gap-2 grid-cols-[auto_1fr_auto] grid-row-2">
37
37
<form class="flex relative col-span-3 sm:col-span-1 sm:col-start-2" method="GET">
38
38
<input type="hidden" name="state" value="{{ .FilteringBy.String }}">
39
-
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none">
40
-
{{ i "search" "w-4 h-4" }}
39
+
<div class="flex-1 flex relative">
40
+
<input
41
+
id="search-q"
42
+
class="flex-1 py-1 pl-2 pr-10 mr-[-1px] rounded-r-none focus:border-0 focus:outline-none focus:ring focus:ring-blue-400 ring-inset peer"
43
+
type="text"
44
+
name="q"
45
+
value="{{ .FilterQuery }}"
46
+
placeholder=" "
47
+
>
48
+
<a
49
+
href="?state={{ .FilteringBy.String }}"
50
+
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hidden peer-[:not(:placeholder-shown)]:block"
51
+
>
52
+
{{ i "x" "w-4 h-4" }}
53
+
</a>
41
54
</div>
42
-
<input class="flex-1 p-1 pl-10 pr-10 peer" type="text" name="q" value="{{ .FilterQuery }}" placeholder=" ">
43
-
<a
44
-
href="?state={{ .FilteringBy.String }}"
45
-
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hidden peer-[:not(:placeholder-shown)]:block"
55
+
<button
56
+
type="submit"
57
+
class="p-2 text-gray-400 border rounded-r border-gray-400 dark:border-gray-600"
46
58
>
47
-
{{ i "x" "w-4 h-4" }}
48
-
</a>
59
+
{{ i "search" "w-4 h-4" }}
60
+
</button>
49
61
</form>
50
62
<div class="sm:row-start-1">
51
-
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active) }}
63
+
{{ template "fragments/tabSelector" (dict "Name" "state" "Values" $values "Active" $active "Include" "#search-q") }}
52
64
</div>
53
65
<a
54
66
href="/{{ .RepoInfo.FullName }}/pulls/new"
+5
-4
appview/pages/templates/repo/settings/access.html
+5
-4
appview/pages/templates/repo/settings/access.html
···
29
29
{{ template "addCollaboratorButton" . }}
30
30
{{ end }}
31
31
{{ range .Collaborators }}
32
+
{{ $handle := resolve .Did }}
32
33
<div class="border border-gray-200 dark:border-gray-700 rounded p-4">
33
34
<div class="flex items-center gap-3">
34
35
<img
35
-
src="{{ fullAvatar .Handle }}"
36
-
alt="{{ .Handle }}"
36
+
src="{{ fullAvatar $handle }}"
37
+
alt="{{ $handle }}"
37
38
class="rounded-full h-10 w-10 border border-gray-300 dark:border-gray-600 flex-shrink-0"/>
38
39
39
40
<div class="flex-1 min-w-0">
40
-
<a href="/{{ .Handle }}" class="block truncate">
41
-
{{ didOrHandle .Did .Handle }}
41
+
<a href="/{{ $handle }}" class="block truncate">
42
+
{{ $handle }}
42
43
</a>
43
44
<p class="text-sm text-gray-500 dark:text-gray-400">{{ .Role }}</p>
44
45
</div>
+22
-6
appview/pages/templates/spindles/dashboard.html
+22
-6
appview/pages/templates/spindles/dashboard.html
···
1
-
{{ define "title" }}{{.Spindle.Instance}} · spindles{{ end }}
1
+
{{ define "title" }}{{.Spindle.Instance}} · {{ .Tab }} settings{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="px-6 py-4">
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "spindleDash" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "spindleDash" }}
20
+
<div>
5
21
<div class="flex justify-between items-center">
6
-
<h1 class="text-xl font-bold dark:text-white">{{ .Spindle.Instance }}</h1>
22
+
<h2 class="text-sm pb-2 uppercase font-bold">{{ .Tab }} · {{ .Spindle.Instance }}</h2>
7
23
<div id="right-side" class="flex gap-2">
8
24
{{ $style := "px-2 py-1 rounded flex items-center flex-shrink-0 gap-2" }}
9
25
{{ $isOwner := and .LoggedInUser (eq .LoggedInUser.Did .Spindle.Owner) }}
···
71
87
<button
72
88
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
73
89
title="Delete spindle"
74
-
hx-delete="/spindles/{{ .Instance }}"
90
+
hx-delete="/settings/spindles/{{ .Instance }}"
75
91
hx-swap="outerHTML"
76
92
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
77
93
hx-headers='{"shouldRedirect": "true"}'
···
87
103
<button
88
104
class="btn gap-2 group"
89
105
title="Retry spindle verification"
90
-
hx-post="/spindles/{{ .Instance }}/retry"
106
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
91
107
hx-swap="none"
92
108
hx-headers='{"shouldRefresh": "true"}'
93
109
>
···
104
120
<button
105
121
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
106
122
title="Remove member"
107
-
hx-post="/spindles/{{ $root.Spindle.Instance }}/remove"
123
+
hx-post="/settings/spindles/{{ $root.Spindle.Instance }}/remove"
108
124
hx-swap="none"
109
125
hx-vals='{"member": "{{$member}}" }'
110
126
hx-confirm="Are you sure you want to remove {{ resolve $member }} from this instance?"
+1
-1
appview/pages/templates/spindles/fragments/addMemberModal.html
+1
-1
appview/pages/templates/spindles/fragments/addMemberModal.html
+3
-3
appview/pages/templates/spindles/fragments/spindleListing.html
+3
-3
appview/pages/templates/spindles/fragments/spindleListing.html
···
7
7
8
8
{{ define "spindleLeftSide" }}
9
9
{{ if .Verified }}
10
-
<a href="/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
10
+
<a href="/settings/spindles/{{ .Instance }}" class="hover:no-underline flex items-center gap-2 min-w-0 max-w-[60%]">
11
11
{{ i "hard-drive" "w-4 h-4" }}
12
12
<span class="hover:underline">
13
13
{{ .Instance }}
···
50
50
<button
51
51
class="btn text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 gap-2 group"
52
52
title="Delete spindle"
53
-
hx-delete="/spindles/{{ .Instance }}"
53
+
hx-delete="/settings/spindles/{{ .Instance }}"
54
54
hx-swap="outerHTML"
55
55
hx-target="#spindle-{{.Id}}"
56
56
hx-confirm="Are you sure you want to delete the spindle '{{ .Instance }}'?"
···
66
66
<button
67
67
class="btn gap-2 group"
68
68
title="Retry spindle verification"
69
-
hx-post="/spindles/{{ .Instance }}/retry"
69
+
hx-post="/settings/spindles/{{ .Instance }}/retry"
70
70
hx-swap="none"
71
71
hx-target="#spindle-{{.Id}}"
72
72
>
+90
-59
appview/pages/templates/spindles/index.html
+90
-59
appview/pages/templates/spindles/index.html
···
1
-
{{ define "title" }}spindles{{ end }}
1
+
{{ define "title" }}{{ .Tab }} settings{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
5
-
<h1 class="text-xl font-bold dark:text-white">Spindles</h1>
6
-
<span class="flex items-center gap-1">
7
-
{{ i "book" "w-3 h-3" }}
8
-
<a href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">docs</a>
9
-
</span>
4
+
<div class="p-6">
5
+
<p class="text-xl font-bold dark:text-white">Settings</p>
6
+
</div>
7
+
<div class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
8
+
<section class="w-full grid grid-cols-1 md:grid-cols-4 gap-6">
9
+
<div class="col-span-1">
10
+
{{ template "user/settings/fragments/sidebar" . }}
11
+
</div>
12
+
<div class="col-span-1 md:col-span-3 flex flex-col gap-6">
13
+
{{ template "spindleList" . }}
14
+
</div>
15
+
</section>
16
+
</div>
17
+
{{ end }}
18
+
19
+
{{ define "spindleList" }}
20
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
21
+
<div class="col-span-1 md:col-span-2">
22
+
<h2 class="text-sm pb-2 uppercase font-bold">Spindle</h2>
23
+
{{ block "about" . }} {{ end }}
24
+
</div>
25
+
<div class="col-span-1 md:col-span-1 md:justify-self-end">
26
+
{{ template "docsButton" . }}
27
+
</div>
10
28
</div>
11
29
12
-
<section class="bg-white dark:bg-gray-800 p-6 rounded relative w-full mx-auto drop-shadow-sm dark:text-white">
30
+
<section>
13
31
<div class="flex flex-col gap-6">
14
-
{{ block "about" . }} {{ end }}
15
32
{{ block "list" . }} {{ end }}
16
33
{{ block "register" . }} {{ end }}
17
34
</div>
···
20
37
21
38
{{ define "about" }}
22
39
<section class="rounded flex items-center gap-2">
23
-
<p class="text-gray-500 dark:text-gray-400">
24
-
Spindles are small CI runners.
25
-
</p>
40
+
<p class="text-gray-500 dark:text-gray-400">
41
+
Spindles are small CI runners.
42
+
</p>
26
43
</section>
27
44
{{ end }}
28
45
29
46
{{ define "list" }}
30
-
<section class="rounded w-full flex flex-col gap-2">
31
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
32
-
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
33
-
{{ range $spindle := .Spindles }}
34
-
{{ template "spindles/fragments/spindleListing" . }}
35
-
{{ else }}
36
-
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
37
-
no spindles registered yet
38
-
</div>
39
-
{{ end }}
47
+
<section class="rounded w-full flex flex-col gap-2">
48
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">your spindles</h2>
49
+
<div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 w-full">
50
+
{{ range $spindle := .Spindles }}
51
+
{{ template "spindles/fragments/spindleListing" . }}
52
+
{{ else }}
53
+
<div class="flex items-center justify-center p-2 border-b border-gray-200 dark:border-gray-700 text-gray-500">
54
+
no spindles registered yet
40
55
</div>
41
-
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
42
-
</section>
56
+
{{ end }}
57
+
</div>
58
+
<div id="operation-error" class="text-red-500 dark:text-red-400"></div>
59
+
</section>
43
60
{{ end }}
44
61
45
62
{{ define "register" }}
46
-
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
47
-
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
48
-
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
49
-
<form
50
-
hx-post="/spindles/register"
51
-
class="max-w-2xl mb-2 space-y-4"
52
-
hx-indicator="#register-button"
53
-
hx-swap="none"
54
-
>
55
-
<div class="flex gap-2">
56
-
<input
57
-
type="text"
58
-
id="instance"
59
-
name="instance"
60
-
placeholder="spindle.example.com"
61
-
required
62
-
class="flex-1 w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
63
-
>
64
-
<button
65
-
type="submit"
66
-
id="register-button"
67
-
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
68
-
>
69
-
<span class="inline-flex items-center gap-2">
70
-
{{ i "plus" "w-4 h-4" }}
71
-
register
72
-
</span>
73
-
<span class="pl-2 hidden group-[.htmx-request]:inline">
74
-
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
75
-
</span>
76
-
</button>
77
-
</div>
63
+
<section class="rounded w-full lg:w-fit flex flex-col gap-2">
64
+
<h2 class="text-sm font-bold py-2 uppercase dark:text-gray-300">register a spindle</h2>
65
+
<p class="mb-2 dark:text-gray-300">Enter the hostname of your spindle to get started.</p>
66
+
<form
67
+
hx-post="/settings/spindles/register"
68
+
class="max-w-2xl mb-2 space-y-4"
69
+
hx-indicator="#register-button"
70
+
hx-swap="none"
71
+
>
72
+
<div class="flex gap-2">
73
+
<input
74
+
type="text"
75
+
id="instance"
76
+
name="instance"
77
+
placeholder="spindle.example.com"
78
+
required
79
+
class="flex-1 w-full dark:bg-gray-700 dark:text-white dark:border-gray-600 dark:placeholder-gray-400 px-3 py-2 border rounded"
80
+
>
81
+
<button
82
+
type="submit"
83
+
id="register-button"
84
+
class="btn rounded flex items-center py-2 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600 group"
85
+
>
86
+
<span class="inline-flex items-center gap-2">
87
+
{{ i "plus" "w-4 h-4" }}
88
+
register
89
+
</span>
90
+
<span class="pl-2 hidden group-[.htmx-request]:inline">
91
+
{{ i "loader-circle" "w-4 h-4 animate-spin" }}
92
+
</span>
93
+
</button>
94
+
</div>
78
95
79
-
<div id="register-error" class="dark:text-red-400"></div>
80
-
</form>
96
+
<div id="register-error" class="dark:text-red-400"></div>
97
+
</form>
98
+
99
+
</section>
100
+
{{ end }}
81
101
82
-
</section>
102
+
{{ define "docsButton" }}
103
+
<a
104
+
class="btn flex items-center gap-2"
105
+
href="https://tangled.org/@tangled.org/core/blob/master/docs/spindle/hosting.md">
106
+
{{ i "book" "size-4" }}
107
+
docs
108
+
</a>
109
+
<div
110
+
id="add-email-modal"
111
+
popover
112
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
113
+
</div>
83
114
{{ end }}
+6
-5
appview/pages/templates/strings/dashboard.html
+6
-5
appview/pages/templates/strings/dashboard.html
···
1
-
{{ define "title" }}strings by {{ or .Card.UserHandle .Card.UserDid }}{{ end }}
1
+
{{ define "title" }}strings by {{ resolve .Card.UserDid }}{{ end }}
2
2
3
3
{{ define "extrameta" }}
4
-
<meta property="og:title" content="{{ or .Card.UserHandle .Card.UserDid }}" />
4
+
{{ $handle := resolve .Card.UserDid }}
5
+
<meta property="og:title" content="{{ $handle }}" />
5
6
<meta property="og:type" content="profile" />
6
-
<meta property="og:url" content="https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}" />
7
-
<meta property="og:description" content="{{ or .Card.Profile.Description .Card.UserHandle .Card.UserDid }}" />
7
+
<meta property="og:url" content="https://tangled.org/{{ $handle }}" />
8
+
<meta property="og:description" content="{{ or .Card.Profile.Description $handle }}" />
8
9
{{ end }}
9
10
10
11
···
35
36
{{ $s := index . 1 }}
36
37
<div class="py-4 px-6 drop-shadow-sm rounded bg-white dark:bg-gray-800">
37
38
<div class="font-medium dark:text-white flex gap-2 items-center">
38
-
<a href="/strings/{{ or $root.Card.UserHandle $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
39
+
<a href="/strings/{{ resolve $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
39
40
</div>
40
41
{{ with $s.Description }}
41
42
<div class="text-gray-600 dark:text-gray-300 text-sm">
+11
-7
appview/pages/templates/strings/string.html
+11
-7
appview/pages/templates/strings/string.html
···
1
-
{{ define "title" }}{{ .String.Filename }} ยท by {{ didOrHandle .Owner.DID.String .Owner.Handle.String }}{{ end }}
1
+
{{ define "title" }}{{ .String.Filename }} ยท by {{ resolve .Owner.DID.String }}{{ end }}
2
2
3
3
{{ define "extrameta" }}
4
-
{{ $ownerId := didOrHandle .Owner.DID.String .Owner.Handle.String }}
4
+
{{ $ownerId := resolve .Owner.DID.String }}
5
5
<meta property="og:title" content="{{ .String.Filename }} ยท by {{ $ownerId }}" />
6
6
<meta property="og:type" content="object" />
7
7
<meta property="og:url" content="https://tangled.org/strings/{{ $ownerId }}/{{ .String.Rkey }}" />
···
9
9
{{ end }}
10
10
11
11
{{ define "content" }}
12
-
{{ $ownerId := didOrHandle .Owner.DID.String .Owner.Handle.String }}
12
+
{{ $ownerId := resolve .Owner.DID.String }}
13
13
<section id="string-header" class="mb-4 py-2 px-6 dark:text-white">
14
14
<div class="text-lg flex items-center justify-between">
15
15
<div>
···
17
17
<span class="select-none">/</span>
18
18
<a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a>
19
19
</div>
20
-
{{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }}
21
-
<div class="flex gap-2 text-base">
20
+
<div class="flex gap-2 text-base">
21
+
{{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }}
22
22
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
23
23
hx-boost="true"
24
24
href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit">
···
37
37
<span class="hidden md:inline">delete</span>
38
38
{{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
39
39
</button>
40
-
</div>
41
-
{{ end }}
40
+
{{ end }}
41
+
{{ template "fragments/starBtn"
42
+
(dict "SubjectAt" .String.AtUri
43
+
"IsStarred" .IsStarred
44
+
"StarCount" .StarCount) }}
45
+
</div>
42
46
</div>
43
47
<span>
44
48
{{ with .String.Description }}
+1
-2
appview/pages/templates/timeline/fragments/goodfirstissues.html
+1
-2
appview/pages/templates/timeline/fragments/goodfirstissues.html
···
3
3
<a href="/goodfirstissues" class="no-underline hover:no-underline">
4
4
<div class="flex items-center justify-between gap-2 bg-purple-200 dark:bg-purple-900 border border-purple-400 dark:border-purple-500 rounded mb-4 py-4 px-6 ">
5
5
<div class="flex-1 flex flex-col gap-2">
6
-
<div class="text-purple-500 dark:text-purple-400">Oct 2025</div>
7
6
<p>
8
-
Make your first contribution to an open-source project this October.
7
+
Make your first contribution to an open-source project.
9
8
<em>good-first-issue</em> helps new contributors find easy ways to
10
9
start contributing to open-source projects.
11
10
</p>
+5
-5
appview/pages/templates/timeline/fragments/timeline.html
+5
-5
appview/pages/templates/timeline/fragments/timeline.html
···
14
14
<div class="flex flex-col divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-sm">
15
15
{{ if .Repo }}
16
16
{{ template "timeline/fragments/repoEvent" (list $ .) }}
17
-
{{ else if .Star }}
17
+
{{ else if .RepoStar }}
18
18
{{ template "timeline/fragments/starEvent" (list $ .) }}
19
19
{{ else if .Follow }}
20
20
{{ template "timeline/fragments/followEvent" (list $ .) }}
···
52
52
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" $repo.Created }}</span>
53
53
</div>
54
54
{{ with $repo }}
55
-
{{ template "user/fragments/repoCard" (list $root . true true (dict "IsStarred" $event.IsStarred "RepoAt" .RepoAt "Stats" (dict "StarCount" $event.StarCount))) }}
55
+
{{ template "user/fragments/repoCard" (list $root . true true (dict "IsStarred" $event.IsStarred "SubjectAt" .RepoAt "StarCount" $event.StarCount)) }}
56
56
{{ end }}
57
57
{{ end }}
58
58
59
59
{{ define "timeline/fragments/starEvent" }}
60
60
{{ $root := index . 0 }}
61
61
{{ $event := index . 1 }}
62
-
{{ $star := $event.Star }}
62
+
{{ $star := $event.RepoStar }}
63
63
{{ with $star }}
64
-
{{ $starrerHandle := resolve .StarredByDid }}
64
+
{{ $starrerHandle := resolve .Did }}
65
65
{{ $repoOwnerHandle := resolve .Repo.Did }}
66
66
<div class="pl-6 py-2 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 flex flex-wrap items-center gap-2 text-sm">
67
67
{{ template "user/fragments/picHandleLink" $starrerHandle }}
···
72
72
<span class="text-gray-700 dark:text-gray-400 text-xs">{{ template "repo/fragments/time" .Created }}</span>
73
73
</div>
74
74
{{ with .Repo }}
75
-
{{ template "user/fragments/repoCard" (list $root . true true (dict "IsStarred" $event.IsStarred "RepoAt" .RepoAt "Stats" (dict "StarCount" $event.StarCount))) }}
75
+
{{ template "user/fragments/repoCard" (list $root . true true (dict "IsStarred" $event.IsStarred "SubjectAt" .RepoAt "StarCount" $event.StarCount)) }}
76
76
{{ end }}
77
77
{{ end }}
78
78
{{ end }}
+4
-2
appview/pages/templates/user/followers.html
+4
-2
appview/pages/templates/user/followers.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท followers {{ end }}
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท followers {{ end }}
2
2
3
3
{{ define "profileContent" }}
4
4
<div id="all-followers" class="md:col-span-8 order-2 md:order-2">
···
19
19
"FollowersCount" .FollowersCount
20
20
"FollowingCount" .FollowingCount) }}
21
21
{{ else }}
22
-
<p class="px-6 dark:text-white">This user does not have any followers yet.</p>
22
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
23
+
<span>This user does not have any followers yet.</span>
24
+
</div>
23
25
{{ end }}
24
26
</div>
25
27
{{ end }}
+4
-2
appview/pages/templates/user/following.html
+4
-2
appview/pages/templates/user/following.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท following {{ end }}
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท following {{ end }}
2
2
3
3
{{ define "profileContent" }}
4
4
<div id="all-following" class="md:col-span-8 order-2 md:order-2">
···
19
19
"FollowersCount" .FollowersCount
20
20
"FollowingCount" .FollowingCount) }}
21
21
{{ else }}
22
-
<p class="px-6 dark:text-white">This user does not follow anyone yet.</p>
22
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
23
+
<span>This user does not follow anyone yet.</span>
24
+
</div>
23
25
{{ end }}
24
26
</div>
25
27
{{ end }}
+44
appview/pages/templates/user/fragments/editAvatar.html
+44
appview/pages/templates/user/fragments/editAvatar.html
···
1
+
{{ define "user/fragments/editAvatar" }}
2
+
<form
3
+
hx-post="/profile/avatar"
4
+
hx-encoding="multipart/form-data"
5
+
hx-indicator="#spinner"
6
+
hx-swap="none"
7
+
class="flex flex-col gap-2">
8
+
<label for="avatar-file" class="uppercase p-0">
9
+
Upload avatar
10
+
</label>
11
+
<p class="text-sm text-gray-500 dark:text-gray-400">Select an image (PNG or JPEG, max 1MB)</p>
12
+
<input
13
+
type="file"
14
+
id="avatar-file"
15
+
name="avatar"
16
+
accept="image/png,image/jpeg"
17
+
required
18
+
class="block w-full text-sm text-gray-500 dark:text-gray-400
19
+
file:mr-4 file:py-2 file:px-4
20
+
file:rounded file:border-0
21
+
file:text-sm file:font-semibold
22
+
file:bg-gray-100 file:text-gray-700
23
+
dark:file:bg-gray-700 dark:file:text-gray-300
24
+
hover:file:bg-gray-200 dark:hover:file:bg-gray-600" />
25
+
<div class="flex gap-2 pt-2">
26
+
<button
27
+
id="cancel-avatar-btn"
28
+
type="button"
29
+
popovertarget="avatar-upload-modal"
30
+
popovertargetaction="hide"
31
+
class="btn w-1/2 flex items-center gap-2 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300">
32
+
{{ i "x" "size-4" }}
33
+
cancel
34
+
</button>
35
+
<button type="submit" class="btn w-1/2 flex items-center">
36
+
<span class="inline-flex gap-2 items-center">{{ i "upload" "size-4" }} upload</span>
37
+
<span id="spinner" class="group">
38
+
{{ i "loader-circle" "ml-2 w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }}
39
+
</span>
40
+
</button>
41
+
</div>
42
+
<div id="avatar-error" class="text-red-500 dark:text-red-400"></div>
43
+
</form>
44
+
{{ end }}
-1
appview/pages/templates/user/fragments/editBio.html
-1
appview/pages/templates/user/fragments/editBio.html
+18
-4
appview/pages/templates/user/fragments/profileCard.html
+18
-4
appview/pages/templates/user/fragments/profileCard.html
···
1
1
{{ define "user/fragments/profileCard" }}
2
-
{{ $userIdent := didOrHandle .UserDid .UserHandle }}
2
+
{{ $userIdent := resolve .UserDid }}
3
3
<div class="grid grid-cols-3 md:grid-cols-1 gap-1 items-center">
4
4
<div id="avatar" class="col-span-1 flex justify-center items-center">
5
5
<div class="w-3/4 aspect-square relative">
6
-
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ fullAvatar .UserDid }}" />
6
+
<img class="absolute inset-0 w-full h-full object-cover rounded-full p-2" src="{{ profileAvatarUrl .Profile "" }}" />
7
+
{{ if eq .FollowStatus.String "IsSelf" }}
8
+
<button
9
+
class="absolute bottom-2 right-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
10
+
popovertarget="avatar-upload-modal"
11
+
popovertargetaction="toggle"
12
+
title="Upload avatar">
13
+
{{ i "camera" "w-4 h-4" }}
14
+
</button>
15
+
{{ end }}
7
16
</div>
17
+
</div>
18
+
<div
19
+
id="avatar-upload-modal"
20
+
popover
21
+
class="bg-white w-full md:w-96 dark:bg-gray-800 p-4 rounded border border-gray-200 dark:border-gray-700 drop-shadow dark:text-white backdrop:bg-gray-400/50 dark:backdrop:bg-gray-800/50">
22
+
{{ template "user/fragments/editAvatar" . }}
8
23
</div>
9
24
<div class="col-span-2">
10
25
<div class="flex items-center flex-row flex-nowrap gap-2">
···
36
51
{{ block "followerFollowing" (list $ $userIdent) }} {{ end }}
37
52
</div>
38
53
39
-
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
54
+
<div class="flex flex-col gap-2 mb-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
40
55
{{ if .Location }}
41
56
<div class="flex items-center gap-2">
42
57
<span class="flex-shrink-0">{{ i "map-pin" "size-4" }}</span>
···
111
126
</div>
112
127
{{ end }}
113
128
{{ end }}
114
-
+2
-1
appview/pages/templates/user/fragments/repoCard.html
+2
-1
appview/pages/templates/user/fragments/repoCard.html
···
1
1
{{ define "user/fragments/repoCard" }}
2
+
{{/* root, repo, fullName [,starButton [,starData]] */}}
2
3
{{ $root := index . 0 }}
3
4
{{ $repo := index . 1 }}
4
5
{{ $fullName := index . 2 }}
···
29
30
</div>
30
31
{{ if and $starButton $root.LoggedInUser }}
31
32
<div class="shrink-0">
32
-
{{ template "repo/fragments/repoStar" $starData }}
33
+
{{ template "fragments/starBtn" $starData }}
33
34
</div>
34
35
{{ end }}
35
36
</div>
+22
-4
appview/pages/templates/user/overview.html
+22
-4
appview/pages/templates/user/overview.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }}{{ end }}
1
+
{{ define "title" }}{{ resolve .Card.UserDid }}{{ end }}
2
2
3
3
{{ define "profileContent" }}
4
4
<div id="all-repos" class="md:col-span-4 order-2 md:order-2">
···
16
16
<p class="text-sm font-bold px-2 pb-4 dark:text-white">ACTIVITY</p>
17
17
<div class="flex flex-col gap-4 relative">
18
18
{{ if .ProfileTimeline.IsEmpty }}
19
-
<p class="dark:text-white">This user does not have any activity yet.</p>
19
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
20
+
<span class="flex items-center gap-2">
21
+
This user does not have any activity yet.
22
+
</span>
23
+
</div>
20
24
{{ end }}
21
25
22
26
{{ with .ProfileTimeline }}
···
33
37
</p>
34
38
35
39
<div class="flex flex-col gap-1">
40
+
{{ block "commits" .Commits }} {{ end }}
36
41
{{ block "repoEvents" .RepoEvents }} {{ end }}
37
42
{{ block "issueEvents" .IssueEvents }} {{ end }}
38
43
{{ block "pullEvents" .PullEvents }} {{ end }}
···
43
48
{{ end }}
44
49
{{ end }}
45
50
</div>
51
+
{{ end }}
52
+
53
+
{{ define "commits" }}
54
+
{{ if . }}
55
+
<div class="flex flex-wrap items-center gap-1">
56
+
{{ i "git-commit-horizontal" "size-5" }}
57
+
created {{ . }} commits
58
+
</div>
59
+
{{ end }}
46
60
{{ end }}
47
61
48
62
{{ define "repoEvents" }}
···
224
238
{{ define "ownRepos" }}
225
239
<div>
226
240
<div class="text-sm font-bold px-2 pb-4 dark:text-white flex items-center gap-2">
227
-
<a href="/@{{ or $.Card.UserHandle $.Card.UserDid }}?tab=repos"
241
+
<a href="/{{ resolve $.Card.UserDid }}?tab=repos"
228
242
class="flex text-black dark:text-white items-center gap-2 no-underline hover:no-underline group">
229
243
<span>PINNED REPOS</span>
230
244
</a>
···
244
258
{{ template "user/fragments/repoCard" (list $ . false) }}
245
259
</div>
246
260
{{ else }}
247
-
<p class="dark:text-white">This user does not have any pinned repos.</p>
261
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
262
+
<span class="flex items-center gap-2">
263
+
This user does not have any pinned repos.
264
+
</span>
265
+
</div>
248
266
{{ end }}
249
267
</div>
250
268
</div>
+4
-2
appview/pages/templates/user/repos.html
+4
-2
appview/pages/templates/user/repos.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }}
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท repos {{ end }}
2
2
3
3
{{ define "profileContent" }}
4
4
<div id="all-repos" class="md:col-span-8 order-2 md:order-2">
···
13
13
{{ template "user/fragments/repoCard" (list $ . false) }}
14
14
</div>
15
15
{{ else }}
16
-
<p class="px-6 dark:text-white">This user does not have any repos yet.</p>
16
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
17
+
<span>This user does not have any repos yet.</span>
18
+
</div>
17
19
{{ end }}
18
20
</div>
19
21
{{ end }}
+4
-2
appview/pages/templates/user/settings/emails.html
+4
-2
appview/pages/templates/user/settings/emails.html
···
62
62
hx-swap="none"
63
63
class="flex flex-col gap-2"
64
64
>
65
-
<p class="uppercase p-0">ADD EMAIL</p>
65
+
<label for="email-address" class="uppercase p-0">
66
+
add email
67
+
</label>
66
68
<p class="text-sm text-gray-500 dark:text-gray-400">Commits using this email will be associated with your profile.</p>
67
69
<input
68
70
type="email"
···
91
93
<div id="settings-emails-error" class="text-red-500 dark:text-red-400"></div>
92
94
<div id="settings-emails-success" class="text-green-500 dark:text-green-400"></div>
93
95
</form>
94
-
{{ end }}
96
+
{{ end }}
+4
-2
appview/pages/templates/user/settings/keys.html
+4
-2
appview/pages/templates/user/settings/keys.html
···
21
21
<div class="col-span-1 md:col-span-2">
22
22
<h2 class="text-sm pb-2 uppercase font-bold">SSH Keys</h2>
23
23
<p class="text-gray-500 dark:text-gray-400">
24
-
SSH public keys added here will be broadcasted to knots that you are a member of,
24
+
SSH public keys added here will be broadcasted to knots that you are a member of,
25
25
allowing you to push to repositories there.
26
26
</p>
27
27
</div>
···
63
63
hx-swap="none"
64
64
class="flex flex-col gap-2"
65
65
>
66
-
<p class="uppercase p-0">ADD SSH KEY</p>
66
+
<label for="key-name" class="uppercase p-0">
67
+
add ssh key
68
+
</label>
67
69
<p class="text-sm text-gray-500 dark:text-gray-400">SSH keys allow you to push to repositories in knots you're a member of.</p>
68
70
<input
69
71
type="text"
+1
-1
appview/pages/templates/user/settings/notifications.html
+1
-1
appview/pages/templates/user/settings/notifications.html
···
151
151
</div>
152
152
</div>
153
153
<label class="flex items-center gap-2">
154
-
<input type="checkbox" name="mentioned" {{if .Preferences.UserMentioned}}checked{{end}}>
154
+
<input type="checkbox" name="user_mentioned" {{if .Preferences.UserMentioned}}checked{{end}}>
155
155
</label>
156
156
</div>
157
157
+4
-2
appview/pages/templates/user/starred.html
+4
-2
appview/pages/templates/user/starred.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท repos {{ end }}
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท repos {{ end }}
2
2
3
3
{{ define "profileContent" }}
4
4
<div id="all-repos" class="md:col-span-8 order-2 md:order-2">
···
13
13
{{ template "user/fragments/repoCard" (list $ . true) }}
14
14
</div>
15
15
{{ else }}
16
-
<p class="px-6 dark:text-white">This user does not have any starred repos yet.</p>
16
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
17
+
<span>This user does not have any starred repos yet.</span>
18
+
</div>
17
19
{{ end }}
18
20
</div>
19
21
{{ end }}
+5
-3
appview/pages/templates/user/strings.html
+5
-3
appview/pages/templates/user/strings.html
···
1
-
{{ define "title" }}{{ or .Card.UserHandle .Card.UserDid }} ยท strings {{ end }}
1
+
{{ define "title" }}{{ resolve .Card.UserDid }} ยท strings {{ end }}
2
2
3
3
{{ define "profileContent" }}
4
4
<div id="all-strings" class="md:col-span-8 order-2 md:order-2">
···
13
13
{{ template "singleString" (list $ .) }}
14
14
</div>
15
15
{{ else }}
16
-
<p class="px-6 dark:text-white">This user does not have any strings yet.</p>
16
+
<div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded">
17
+
<span>This user does not have any strings yet.</span>
18
+
</div>
17
19
{{ end }}
18
20
</div>
19
21
{{ end }}
···
23
25
{{ $s := index . 1 }}
24
26
<div class="py-4 px-6 rounded bg-white dark:bg-gray-800">
25
27
<div class="font-medium dark:text-white flex gap-2 items-center">
26
-
<a href="/strings/{{ or $root.Card.UserHandle $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
28
+
<a href="/strings/{{ resolve $root.Card.UserDid }}/{{ $s.Rkey }}">{{ $s.Filename }}</a>
27
29
</div>
28
30
{{ with $s.Description }}
29
31
<div class="text-gray-600 dark:text-gray-300 text-sm">
+16
-22
appview/pipelines/pipelines.go
+16
-22
appview/pipelines/pipelines.go
···
16
16
"tangled.org/core/appview/reporesolver"
17
17
"tangled.org/core/eventconsumer"
18
18
"tangled.org/core/idresolver"
19
+
"tangled.org/core/orm"
19
20
"tangled.org/core/rbac"
20
21
spindlemodel "tangled.org/core/spindle/models"
21
22
···
78
79
return
79
80
}
80
81
81
-
repoInfo := f.RepoInfo(user)
82
-
83
82
ps, err := db.GetPipelineStatuses(
84
83
p.db,
85
84
30,
86
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
87
-
db.FilterEq("repo_name", repoInfo.Name),
88
-
db.FilterEq("knot", repoInfo.Knot),
85
+
orm.FilterEq("repo_owner", f.Did),
86
+
orm.FilterEq("repo_name", f.Name),
87
+
orm.FilterEq("knot", f.Knot),
89
88
)
90
89
if err != nil {
91
90
l.Error("failed to query db", "err", err)
···
94
93
95
94
p.pages.Pipelines(w, pages.PipelinesParams{
96
95
LoggedInUser: user,
97
-
RepoInfo: repoInfo,
96
+
RepoInfo: p.repoResolver.GetRepoInfo(r, user),
98
97
Pipelines: ps,
99
98
})
100
99
}
···
108
107
l.Error("failed to get repo and knot", "err", err)
109
108
return
110
109
}
111
-
112
-
repoInfo := f.RepoInfo(user)
113
110
114
111
pipelineId := chi.URLParam(r, "pipeline")
115
112
if pipelineId == "" {
···
126
123
ps, err := db.GetPipelineStatuses(
127
124
p.db,
128
125
1,
129
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
130
-
db.FilterEq("repo_name", repoInfo.Name),
131
-
db.FilterEq("knot", repoInfo.Knot),
132
-
db.FilterEq("id", pipelineId),
126
+
orm.FilterEq("repo_owner", f.Did),
127
+
orm.FilterEq("repo_name", f.Name),
128
+
orm.FilterEq("knot", f.Knot),
129
+
orm.FilterEq("id", pipelineId),
133
130
)
134
131
if err != nil {
135
132
l.Error("failed to query db", "err", err)
···
145
142
146
143
p.pages.Workflow(w, pages.WorkflowParams{
147
144
LoggedInUser: user,
148
-
RepoInfo: repoInfo,
145
+
RepoInfo: p.repoResolver.GetRepoInfo(r, user),
149
146
Pipeline: singlePipeline,
150
147
Workflow: workflow,
151
148
})
···
176
173
ctx, cancel := context.WithCancel(r.Context())
177
174
defer cancel()
178
175
179
-
user := p.oauth.GetUser(r)
180
176
f, err := p.repoResolver.Resolve(r)
181
177
if err != nil {
182
178
l.Error("failed to get repo and knot", "err", err)
···
184
180
return
185
181
}
186
182
187
-
repoInfo := f.RepoInfo(user)
188
-
189
183
pipelineId := chi.URLParam(r, "pipeline")
190
184
workflow := chi.URLParam(r, "workflow")
191
185
if pipelineId == "" || workflow == "" {
···
196
190
ps, err := db.GetPipelineStatuses(
197
191
p.db,
198
192
1,
199
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
200
-
db.FilterEq("repo_name", repoInfo.Name),
201
-
db.FilterEq("knot", repoInfo.Knot),
202
-
db.FilterEq("id", pipelineId),
193
+
orm.FilterEq("repo_owner", f.Did),
194
+
orm.FilterEq("repo_name", f.Name),
195
+
orm.FilterEq("knot", f.Knot),
196
+
orm.FilterEq("id", pipelineId),
203
197
)
204
198
if err != nil || len(ps) != 1 {
205
199
l.Error("pipeline query failed", "err", err, "count", len(ps))
···
208
202
}
209
203
210
204
singlePipeline := ps[0]
211
-
spindle := repoInfo.Spindle
212
-
knot := repoInfo.Knot
205
+
spindle := f.Spindle
206
+
knot := f.Knot
213
207
rkey := singlePipeline.Rkey
214
208
215
209
if spindle == "" || knot == "" || rkey == "" {
+3
-2
appview/pulls/opengraph.go
+3
-2
appview/pulls/opengraph.go
···
13
13
"tangled.org/core/appview/db"
14
14
"tangled.org/core/appview/models"
15
15
"tangled.org/core/appview/ogcard"
16
+
"tangled.org/core/orm"
16
17
"tangled.org/core/patchutil"
17
18
"tangled.org/core/types"
18
19
)
···
276
277
}
277
278
278
279
// Get comment count from database
279
-
comments, err := db.GetPullComments(s.db, db.FilterEq("pull_id", pull.ID))
280
+
comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID))
280
281
if err != nil {
281
282
log.Printf("failed to get pull comments: %v", err)
282
283
}
···
293
294
filesChanged = niceDiff.Stat.FilesChanged
294
295
}
295
296
296
-
card, err := s.drawPullSummaryCard(pull, &f.Repo, commentCount, diffStats, filesChanged)
297
+
card, err := s.drawPullSummaryCard(pull, f, commentCount, diffStats, filesChanged)
297
298
if err != nil {
298
299
log.Println("failed to draw pull summary card", err)
299
300
http.Error(w, "failed to draw pull summary card", http.StatusInternalServerError)
+138
-142
appview/pulls/pulls.go
+138
-142
appview/pulls/pulls.go
···
1
1
package pulls
2
2
3
3
import (
4
+
"context"
4
5
"database/sql"
5
6
"encoding/json"
6
7
"errors"
···
18
19
"tangled.org/core/appview/config"
19
20
"tangled.org/core/appview/db"
20
21
pulls_indexer "tangled.org/core/appview/indexer/pulls"
22
+
"tangled.org/core/appview/mentions"
21
23
"tangled.org/core/appview/models"
22
24
"tangled.org/core/appview/notify"
23
25
"tangled.org/core/appview/oauth"
24
26
"tangled.org/core/appview/pages"
25
27
"tangled.org/core/appview/pages/markup"
28
+
"tangled.org/core/appview/pages/repoinfo"
26
29
"tangled.org/core/appview/reporesolver"
27
30
"tangled.org/core/appview/validator"
28
31
"tangled.org/core/appview/xrpcclient"
29
32
"tangled.org/core/idresolver"
33
+
"tangled.org/core/orm"
30
34
"tangled.org/core/patchutil"
31
35
"tangled.org/core/rbac"
32
36
"tangled.org/core/tid"
···
41
45
)
42
46
43
47
type Pulls struct {
44
-
oauth *oauth.OAuth
45
-
repoResolver *reporesolver.RepoResolver
46
-
pages *pages.Pages
47
-
idResolver *idresolver.Resolver
48
-
db *db.DB
49
-
config *config.Config
50
-
notifier notify.Notifier
51
-
enforcer *rbac.Enforcer
52
-
logger *slog.Logger
53
-
validator *validator.Validator
54
-
indexer *pulls_indexer.Indexer
48
+
oauth *oauth.OAuth
49
+
repoResolver *reporesolver.RepoResolver
50
+
pages *pages.Pages
51
+
idResolver *idresolver.Resolver
52
+
mentionsResolver *mentions.Resolver
53
+
db *db.DB
54
+
config *config.Config
55
+
notifier notify.Notifier
56
+
enforcer *rbac.Enforcer
57
+
logger *slog.Logger
58
+
validator *validator.Validator
59
+
indexer *pulls_indexer.Indexer
55
60
}
56
61
57
62
func New(
···
59
64
repoResolver *reporesolver.RepoResolver,
60
65
pages *pages.Pages,
61
66
resolver *idresolver.Resolver,
67
+
mentionsResolver *mentions.Resolver,
62
68
db *db.DB,
63
69
config *config.Config,
64
70
notifier notify.Notifier,
···
68
74
logger *slog.Logger,
69
75
) *Pulls {
70
76
return &Pulls{
71
-
oauth: oauth,
72
-
repoResolver: repoResolver,
73
-
pages: pages,
74
-
idResolver: resolver,
75
-
db: db,
76
-
config: config,
77
-
notifier: notifier,
78
-
enforcer: enforcer,
79
-
logger: logger,
80
-
validator: validator,
81
-
indexer: indexer,
77
+
oauth: oauth,
78
+
repoResolver: repoResolver,
79
+
pages: pages,
80
+
idResolver: resolver,
81
+
mentionsResolver: mentionsResolver,
82
+
db: db,
83
+
config: config,
84
+
notifier: notifier,
85
+
enforcer: enforcer,
86
+
logger: logger,
87
+
validator: validator,
88
+
indexer: indexer,
82
89
}
83
90
}
84
91
···
123
130
124
131
s.pages.PullActionsFragment(w, pages.PullActionsParams{
125
132
LoggedInUser: user,
126
-
RepoInfo: f.RepoInfo(user),
133
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
127
134
Pull: pull,
128
135
RoundNumber: roundNumber,
129
136
MergeCheck: mergeCheckResponse,
···
150
157
return
151
158
}
152
159
160
+
backlinks, err := db.GetBacklinks(s.db, pull.AtUri())
161
+
if err != nil {
162
+
log.Println("failed to get pull backlinks", err)
163
+
s.pages.Notice(w, "pull-error", "Failed to get pull. Try again later.")
164
+
return
165
+
}
166
+
153
167
// can be nil if this pull is not stacked
154
168
stack, _ := r.Context().Value("stack").(models.Stack)
155
169
abandonedPulls, _ := r.Context().Value("abandonedPulls").([]*models.Pull)
···
160
174
if user != nil && user.Did == pull.OwnerDid {
161
175
resubmitResult = s.resubmitCheck(r, f, pull, stack)
162
176
}
163
-
164
-
repoInfo := f.RepoInfo(user)
165
177
166
178
m := make(map[string]models.Pipeline)
167
179
···
179
191
ps, err := db.GetPipelineStatuses(
180
192
s.db,
181
193
len(shas),
182
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
183
-
db.FilterEq("repo_name", repoInfo.Name),
184
-
db.FilterEq("knot", repoInfo.Knot),
185
-
db.FilterIn("sha", shas),
194
+
orm.FilterEq("repo_owner", f.Did),
195
+
orm.FilterEq("repo_name", f.Name),
196
+
orm.FilterEq("knot", f.Knot),
197
+
orm.FilterIn("sha", shas),
186
198
)
187
199
if err != nil {
188
200
log.Printf("failed to fetch pipeline statuses: %s", err)
···
206
218
207
219
labelDefs, err := db.GetLabelDefinitions(
208
220
s.db,
209
-
db.FilterIn("at_uri", f.Repo.Labels),
210
-
db.FilterContains("scope", tangled.RepoPullNSID),
221
+
orm.FilterIn("at_uri", f.Labels),
222
+
orm.FilterContains("scope", tangled.RepoPullNSID),
211
223
)
212
224
if err != nil {
213
225
log.Println("failed to fetch labels", err)
···
222
234
223
235
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
224
236
LoggedInUser: user,
225
-
RepoInfo: repoInfo,
237
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
226
238
Pull: pull,
227
239
Stack: stack,
228
240
AbandonedPulls: abandonedPulls,
241
+
Backlinks: backlinks,
229
242
BranchDeleteStatus: branchDeleteStatus,
230
243
MergeCheck: mergeCheckResponse,
231
244
ResubmitCheck: resubmitResult,
···
239
252
})
240
253
}
241
254
242
-
func (s *Pulls) mergeCheck(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse {
255
+
func (s *Pulls) mergeCheck(r *http.Request, f *models.Repo, pull *models.Pull, stack models.Stack) types.MergeCheckResponse {
243
256
if pull.State == models.PullMerged {
244
257
return types.MergeCheckResponse{}
245
258
}
···
268
281
r.Context(),
269
282
&xrpcc,
270
283
&tangled.RepoMergeCheck_Input{
271
-
Did: f.OwnerDid(),
284
+
Did: f.Did,
272
285
Name: f.Name,
273
286
Branch: pull.TargetBranch,
274
287
Patch: patch,
···
306
319
return result
307
320
}
308
321
309
-
func (s *Pulls) branchDeleteStatus(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull) *models.BranchDeleteStatus {
322
+
func (s *Pulls) branchDeleteStatus(r *http.Request, repo *models.Repo, pull *models.Pull) *models.BranchDeleteStatus {
310
323
if pull.State != models.PullMerged {
311
324
return nil
312
325
}
···
317
330
}
318
331
319
332
var branch string
320
-
var repo *models.Repo
321
333
// check if the branch exists
322
334
// NOTE: appview could cache branches/tags etc. for every repo by listening for gitRefUpdates
323
335
if pull.IsBranchBased() {
324
336
branch = pull.PullSource.Branch
325
-
repo = &f.Repo
326
337
} else if pull.IsForkBased() {
327
338
branch = pull.PullSource.Branch
328
339
repo = pull.PullSource.Repo
···
361
372
}
362
373
}
363
374
364
-
func (s *Pulls) resubmitCheck(r *http.Request, f *reporesolver.ResolvedRepo, pull *models.Pull, stack models.Stack) pages.ResubmitResult {
375
+
func (s *Pulls) resubmitCheck(r *http.Request, repo *models.Repo, pull *models.Pull, stack models.Stack) pages.ResubmitResult {
365
376
if pull.State == models.PullMerged || pull.State == models.PullDeleted || pull.PullSource == nil {
366
377
return pages.Unknown
367
378
}
···
381
392
repoName = sourceRepo.Name
382
393
} else {
383
394
// pulls within the same repo
384
-
knot = f.Knot
385
-
ownerDid = f.OwnerDid()
386
-
repoName = f.Name
395
+
knot = repo.Knot
396
+
ownerDid = repo.Did
397
+
repoName = repo.Name
387
398
}
388
399
389
400
scheme := "http"
···
395
406
Host: host,
396
407
}
397
408
398
-
repo := fmt.Sprintf("%s/%s", ownerDid, repoName)
399
-
branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, repo)
409
+
didSlashName := fmt.Sprintf("%s/%s", ownerDid, repoName)
410
+
branchResp, err := tangled.RepoBranch(r.Context(), xrpcc, pull.PullSource.Branch, didSlashName)
400
411
if err != nil {
401
412
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
402
413
log.Println("failed to call XRPC repo.branches", xrpcerr)
···
424
435
425
436
func (s *Pulls) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
426
437
user := s.oauth.GetUser(r)
427
-
f, err := s.repoResolver.Resolve(r)
428
-
if err != nil {
429
-
log.Println("failed to get repo and knot", err)
430
-
return
431
-
}
432
438
433
439
var diffOpts types.DiffOpts
434
440
if d := r.URL.Query().Get("diff"); d == "split" {
···
457
463
458
464
s.pages.RepoPullPatchPage(w, pages.RepoPullPatchParams{
459
465
LoggedInUser: user,
460
-
RepoInfo: f.RepoInfo(user),
466
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
461
467
Pull: pull,
462
468
Stack: stack,
463
469
Round: roundIdInt,
···
471
477
func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) {
472
478
user := s.oauth.GetUser(r)
473
479
474
-
f, err := s.repoResolver.Resolve(r)
475
-
if err != nil {
476
-
log.Println("failed to get repo and knot", err)
477
-
return
478
-
}
479
-
480
480
var diffOpts types.DiffOpts
481
481
if d := r.URL.Query().Get("diff"); d == "split" {
482
482
diffOpts.Split = true
···
521
521
522
522
s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{
523
523
LoggedInUser: s.oauth.GetUser(r),
524
-
RepoInfo: f.RepoInfo(user),
524
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
525
525
Pull: pull,
526
526
Round: roundIdInt,
527
527
Interdiff: interdiff,
···
598
598
599
599
pulls, err := db.GetPulls(
600
600
s.db,
601
-
db.FilterIn("id", ids),
601
+
orm.FilterIn("id", ids),
602
602
)
603
603
if err != nil {
604
604
log.Println("failed to get pulls", err)
···
646
646
}
647
647
pulls = pulls[:n]
648
648
649
-
repoInfo := f.RepoInfo(user)
650
649
ps, err := db.GetPipelineStatuses(
651
650
s.db,
652
651
len(shas),
653
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
654
-
db.FilterEq("repo_name", repoInfo.Name),
655
-
db.FilterEq("knot", repoInfo.Knot),
656
-
db.FilterIn("sha", shas),
652
+
orm.FilterEq("repo_owner", f.Did),
653
+
orm.FilterEq("repo_name", f.Name),
654
+
orm.FilterEq("knot", f.Knot),
655
+
orm.FilterIn("sha", shas),
657
656
)
658
657
if err != nil {
659
658
log.Printf("failed to fetch pipeline statuses: %s", err)
···
666
665
667
666
labelDefs, err := db.GetLabelDefinitions(
668
667
s.db,
669
-
db.FilterIn("at_uri", f.Repo.Labels),
670
-
db.FilterContains("scope", tangled.RepoPullNSID),
668
+
orm.FilterIn("at_uri", f.Labels),
669
+
orm.FilterContains("scope", tangled.RepoPullNSID),
671
670
)
672
671
if err != nil {
673
672
log.Println("failed to fetch labels", err)
···
682
681
683
682
s.pages.RepoPulls(w, pages.RepoPullsParams{
684
683
LoggedInUser: s.oauth.GetUser(r),
685
-
RepoInfo: f.RepoInfo(user),
684
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
686
685
Pulls: pulls,
687
686
LabelDefs: defs,
688
687
FilteringBy: state,
···
693
692
}
694
693
695
694
func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) {
696
-
l := s.logger.With("handler", "PullComment")
697
695
user := s.oauth.GetUser(r)
698
696
f, err := s.repoResolver.Resolve(r)
699
697
if err != nil {
···
720
718
case http.MethodGet:
721
719
s.pages.PullNewCommentFragment(w, pages.PullNewCommentParams{
722
720
LoggedInUser: user,
723
-
RepoInfo: f.RepoInfo(user),
721
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
724
722
Pull: pull,
725
723
RoundNumber: roundNumber,
726
724
})
···
732
730
return
733
731
}
734
732
733
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
734
+
735
735
// Start a transaction
736
736
tx, err := s.db.BeginTx(r.Context(), nil)
737
737
if err != nil {
···
774
774
Body: body,
775
775
CommentAt: atResp.Uri,
776
776
SubmissionId: pull.Submissions[roundNumber].ID,
777
+
Mentions: mentions,
778
+
References: references,
777
779
}
778
780
779
781
// Create the pull comment in the database with the commentAt field
···
791
793
return
792
794
}
793
795
794
-
rawMentions := markup.FindUserMentions(comment.Body)
795
-
idents := s.idResolver.ResolveIdents(r.Context(), rawMentions)
796
-
l.Debug("parsed mentions", "raw", rawMentions, "idents", idents)
797
-
var mentions []syntax.DID
798
-
for _, ident := range idents {
799
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
800
-
mentions = append(mentions, ident.DID)
801
-
}
802
-
}
803
796
s.notifier.NewPullComment(r.Context(), comment, mentions)
804
797
805
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId))
798
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
799
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", ownerSlashRepo, pull.PullId, commentId))
806
800
return
807
801
}
808
802
}
···
826
820
Host: host,
827
821
}
828
822
829
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
823
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
830
824
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
831
825
if err != nil {
832
826
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
853
847
854
848
s.pages.RepoNewPull(w, pages.RepoNewPullParams{
855
849
LoggedInUser: user,
856
-
RepoInfo: f.RepoInfo(user),
850
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
857
851
Branches: result.Branches,
858
852
Strategy: strategy,
859
853
SourceBranch: sourceBranch,
···
876
870
}
877
871
878
872
// Determine PR type based on input parameters
879
-
isPushAllowed := f.RepoInfo(user).Roles.IsPushAllowed()
873
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
874
+
isPushAllowed := roles.IsPushAllowed()
880
875
isBranchBased := isPushAllowed && sourceBranch != "" && fromFork == ""
881
876
isForkBased := fromFork != "" && sourceBranch != ""
882
877
isPatchBased := patch != "" && !isBranchBased && !isForkBased
···
974
969
func (s *Pulls) handleBranchBasedPull(
975
970
w http.ResponseWriter,
976
971
r *http.Request,
977
-
f *reporesolver.ResolvedRepo,
972
+
repo *models.Repo,
978
973
user *oauth.User,
979
974
title,
980
975
body,
···
986
981
if !s.config.Core.Dev {
987
982
scheme = "https"
988
983
}
989
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
984
+
host := fmt.Sprintf("%s://%s", scheme, repo.Knot)
990
985
xrpcc := &indigoxrpc.Client{
991
986
Host: host,
992
987
}
993
988
994
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
995
-
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, targetBranch, sourceBranch)
989
+
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
990
+
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, didSlashRepo, targetBranch, sourceBranch)
996
991
if err != nil {
997
992
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
998
993
log.Println("failed to call XRPC repo.compare", xrpcerr)
···
1029
1024
Sha: comparison.Rev2,
1030
1025
}
1031
1026
1032
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1027
+
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1033
1028
}
1034
1029
1035
-
func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) {
1030
+
func (s *Pulls) handlePatchBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, title, body, targetBranch, patch string, isStacked bool) {
1036
1031
if err := s.validator.ValidatePatch(&patch); err != nil {
1037
1032
s.logger.Error("patch validation failed", "err", err)
1038
1033
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
1039
1034
return
1040
1035
}
1041
1036
1042
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked)
1037
+
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, "", "", nil, nil, isStacked)
1043
1038
}
1044
1039
1045
-
func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, f *reporesolver.ResolvedRepo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) {
1040
+
func (s *Pulls) handleForkBasedPull(w http.ResponseWriter, r *http.Request, repo *models.Repo, user *oauth.User, forkRepo string, title, body, targetBranch, sourceBranch string, isStacked bool) {
1046
1041
repoString := strings.SplitN(forkRepo, "/", 2)
1047
1042
forkOwnerDid := repoString[0]
1048
1043
repoName := repoString[1]
···
1144
1139
Sha: sourceRev,
1145
1140
}
1146
1141
1147
-
s.createPullRequest(w, r, f, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1142
+
s.createPullRequest(w, r, repo, user, title, body, targetBranch, patch, combined, sourceRev, pullSource, recordPullSource, isStacked)
1148
1143
}
1149
1144
1150
1145
func (s *Pulls) createPullRequest(
1151
1146
w http.ResponseWriter,
1152
1147
r *http.Request,
1153
-
f *reporesolver.ResolvedRepo,
1148
+
repo *models.Repo,
1154
1149
user *oauth.User,
1155
1150
title, body, targetBranch string,
1156
1151
patch string,
···
1165
1160
s.createStackedPullRequest(
1166
1161
w,
1167
1162
r,
1168
-
f,
1163
+
repo,
1169
1164
user,
1170
1165
targetBranch,
1171
1166
patch,
···
1211
1206
}
1212
1207
}
1213
1208
1209
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
1210
+
1214
1211
rkey := tid.TID()
1215
1212
initialSubmission := models.PullSubmission{
1216
1213
Patch: patch,
···
1222
1219
Body: body,
1223
1220
TargetBranch: targetBranch,
1224
1221
OwnerDid: user.Did,
1225
-
RepoAt: f.RepoAt(),
1222
+
RepoAt: repo.RepoAt(),
1226
1223
Rkey: rkey,
1224
+
Mentions: mentions,
1225
+
References: references,
1227
1226
Submissions: []*models.PullSubmission{
1228
1227
&initialSubmission,
1229
1228
},
···
1235
1234
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1236
1235
return
1237
1236
}
1238
-
pullId, err := db.NextPullId(tx, f.RepoAt())
1237
+
pullId, err := db.NextPullId(tx, repo.RepoAt())
1239
1238
if err != nil {
1240
1239
log.Println("failed to get pull id", err)
1241
1240
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
···
1250
1249
Val: &tangled.RepoPull{
1251
1250
Title: title,
1252
1251
Target: &tangled.RepoPull_Target{
1253
-
Repo: string(f.RepoAt()),
1252
+
Repo: string(repo.RepoAt()),
1254
1253
Branch: targetBranch,
1255
1254
},
1256
1255
Patch: patch,
···
1273
1272
1274
1273
s.notifier.NewPull(r.Context(), pull)
1275
1274
1276
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
1275
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
1276
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pullId))
1277
1277
}
1278
1278
1279
1279
func (s *Pulls) createStackedPullRequest(
1280
1280
w http.ResponseWriter,
1281
1281
r *http.Request,
1282
-
f *reporesolver.ResolvedRepo,
1282
+
repo *models.Repo,
1283
1283
user *oauth.User,
1284
1284
targetBranch string,
1285
1285
patch string,
···
1311
1311
1312
1312
// build a stack out of this patch
1313
1313
stackId := uuid.New()
1314
-
stack, err := newStack(f, user, targetBranch, patch, pullSource, stackId.String())
1314
+
stack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pullSource, stackId.String())
1315
1315
if err != nil {
1316
1316
log.Println("failed to create stack", err)
1317
1317
s.pages.Notice(w, "pull", fmt.Sprintf("Failed to create stack: %v", err))
···
1374
1374
return
1375
1375
}
1376
1376
1377
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", f.OwnerSlashRepo()))
1377
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
1378
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls", ownerSlashRepo))
1378
1379
}
1379
1380
1380
1381
func (s *Pulls) ValidatePatch(w http.ResponseWriter, r *http.Request) {
···
1405
1406
1406
1407
func (s *Pulls) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {
1407
1408
user := s.oauth.GetUser(r)
1408
-
f, err := s.repoResolver.Resolve(r)
1409
-
if err != nil {
1410
-
log.Println("failed to get repo and knot", err)
1411
-
return
1412
-
}
1413
1409
1414
1410
s.pages.PullPatchUploadFragment(w, pages.PullPatchUploadParams{
1415
-
RepoInfo: f.RepoInfo(user),
1411
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1416
1412
})
1417
1413
}
1418
1414
···
1433
1429
Host: host,
1434
1430
}
1435
1431
1436
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1432
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1437
1433
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
1438
1434
if err != nil {
1439
1435
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1466
1462
}
1467
1463
1468
1464
s.pages.PullCompareBranchesFragment(w, pages.PullCompareBranchesParams{
1469
-
RepoInfo: f.RepoInfo(user),
1465
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1470
1466
Branches: withoutDefault,
1471
1467
})
1472
1468
}
1473
1469
1474
1470
func (s *Pulls) CompareForksFragment(w http.ResponseWriter, r *http.Request) {
1475
1471
user := s.oauth.GetUser(r)
1476
-
f, err := s.repoResolver.Resolve(r)
1477
-
if err != nil {
1478
-
log.Println("failed to get repo and knot", err)
1479
-
return
1480
-
}
1481
1472
1482
1473
forks, err := db.GetForksByDid(s.db, user.Did)
1483
1474
if err != nil {
···
1486
1477
}
1487
1478
1488
1479
s.pages.PullCompareForkFragment(w, pages.PullCompareForkParams{
1489
-
RepoInfo: f.RepoInfo(user),
1480
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1490
1481
Forks: forks,
1491
1482
Selected: r.URL.Query().Get("fork"),
1492
1483
})
···
1508
1499
// fork repo
1509
1500
repo, err := db.GetRepo(
1510
1501
s.db,
1511
-
db.FilterEq("did", forkOwnerDid),
1512
-
db.FilterEq("name", forkName),
1502
+
orm.FilterEq("did", forkOwnerDid),
1503
+
orm.FilterEq("name", forkName),
1513
1504
)
1514
1505
if err != nil {
1515
1506
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
1554
1545
Host: targetHost,
1555
1546
}
1556
1547
1557
-
targetRepo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1548
+
targetRepo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1558
1549
targetXrpcBytes, err := tangled.RepoBranches(r.Context(), targetXrpcc, "", 0, targetRepo)
1559
1550
if err != nil {
1560
1551
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1579
1570
})
1580
1571
1581
1572
s.pages.PullCompareForkBranchesFragment(w, pages.PullCompareForkBranchesParams{
1582
-
RepoInfo: f.RepoInfo(user),
1573
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1583
1574
SourceBranches: sourceBranches.Branches,
1584
1575
TargetBranches: targetBranches.Branches,
1585
1576
})
···
1587
1578
1588
1579
func (s *Pulls) ResubmitPull(w http.ResponseWriter, r *http.Request) {
1589
1580
user := s.oauth.GetUser(r)
1590
-
f, err := s.repoResolver.Resolve(r)
1591
-
if err != nil {
1592
-
log.Println("failed to get repo and knot", err)
1593
-
return
1594
-
}
1595
1581
1596
1582
pull, ok := r.Context().Value("pull").(*models.Pull)
1597
1583
if !ok {
···
1603
1589
switch r.Method {
1604
1590
case http.MethodGet:
1605
1591
s.pages.PullResubmitFragment(w, pages.PullResubmitParams{
1606
-
RepoInfo: f.RepoInfo(user),
1592
+
RepoInfo: s.repoResolver.GetRepoInfo(r, user),
1607
1593
Pull: pull,
1608
1594
})
1609
1595
return
···
1670
1656
return
1671
1657
}
1672
1658
1673
-
if !f.RepoInfo(user).Roles.IsPushAllowed() {
1659
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
1660
+
if !roles.IsPushAllowed() {
1674
1661
log.Println("unauthorized user")
1675
1662
w.WriteHeader(http.StatusUnauthorized)
1676
1663
return
···
1685
1672
Host: host,
1686
1673
}
1687
1674
1688
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
1675
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
1689
1676
xrpcBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, pull.TargetBranch, pull.PullSource.Branch)
1690
1677
if err != nil {
1691
1678
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
1812
1799
func (s *Pulls) resubmitPullHelper(
1813
1800
w http.ResponseWriter,
1814
1801
r *http.Request,
1815
-
f *reporesolver.ResolvedRepo,
1802
+
repo *models.Repo,
1816
1803
user *oauth.User,
1817
1804
pull *models.Pull,
1818
1805
patch string,
···
1821
1808
) {
1822
1809
if pull.IsStacked() {
1823
1810
log.Println("resubmitting stacked PR")
1824
-
s.resubmitStackedPullHelper(w, r, f, user, pull, patch, pull.StackId)
1811
+
s.resubmitStackedPullHelper(w, r, repo, user, pull, patch, pull.StackId)
1825
1812
return
1826
1813
}
1827
1814
···
1901
1888
Val: &tangled.RepoPull{
1902
1889
Title: pull.Title,
1903
1890
Target: &tangled.RepoPull_Target{
1904
-
Repo: string(f.RepoAt()),
1891
+
Repo: string(repo.RepoAt()),
1905
1892
Branch: pull.TargetBranch,
1906
1893
},
1907
1894
Patch: patch, // new patch
···
1922
1909
return
1923
1910
}
1924
1911
1925
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
1912
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
1913
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
1926
1914
}
1927
1915
1928
1916
func (s *Pulls) resubmitStackedPullHelper(
1929
1917
w http.ResponseWriter,
1930
1918
r *http.Request,
1931
-
f *reporesolver.ResolvedRepo,
1919
+
repo *models.Repo,
1932
1920
user *oauth.User,
1933
1921
pull *models.Pull,
1934
1922
patch string,
···
1937
1925
targetBranch := pull.TargetBranch
1938
1926
1939
1927
origStack, _ := r.Context().Value("stack").(models.Stack)
1940
-
newStack, err := newStack(f, user, targetBranch, patch, pull.PullSource, stackId)
1928
+
newStack, err := s.newStack(r.Context(), repo, user, targetBranch, patch, pull.PullSource, stackId)
1941
1929
if err != nil {
1942
1930
log.Println("failed to create resubmitted stack", err)
1943
1931
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
2079
2067
tx,
2080
2068
p.ParentChangeId,
2081
2069
// these should be enough filters to be unique per-stack
2082
-
db.FilterEq("repo_at", p.RepoAt.String()),
2083
-
db.FilterEq("owner_did", p.OwnerDid),
2084
-
db.FilterEq("change_id", p.ChangeId),
2070
+
orm.FilterEq("repo_at", p.RepoAt.String()),
2071
+
orm.FilterEq("owner_did", p.OwnerDid),
2072
+
orm.FilterEq("change_id", p.ChangeId),
2085
2073
)
2086
2074
2087
2075
if err != nil {
···
2115
2103
return
2116
2104
}
2117
2105
2118
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2106
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
2107
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2119
2108
}
2120
2109
2121
2110
func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
···
2168
2157
2169
2158
authorName := ident.Handle.String()
2170
2159
mergeInput := &tangled.RepoMerge_Input{
2171
-
Did: f.OwnerDid(),
2160
+
Did: f.Did,
2172
2161
Name: f.Name,
2173
2162
Branch: pull.TargetBranch,
2174
2163
Patch: patch,
···
2233
2222
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2234
2223
}
2235
2224
2236
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.Name, pull.PullId))
2225
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2226
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2237
2227
}
2238
2228
2239
2229
func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
···
2253
2243
}
2254
2244
2255
2245
// auth filter: only owner or collaborators can close
2256
-
roles := f.RolesInRepo(user)
2246
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2257
2247
isOwner := roles.IsOwner()
2258
2248
isCollaborator := roles.IsCollaborator()
2259
2249
isPullAuthor := user.Did == pull.OwnerDid
···
2305
2295
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2306
2296
}
2307
2297
2308
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2298
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2299
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2309
2300
}
2310
2301
2311
2302
func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
···
2326
2317
}
2327
2318
2328
2319
// auth filter: only owner or collaborators can close
2329
-
roles := f.RolesInRepo(user)
2320
+
roles := repoinfo.RolesInRepo{Roles: s.enforcer.GetPermissionsInRepo(user.Did, f.Knot, f.DidSlashRepo())}
2330
2321
isOwner := roles.IsOwner()
2331
2322
isCollaborator := roles.IsCollaborator()
2332
2323
isPullAuthor := user.Did == pull.OwnerDid
···
2378
2369
s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
2379
2370
}
2380
2371
2381
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
2372
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
2373
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
2382
2374
}
2383
2375
2384
-
func newStack(f *reporesolver.ResolvedRepo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
2376
+
func (s *Pulls) newStack(ctx context.Context, repo *models.Repo, user *oauth.User, targetBranch, patch string, pullSource *models.PullSource, stackId string) (models.Stack, error) {
2385
2377
formatPatches, err := patchutil.ExtractPatches(patch)
2386
2378
if err != nil {
2387
2379
return nil, fmt.Errorf("Failed to extract patches: %v", err)
···
2406
2398
body := fp.Body
2407
2399
rkey := tid.TID()
2408
2400
2401
+
mentions, references := s.mentionsResolver.Resolve(ctx, body)
2402
+
2409
2403
initialSubmission := models.PullSubmission{
2410
2404
Patch: fp.Raw,
2411
2405
SourceRev: fp.SHA,
···
2416
2410
Body: body,
2417
2411
TargetBranch: targetBranch,
2418
2412
OwnerDid: user.Did,
2419
-
RepoAt: f.RepoAt(),
2413
+
RepoAt: repo.RepoAt(),
2420
2414
Rkey: rkey,
2415
+
Mentions: mentions,
2416
+
References: references,
2421
2417
Submissions: []*models.PullSubmission{
2422
2418
&initialSubmission,
2423
2419
},
+2
-2
appview/repo/archive.go
+2
-2
appview/repo/archive.go
···
31
31
xrpcc := &indigoxrpc.Client{
32
32
Host: host,
33
33
}
34
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
35
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
34
+
didSlashRepo := f.DidSlashRepo()
35
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, didSlashRepo)
36
36
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
37
37
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
38
38
rp.pages.Error503(w)
+21
-14
appview/repo/artifact.go
+21
-14
appview/repo/artifact.go
···
14
14
"tangled.org/core/appview/db"
15
15
"tangled.org/core/appview/models"
16
16
"tangled.org/core/appview/pages"
17
-
"tangled.org/core/appview/reporesolver"
18
17
"tangled.org/core/appview/xrpcclient"
18
+
"tangled.org/core/orm"
19
19
"tangled.org/core/tid"
20
20
"tangled.org/core/types"
21
21
···
131
131
132
132
rp.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
133
133
LoggedInUser: user,
134
-
RepoInfo: f.RepoInfo(user),
134
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
135
135
Artifact: artifact,
136
136
})
137
137
}
···
156
156
157
157
artifacts, err := db.GetArtifact(
158
158
rp.db,
159
-
db.FilterEq("repo_at", f.RepoAt()),
160
-
db.FilterEq("tag", tag.Tag.Hash[:]),
161
-
db.FilterEq("name", filename),
159
+
orm.FilterEq("repo_at", f.RepoAt()),
160
+
orm.FilterEq("tag", tag.Tag.Hash[:]),
161
+
orm.FilterEq("name", filename),
162
162
)
163
163
if err != nil {
164
164
log.Println("failed to get artifacts", err)
···
174
174
175
175
artifact := artifacts[0]
176
176
177
-
ownerPds := f.OwnerId.PDSEndpoint()
177
+
ownerId, err := rp.idResolver.ResolveIdent(r.Context(), f.Did)
178
+
if err != nil {
179
+
log.Println("failed to resolve repo owner did", f.Did, err)
180
+
http.Error(w, "repository owner not found", http.StatusNotFound)
181
+
return
182
+
}
183
+
184
+
ownerPds := ownerId.PDSEndpoint()
178
185
url, _ := url.Parse(fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob", ownerPds))
179
186
q := url.Query()
180
187
q.Set("cid", artifact.BlobCid.String())
···
228
235
229
236
artifacts, err := db.GetArtifact(
230
237
rp.db,
231
-
db.FilterEq("repo_at", f.RepoAt()),
232
-
db.FilterEq("tag", tag[:]),
233
-
db.FilterEq("name", filename),
238
+
orm.FilterEq("repo_at", f.RepoAt()),
239
+
orm.FilterEq("tag", tag[:]),
240
+
orm.FilterEq("name", filename),
234
241
)
235
242
if err != nil {
236
243
log.Println("failed to get artifacts", err)
···
270
277
defer tx.Rollback()
271
278
272
279
err = db.DeleteArtifact(tx,
273
-
db.FilterEq("repo_at", f.RepoAt()),
274
-
db.FilterEq("tag", artifact.Tag[:]),
275
-
db.FilterEq("name", filename),
280
+
orm.FilterEq("repo_at", f.RepoAt()),
281
+
orm.FilterEq("tag", artifact.Tag[:]),
282
+
orm.FilterEq("name", filename),
276
283
)
277
284
if err != nil {
278
285
log.Println("failed to remove artifact record from db", err)
···
290
297
w.Write([]byte{})
291
298
}
292
299
293
-
func (rp *Repo) resolveTag(ctx context.Context, f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
300
+
func (rp *Repo) resolveTag(ctx context.Context, f *models.Repo, tagParam string) (*types.TagReference, error) {
294
301
tagParam, err := url.QueryUnescape(tagParam)
295
302
if err != nil {
296
303
return nil, err
···
305
312
Host: host,
306
313
}
307
314
308
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
315
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
309
316
xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
310
317
if err != nil {
311
318
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+11
-9
appview/repo/blob.go
+11
-9
appview/repo/blob.go
···
54
54
xrpcc := &indigoxrpc.Client{
55
55
Host: host,
56
56
}
57
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
57
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
58
58
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
59
59
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
60
60
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
···
62
62
return
63
63
}
64
64
65
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
66
+
65
67
// Use XRPC response directly instead of converting to internal types
66
68
var breadcrumbs [][]string
67
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
69
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))})
68
70
if filePath != "" {
69
71
for idx, elem := range strings.Split(filePath, "/") {
70
72
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
···
78
80
79
81
rp.pages.RepoBlob(w, pages.RepoBlobParams{
80
82
LoggedInUser: user,
81
-
RepoInfo: f.RepoInfo(user),
83
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
82
84
BreadCrumbs: breadcrumbs,
83
85
BlobView: blobView,
84
86
RepoBlob_Output: resp,
···
105
107
if !rp.config.Core.Dev {
106
108
scheme = "https"
107
109
}
108
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
110
+
repo := f.DidSlashRepo()
109
111
baseURL := &url.URL{
110
112
Scheme: scheme,
111
113
Host: f.Knot,
···
176
178
}
177
179
178
180
// NewBlobView creates a BlobView from the XRPC response
179
-
func NewBlobView(resp *tangled.RepoBlob_Output, config *config.Config, f *reporesolver.ResolvedRepo, ref, filePath string, queryParams url.Values) models.BlobView {
181
+
func NewBlobView(resp *tangled.RepoBlob_Output, config *config.Config, repo *models.Repo, ref, filePath string, queryParams url.Values) models.BlobView {
180
182
view := models.BlobView{
181
183
Contents: "",
182
184
Lines: 0,
···
198
200
199
201
// Determine if binary
200
202
if resp.IsBinary != nil && *resp.IsBinary {
201
-
view.ContentSrc = generateBlobURL(config, f, ref, filePath)
203
+
view.ContentSrc = generateBlobURL(config, repo, ref, filePath)
202
204
ext := strings.ToLower(filepath.Ext(resp.Path))
203
205
204
206
switch ext {
···
250
252
return view
251
253
}
252
254
253
-
func generateBlobURL(config *config.Config, f *reporesolver.ResolvedRepo, ref, filePath string) string {
255
+
func generateBlobURL(config *config.Config, repo *models.Repo, ref, filePath string) string {
254
256
scheme := "http"
255
257
if !config.Core.Dev {
256
258
scheme = "https"
257
259
}
258
260
259
-
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
261
+
repoName := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
260
262
baseURL := &url.URL{
261
263
Scheme: scheme,
262
-
Host: f.Knot,
264
+
Host: repo.Knot,
263
265
Path: "/xrpc/sh.tangled.repo.blob",
264
266
}
265
267
query := baseURL.Query()
+2
-2
appview/repo/branches.go
+2
-2
appview/repo/branches.go
···
29
29
xrpcc := &indigoxrpc.Client{
30
30
Host: host,
31
31
}
32
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
32
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
33
33
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
34
34
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
35
35
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
46
46
user := rp.oauth.GetUser(r)
47
47
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
48
48
LoggedInUser: user,
49
-
RepoInfo: f.RepoInfo(user),
49
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
50
50
RepoBranchesResponse: result,
51
51
})
52
52
}
+4
-8
appview/repo/compare.go
+4
-8
appview/repo/compare.go
···
36
36
Host: host,
37
37
}
38
38
39
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
39
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
40
40
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
41
41
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
42
42
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
88
88
return
89
89
}
90
90
91
-
repoinfo := f.RepoInfo(user)
92
-
93
91
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
94
92
LoggedInUser: user,
95
-
RepoInfo: repoinfo,
93
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
96
94
Branches: branches,
97
95
Tags: tags.Tags,
98
96
Base: base,
···
151
149
Host: host,
152
150
}
153
151
154
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
152
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
155
153
156
154
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
157
155
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
···
202
200
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
203
201
}
204
202
205
-
repoinfo := f.RepoInfo(user)
206
-
207
203
rp.pages.RepoCompare(w, pages.RepoCompareParams{
208
204
LoggedInUser: user,
209
-
RepoInfo: repoinfo,
205
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
210
206
Branches: branches.Branches,
211
207
Tags: tags.Tags,
212
208
Base: base,
+24
-17
appview/repo/feed.go
+24
-17
appview/repo/feed.go
···
11
11
"tangled.org/core/appview/db"
12
12
"tangled.org/core/appview/models"
13
13
"tangled.org/core/appview/pagination"
14
-
"tangled.org/core/appview/reporesolver"
14
+
"tangled.org/core/orm"
15
15
16
+
"github.com/bluesky-social/indigo/atproto/identity"
16
17
"github.com/bluesky-social/indigo/atproto/syntax"
17
18
"github.com/gorilla/feeds"
18
19
)
19
20
20
-
func (rp *Repo) getRepoFeed(ctx context.Context, f *reporesolver.ResolvedRepo) (*feeds.Feed, error) {
21
+
func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) {
21
22
const feedLimitPerType = 100
22
23
23
-
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt()))
24
+
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt()))
24
25
if err != nil {
25
26
return nil, err
26
27
}
···
28
29
issues, err := db.GetIssuesPaginated(
29
30
rp.db,
30
31
pagination.Page{Limit: feedLimitPerType},
31
-
db.FilterEq("repo_at", f.RepoAt()),
32
+
orm.FilterEq("repo_at", repo.RepoAt()),
32
33
)
33
34
if err != nil {
34
35
return nil, err
35
36
}
36
37
37
38
feed := &feeds.Feed{
38
-
Title: fmt.Sprintf("activity feed for %s", f.OwnerSlashRepo()),
39
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, f.OwnerSlashRepo()), Type: "text/html", Rel: "alternate"},
39
+
Title: fmt.Sprintf("activity feed for @%s", ownerSlashRepo),
40
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s", rp.config.Core.AppviewHost, ownerSlashRepo), Type: "text/html", Rel: "alternate"},
40
41
Items: make([]*feeds.Item, 0),
41
42
Updated: time.UnixMilli(0),
42
43
}
43
44
44
45
for _, pull := range pulls {
45
-
items, err := rp.createPullItems(ctx, pull, f)
46
+
items, err := rp.createPullItems(ctx, pull, repo, ownerSlashRepo)
46
47
if err != nil {
47
48
return nil, err
48
49
}
···
50
51
}
51
52
52
53
for _, issue := range issues {
53
-
item, err := rp.createIssueItem(ctx, issue, f)
54
+
item, err := rp.createIssueItem(ctx, issue, repo, ownerSlashRepo)
54
55
if err != nil {
55
56
return nil, err
56
57
}
···
71
72
return feed, nil
72
73
}
73
74
74
-
func (rp *Repo) createPullItems(ctx context.Context, pull *models.Pull, f *reporesolver.ResolvedRepo) ([]*feeds.Item, error) {
75
+
func (rp *Repo) createPullItems(ctx context.Context, pull *models.Pull, repo *models.Repo, ownerSlashRepo string) ([]*feeds.Item, error) {
75
76
owner, err := rp.idResolver.ResolveIdent(ctx, pull.OwnerDid)
76
77
if err != nil {
77
78
return nil, err
···
80
81
var items []*feeds.Item
81
82
82
83
state := rp.getPullState(pull)
83
-
description := rp.buildPullDescription(owner.Handle, state, pull, f.OwnerSlashRepo())
84
+
description := rp.buildPullDescription(owner.Handle, state, pull, ownerSlashRepo)
84
85
85
86
mainItem := &feeds.Item{
86
87
Title: fmt.Sprintf("[PR #%d] %s", pull.PullId, pull.Title),
87
88
Description: description,
88
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId)},
89
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId)},
89
90
Created: pull.Created,
90
91
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
91
92
}
···
98
99
99
100
roundItem := &feeds.Item{
100
101
Title: fmt.Sprintf("[PR #%d] %s (round #%d)", pull.PullId, pull.Title, round.RoundNumber),
101
-
Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in %s", owner.Handle, round.RoundNumber, pull.PullId, f.OwnerSlashRepo()),
102
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), pull.PullId, round.RoundNumber)},
102
+
Description: fmt.Sprintf("@%s submitted changes (at round #%d) on PR #%d in @%s", owner.Handle, round.RoundNumber, pull.PullId, ownerSlashRepo),
103
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/pulls/%d/round/%d/", rp.config.Core.AppviewHost, ownerSlashRepo, pull.PullId, round.RoundNumber)},
103
104
Created: round.Created,
104
105
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
105
106
}
···
109
110
return items, nil
110
111
}
111
112
112
-
func (rp *Repo) createIssueItem(ctx context.Context, issue models.Issue, f *reporesolver.ResolvedRepo) (*feeds.Item, error) {
113
+
func (rp *Repo) createIssueItem(ctx context.Context, issue models.Issue, repo *models.Repo, ownerSlashRepo string) (*feeds.Item, error) {
113
114
owner, err := rp.idResolver.ResolveIdent(ctx, issue.Did)
114
115
if err != nil {
115
116
return nil, err
···
122
123
123
124
return &feeds.Item{
124
125
Title: fmt.Sprintf("[Issue #%d] %s", issue.IssueId, issue.Title),
125
-
Description: fmt.Sprintf("@%s %s issue #%d in %s", owner.Handle, state, issue.IssueId, f.OwnerSlashRepo()),
126
-
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, f.OwnerSlashRepo(), issue.IssueId)},
126
+
Description: fmt.Sprintf("@%s %s issue #%d in @%s", owner.Handle, state, issue.IssueId, ownerSlashRepo),
127
+
Link: &feeds.Link{Href: fmt.Sprintf("%s/%s/issues/%d", rp.config.Core.AppviewHost, ownerSlashRepo, issue.IssueId)},
127
128
Created: issue.Created,
128
129
Author: &feeds.Author{Name: fmt.Sprintf("@%s", owner.Handle)},
129
130
}, nil
···
152
153
log.Println("failed to fully resolve repo:", err)
153
154
return
154
155
}
156
+
repoOwnerId, ok := r.Context().Value("resolvedId").(identity.Identity)
157
+
if !ok || repoOwnerId.Handle.IsInvalidHandle() {
158
+
log.Println("failed to get resolved repo owner id")
159
+
return
160
+
}
161
+
ownerSlashRepo := repoOwnerId.Handle.String() + "/" + f.Name
155
162
156
-
feed, err := rp.getRepoFeed(r.Context(), f)
163
+
feed, err := rp.getRepoFeed(r.Context(), f, ownerSlashRepo)
157
164
if err != nil {
158
165
log.Println("failed to get repo feed:", err)
159
166
rp.pages.Error500(w)
+18
-19
appview/repo/index.go
+18
-19
appview/repo/index.go
···
22
22
"tangled.org/core/appview/db"
23
23
"tangled.org/core/appview/models"
24
24
"tangled.org/core/appview/pages"
25
-
"tangled.org/core/appview/reporesolver"
26
25
"tangled.org/core/appview/xrpcclient"
26
+
"tangled.org/core/orm"
27
27
"tangled.org/core/types"
28
28
29
29
"github.com/go-chi/chi/v5"
···
52
52
}
53
53
54
54
user := rp.oauth.GetUser(r)
55
-
repoInfo := f.RepoInfo(user)
56
55
57
56
// Build index response from multiple XRPC calls
58
57
result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
···
62
61
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
63
62
LoggedInUser: user,
64
63
NeedsKnotUpgrade: true,
65
-
RepoInfo: repoInfo,
64
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
66
65
})
67
66
return
68
67
}
···
124
123
l.Error("failed to get email to did map", "err", err)
125
124
}
126
125
127
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, commitsTrunc)
126
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, commitsTrunc)
128
127
if err != nil {
129
128
l.Error("failed to GetVerifiedObjectCommits", "err", err)
130
129
}
···
140
139
for _, c := range commitsTrunc {
141
140
shas = append(shas, c.Hash.String())
142
141
}
143
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
142
+
pipelines, err := getPipelineStatuses(rp.db, f, shas)
144
143
if err != nil {
145
144
l.Error("failed to fetch pipeline statuses", "err", err)
146
145
// non-fatal
···
148
147
149
148
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
150
149
LoggedInUser: user,
151
-
RepoInfo: repoInfo,
150
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
152
151
TagMap: tagMap,
153
152
RepoIndexResponse: *result,
154
153
CommitsTrunc: commitsTrunc,
···
165
164
func (rp *Repo) getLanguageInfo(
166
165
ctx context.Context,
167
166
l *slog.Logger,
168
-
f *reporesolver.ResolvedRepo,
167
+
repo *models.Repo,
169
168
xrpcc *indigoxrpc.Client,
170
169
currentRef string,
171
170
isDefaultRef bool,
···
173
172
// first attempt to fetch from db
174
173
langs, err := db.GetRepoLanguages(
175
174
rp.db,
176
-
db.FilterEq("repo_at", f.RepoAt()),
177
-
db.FilterEq("ref", currentRef),
175
+
orm.FilterEq("repo_at", repo.RepoAt()),
176
+
orm.FilterEq("ref", currentRef),
178
177
)
179
178
180
179
if err != nil || langs == nil {
181
180
// non-fatal, fetch langs from ks via XRPC
182
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
183
-
ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo)
181
+
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
182
+
ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, didSlashRepo)
184
183
if err != nil {
185
184
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
186
185
l.Error("failed to call XRPC repo.languages", "err", xrpcerr)
···
195
194
196
195
for _, lang := range ls.Languages {
197
196
langs = append(langs, models.RepoLanguage{
198
-
RepoAt: f.RepoAt(),
197
+
RepoAt: repo.RepoAt(),
199
198
Ref: currentRef,
200
199
IsDefaultRef: isDefaultRef,
201
200
Language: lang.Name,
···
210
209
defer tx.Rollback()
211
210
212
211
// update appview's cache
213
-
err = db.UpdateRepoLanguages(tx, f.RepoAt(), currentRef, langs)
212
+
err = db.UpdateRepoLanguages(tx, repo.RepoAt(), currentRef, langs)
214
213
if err != nil {
215
214
// non-fatal
216
215
l.Error("failed to cache lang results", "err", err)
···
255
254
}
256
255
257
256
// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel
258
-
func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, f *reporesolver.ResolvedRepo, ref string) (*types.RepoIndexResponse, error) {
259
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
257
+
func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, repo *models.Repo, ref string) (*types.RepoIndexResponse, error) {
258
+
didSlashRepo := fmt.Sprintf("%s/%s", repo.Did, repo.Name)
260
259
261
260
// first get branches to determine the ref if not specified
262
-
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
261
+
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, didSlashRepo)
263
262
if err != nil {
264
263
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
265
264
}
···
303
302
wg.Add(1)
304
303
go func() {
305
304
defer wg.Done()
306
-
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
305
+
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, didSlashRepo)
307
306
if err != nil {
308
307
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
309
308
return
···
318
317
wg.Add(1)
319
318
go func() {
320
319
defer wg.Done()
321
-
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
320
+
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, didSlashRepo)
322
321
if err != nil {
323
322
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
324
323
return
···
330
329
wg.Add(1)
331
330
go func() {
332
331
defer wg.Done()
333
-
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
332
+
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, didSlashRepo)
334
333
if err != nil {
335
334
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
336
335
return
+8
-11
appview/repo/log.go
+8
-11
appview/repo/log.go
···
57
57
cursor = strconv.Itoa(offset)
58
58
}
59
59
60
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
60
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
61
61
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
62
62
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
63
63
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
···
116
116
l.Error("failed to fetch email to did mapping", "err", err)
117
117
}
118
118
119
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
119
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, xrpcResp.Commits)
120
120
if err != nil {
121
121
l.Error("failed to GetVerifiedObjectCommits", "err", err)
122
122
}
123
-
124
-
repoInfo := f.RepoInfo(user)
125
123
126
124
var shas []string
127
125
for _, c := range xrpcResp.Commits {
128
126
shas = append(shas, c.Hash.String())
129
127
}
130
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
128
+
pipelines, err := getPipelineStatuses(rp.db, f, shas)
131
129
if err != nil {
132
130
l.Error("failed to getPipelineStatuses", "err", err)
133
131
// non-fatal
···
136
134
rp.pages.RepoLog(w, pages.RepoLogParams{
137
135
LoggedInUser: user,
138
136
TagMap: tagMap,
139
-
RepoInfo: repoInfo,
137
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
140
138
RepoLogResponse: xrpcResp,
141
139
EmailToDid: emailToDidMap,
142
140
VerifiedCommits: vc,
···
174
172
Host: host,
175
173
}
176
174
177
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
175
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
178
176
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
179
177
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
180
178
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
···
194
192
l.Error("failed to get email to did mapping", "err", err)
195
193
}
196
194
197
-
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
195
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.Commit{result.Diff.Commit})
198
196
if err != nil {
199
197
l.Error("failed to GetVerifiedCommits", "err", err)
200
198
}
201
199
202
200
user := rp.oauth.GetUser(r)
203
-
repoInfo := f.RepoInfo(user)
204
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This})
201
+
pipelines, err := getPipelineStatuses(rp.db, f, []string{result.Diff.Commit.This})
205
202
if err != nil {
206
203
l.Error("failed to getPipelineStatuses", "err", err)
207
204
// non-fatal
···
213
210
214
211
rp.pages.RepoCommit(w, pages.RepoCommitParams{
215
212
LoggedInUser: user,
216
-
RepoInfo: f.RepoInfo(user),
213
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
217
214
RepoCommitResponse: result,
218
215
EmailToDid: emailToDidMap,
219
216
VerifiedCommit: vc,
+4
-3
appview/repo/opengraph.go
+4
-3
appview/repo/opengraph.go
···
16
16
"tangled.org/core/appview/db"
17
17
"tangled.org/core/appview/models"
18
18
"tangled.org/core/appview/ogcard"
19
+
"tangled.org/core/orm"
19
20
"tangled.org/core/types"
20
21
)
21
22
···
338
339
var languageStats []types.RepoLanguageDetails
339
340
langs, err := db.GetRepoLanguages(
340
341
rp.db,
341
-
db.FilterEq("repo_at", f.RepoAt()),
342
-
db.FilterEq("is_default_ref", 1),
342
+
orm.FilterEq("repo_at", f.RepoAt()),
343
+
orm.FilterEq("is_default_ref", 1),
343
344
)
344
345
if err != nil {
345
346
log.Printf("failed to get language stats from db: %v", err)
···
374
375
})
375
376
}
376
377
377
-
card, err := rp.drawRepoSummaryCard(&f.Repo, languageStats)
378
+
card, err := rp.drawRepoSummaryCard(f, languageStats)
378
379
if err != nil {
379
380
log.Println("failed to draw repo summary card", err)
380
381
http.Error(w, "failed to draw repo summary card", http.StatusInternalServerError)
+37
-35
appview/repo/repo.go
+37
-35
appview/repo/repo.go
···
24
24
xrpcclient "tangled.org/core/appview/xrpcclient"
25
25
"tangled.org/core/eventconsumer"
26
26
"tangled.org/core/idresolver"
27
+
"tangled.org/core/orm"
27
28
"tangled.org/core/rbac"
28
29
"tangled.org/core/tid"
29
30
"tangled.org/core/xrpc/serviceauth"
···
118
119
}
119
120
}
120
121
121
-
newRepo := f.Repo
122
+
newRepo := *f
122
123
newRepo.Spindle = newSpindle
123
124
record := newRepo.AsRecord()
124
125
···
257
258
l.Info("wrote label record to PDS")
258
259
259
260
// update the repo to subscribe to this label
260
-
newRepo := f.Repo
261
+
newRepo := *f
261
262
newRepo.Labels = append(newRepo.Labels, aturi)
262
263
repoRecord := newRepo.AsRecord()
263
264
···
345
346
// get form values
346
347
labelId := r.FormValue("label-id")
347
348
348
-
label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId))
349
+
label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId))
349
350
if err != nil {
350
351
fail("Failed to find label definition.", err)
351
352
return
···
369
370
}
370
371
371
372
// update repo record to remove the label reference
372
-
newRepo := f.Repo
373
+
newRepo := *f
373
374
var updated []string
374
375
removedAt := label.AtUri().String()
375
376
for _, l := range newRepo.Labels {
···
409
410
410
411
err = db.UnsubscribeLabel(
411
412
tx,
412
-
db.FilterEq("repo_at", f.RepoAt()),
413
-
db.FilterEq("label_at", removedAt),
413
+
orm.FilterEq("repo_at", f.RepoAt()),
414
+
orm.FilterEq("label_at", removedAt),
414
415
)
415
416
if err != nil {
416
417
fail("Failed to unsubscribe label.", err)
417
418
return
418
419
}
419
420
420
-
err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id))
421
+
err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id))
421
422
if err != nil {
422
423
fail("Failed to delete label definition.", err)
423
424
return
···
456
457
}
457
458
458
459
labelAts := r.Form["label"]
459
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
460
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
460
461
if err != nil {
461
462
fail("Failed to subscribe to label.", err)
462
463
return
463
464
}
464
465
465
-
newRepo := f.Repo
466
+
newRepo := *f
466
467
newRepo.Labels = append(newRepo.Labels, labelAts...)
467
468
468
469
// dedup
···
477
478
return
478
479
}
479
480
480
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey)
481
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey)
481
482
if err != nil {
482
483
fail("Failed to update labels, no record found on PDS.", err)
483
484
return
···
542
543
}
543
544
544
545
labelAts := r.Form["label"]
545
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
546
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
546
547
if err != nil {
547
548
fail("Failed to unsubscribe to label.", err)
548
549
return
549
550
}
550
551
551
552
// update repo record to remove the label reference
552
-
newRepo := f.Repo
553
+
newRepo := *f
553
554
var updated []string
554
555
for _, l := range newRepo.Labels {
555
556
if !slices.Contains(labelAts, l) {
···
565
566
return
566
567
}
567
568
568
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Repo.Did, f.Repo.Rkey)
569
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, f.Did, f.Rkey)
569
570
if err != nil {
570
571
fail("Failed to update labels, no record found on PDS.", err)
571
572
return
···
582
583
583
584
err = db.UnsubscribeLabel(
584
585
rp.db,
585
-
db.FilterEq("repo_at", f.RepoAt()),
586
-
db.FilterIn("label_at", labelAts),
586
+
orm.FilterEq("repo_at", f.RepoAt()),
587
+
orm.FilterIn("label_at", labelAts),
587
588
)
588
589
if err != nil {
589
590
fail("Failed to unsubscribe label.", err)
···
612
613
613
614
labelDefs, err := db.GetLabelDefinitions(
614
615
rp.db,
615
-
db.FilterIn("at_uri", f.Repo.Labels),
616
-
db.FilterContains("scope", subject.Collection().String()),
616
+
orm.FilterIn("at_uri", f.Labels),
617
+
orm.FilterContains("scope", subject.Collection().String()),
617
618
)
618
619
if err != nil {
619
620
l.Error("failed to fetch label defs", "err", err)
···
625
626
defs[l.AtUri().String()] = &l
626
627
}
627
628
628
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
629
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
629
630
if err != nil {
630
631
l.Error("failed to build label state", "err", err)
631
632
return
···
635
636
user := rp.oauth.GetUser(r)
636
637
rp.pages.LabelPanel(w, pages.LabelPanelParams{
637
638
LoggedInUser: user,
638
-
RepoInfo: f.RepoInfo(user),
639
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
639
640
Defs: defs,
640
641
Subject: subject.String(),
641
642
State: state,
···
660
661
661
662
labelDefs, err := db.GetLabelDefinitions(
662
663
rp.db,
663
-
db.FilterIn("at_uri", f.Repo.Labels),
664
-
db.FilterContains("scope", subject.Collection().String()),
664
+
orm.FilterIn("at_uri", f.Labels),
665
+
orm.FilterContains("scope", subject.Collection().String()),
665
666
)
666
667
if err != nil {
667
668
l.Error("failed to fetch labels", "err", err)
···
673
674
defs[l.AtUri().String()] = &l
674
675
}
675
676
676
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
677
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
677
678
if err != nil {
678
679
l.Error("failed to build label state", "err", err)
679
680
return
···
683
684
user := rp.oauth.GetUser(r)
684
685
rp.pages.EditLabelPanel(w, pages.EditLabelPanelParams{
685
686
LoggedInUser: user,
686
-
RepoInfo: f.RepoInfo(user),
687
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
687
688
Defs: defs,
688
689
Subject: subject.String(),
689
690
State: state,
···
864
865
r.Context(),
865
866
client,
866
867
&tangled.RepoDelete_Input{
867
-
Did: f.OwnerDid(),
868
+
Did: f.Did,
868
869
Name: f.Name,
869
870
Rkey: f.Rkey,
870
871
},
···
902
903
l.Info("removed collaborators")
903
904
904
905
// remove repo RBAC
905
-
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
906
+
err = rp.enforcer.RemoveRepo(f.Did, f.Knot, f.DidSlashRepo())
906
907
if err != nil {
907
908
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
908
909
return
909
910
}
910
911
911
912
// remove repo from db
912
-
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
913
+
err = db.RemoveRepo(tx, f.Did, f.Name)
913
914
if err != nil {
914
915
rp.pages.Notice(w, noticeId, "Failed to update appview")
915
916
return
···
930
931
return
931
932
}
932
933
933
-
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
934
+
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.Did))
934
935
}
935
936
936
937
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
959
960
return
960
961
}
961
962
962
-
repoInfo := f.RepoInfo(user)
963
-
if repoInfo.Source == nil {
963
+
if f.Source == "" {
964
964
rp.pages.Notice(w, "repo", "This repository is not a fork.")
965
965
return
966
966
}
···
971
971
&tangled.RepoForkSync_Input{
972
972
Did: user.Did,
973
973
Name: f.Name,
974
-
Source: repoInfo.Source.RepoAt().String(),
974
+
Source: f.Source,
975
975
Branch: ref,
976
976
},
977
977
)
···
1007
1007
rp.pages.ForkRepo(w, pages.ForkRepoParams{
1008
1008
LoggedInUser: user,
1009
1009
Knots: knots,
1010
-
RepoInfo: f.RepoInfo(user),
1010
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
1011
1011
})
1012
1012
1013
1013
case http.MethodPost:
···
1037
1037
// in the user's account.
1038
1038
existingRepo, err := db.GetRepo(
1039
1039
rp.db,
1040
-
db.FilterEq("did", user.Did),
1041
-
db.FilterEq("name", forkName),
1040
+
orm.FilterEq("did", user.Did),
1041
+
orm.FilterEq("name", forkName),
1042
1042
)
1043
1043
if err != nil {
1044
1044
if !errors.Is(err, sql.ErrNoRows) {
···
1058
1058
uri = "http"
1059
1059
}
1060
1060
1061
-
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.Repo.Name)
1061
+
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.Did, f.Name)
1062
1062
l = l.With("cloneUrl", forkSourceUrl)
1063
1063
1064
1064
sourceAt := f.RepoAt().String()
···
1071
1071
Knot: targetKnot,
1072
1072
Rkey: rkey,
1073
1073
Source: sourceAt,
1074
-
Description: f.Repo.Description,
1074
+
Description: f.Description,
1075
1075
Created: time.Now(),
1076
1076
Labels: rp.config.Label.DefaultLabelDefs,
1077
1077
}
···
1130
1130
}
1131
1131
defer rollback()
1132
1132
1133
+
// TODO: this could coordinate better with the knot to recieve a clone status
1133
1134
client, err := rp.oauth.ServiceClient(
1134
1135
r,
1135
1136
oauth.WithService(targetKnot),
1136
1137
oauth.WithLxm(tangled.RepoCreateNSID),
1137
1138
oauth.WithDev(rp.config.Core.Dev),
1139
+
oauth.WithTimeout(time.Second*20), // big repos take time to clone
1138
1140
)
1139
1141
if err != nil {
1140
1142
l.Error("could not create service client", "err", err)
+17
-19
appview/repo/repo_util.go
+17
-19
appview/repo/repo_util.go
···
1
1
package repo
2
2
3
3
import (
4
+
"maps"
4
5
"slices"
5
6
"sort"
6
7
"strings"
7
8
8
9
"tangled.org/core/appview/db"
9
10
"tangled.org/core/appview/models"
10
-
"tangled.org/core/appview/pages/repoinfo"
11
+
"tangled.org/core/orm"
11
12
"tangled.org/core/types"
12
-
13
-
"github.com/go-git/go-git/v5/plumbing/object"
14
13
)
15
14
16
15
func sortFiles(files []types.NiceTree) {
···
43
42
})
44
43
}
45
44
46
-
func uniqueEmails(commits []*object.Commit) []string {
45
+
func uniqueEmails(commits []types.Commit) []string {
47
46
emails := make(map[string]struct{})
48
47
for _, commit := range commits {
49
-
if commit.Author.Email != "" {
50
-
emails[commit.Author.Email] = struct{}{}
51
-
}
52
-
if commit.Committer.Email != "" {
53
-
emails[commit.Committer.Email] = struct{}{}
48
+
emails[commit.Author.Email] = struct{}{}
49
+
emails[commit.Committer.Email] = struct{}{}
50
+
for _, c := range commit.CoAuthors() {
51
+
emails[c.Email] = struct{}{}
54
52
}
55
53
}
56
-
var uniqueEmails []string
57
-
for email := range emails {
58
-
uniqueEmails = append(uniqueEmails, email)
59
-
}
60
-
return uniqueEmails
54
+
55
+
// delete empty emails if any, from the set
56
+
delete(emails, "")
57
+
58
+
return slices.Collect(maps.Keys(emails))
61
59
}
62
60
63
61
func balanceIndexItems(commitCount, branchCount, tagCount, fileCount int) (commitsTrunc int, branchesTrunc int, tagsTrunc int) {
···
93
91
// golang is so blessed that it requires 35 lines of imperative code for this
94
92
func getPipelineStatuses(
95
93
d *db.DB,
96
-
repoInfo repoinfo.RepoInfo,
94
+
repo *models.Repo,
97
95
shas []string,
98
96
) (map[string]models.Pipeline, error) {
99
97
m := make(map[string]models.Pipeline)
···
105
103
ps, err := db.GetPipelineStatuses(
106
104
d,
107
105
len(shas),
108
-
db.FilterEq("repo_owner", repoInfo.OwnerDid),
109
-
db.FilterEq("repo_name", repoInfo.Name),
110
-
db.FilterEq("knot", repoInfo.Knot),
111
-
db.FilterIn("sha", shas),
106
+
orm.FilterEq("repo_owner", repo.Did),
107
+
orm.FilterEq("repo_name", repo.Name),
108
+
orm.FilterEq("knot", repo.Knot),
109
+
orm.FilterIn("sha", shas),
112
110
)
113
111
if err != nil {
114
112
return nil, err
+40
-11
appview/repo/settings.go
+40
-11
appview/repo/settings.go
···
10
10
11
11
"tangled.org/core/api/tangled"
12
12
"tangled.org/core/appview/db"
13
+
"tangled.org/core/appview/models"
13
14
"tangled.org/core/appview/oauth"
14
15
"tangled.org/core/appview/pages"
15
16
xrpcclient "tangled.org/core/appview/xrpcclient"
17
+
"tangled.org/core/orm"
16
18
"tangled.org/core/types"
17
19
18
20
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
194
196
Host: host,
195
197
}
196
198
197
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
199
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
198
200
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
199
201
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
200
202
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
···
209
211
return
210
212
}
211
213
212
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
214
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
213
215
if err != nil {
214
216
l.Error("failed to fetch labels", "err", err)
215
217
rp.pages.Error503(w)
216
218
return
217
219
}
218
220
219
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
221
+
labels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", f.Labels))
220
222
if err != nil {
221
223
l.Error("failed to fetch labels", "err", err)
222
224
rp.pages.Error503(w)
···
237
239
labels = labels[:n]
238
240
239
241
subscribedLabels := make(map[string]struct{})
240
-
for _, l := range f.Repo.Labels {
242
+
for _, l := range f.Labels {
241
243
subscribedLabels[l] = struct{}{}
242
244
}
243
245
···
254
256
255
257
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
256
258
LoggedInUser: user,
257
-
RepoInfo: f.RepoInfo(user),
259
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
258
260
Branches: result.Branches,
259
261
Labels: labels,
260
262
DefaultLabels: defaultLabels,
···
271
273
f, err := rp.repoResolver.Resolve(r)
272
274
user := rp.oauth.GetUser(r)
273
275
274
-
repoCollaborators, err := f.Collaborators(r.Context())
276
+
collaborators, err := func(repo *models.Repo) ([]pages.Collaborator, error) {
277
+
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(repo.DidSlashRepo(), repo.Knot)
278
+
if err != nil {
279
+
return nil, err
280
+
}
281
+
var collaborators []pages.Collaborator
282
+
for _, item := range repoCollaborators {
283
+
// currently only two roles: owner and member
284
+
var role string
285
+
switch item[3] {
286
+
case "repo:owner":
287
+
role = "owner"
288
+
case "repo:collaborator":
289
+
role = "collaborator"
290
+
default:
291
+
continue
292
+
}
293
+
294
+
did := item[0]
295
+
296
+
c := pages.Collaborator{
297
+
Did: did,
298
+
Role: role,
299
+
}
300
+
collaborators = append(collaborators, c)
301
+
}
302
+
return collaborators, nil
303
+
}(f)
275
304
if err != nil {
276
305
l.Error("failed to get collaborators", "err", err)
277
306
}
278
307
279
308
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
280
309
LoggedInUser: user,
281
-
RepoInfo: f.RepoInfo(user),
310
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
282
311
Tabs: settingsTabs,
283
312
Tab: "access",
284
-
Collaborators: repoCollaborators,
313
+
Collaborators: collaborators,
285
314
})
286
315
}
287
316
···
292
321
user := rp.oauth.GetUser(r)
293
322
294
323
// all spindles that the repo owner is a member of
295
-
spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid())
324
+
spindles, err := rp.enforcer.GetSpindlesForUser(f.Did)
296
325
if err != nil {
297
326
l.Error("failed to fetch spindles", "err", err)
298
327
return
···
339
368
340
369
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
341
370
LoggedInUser: user,
342
-
RepoInfo: f.RepoInfo(user),
371
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
343
372
Tabs: settingsTabs,
344
373
Tab: "pipelines",
345
374
Spindles: spindles,
···
388
417
}
389
418
l.Debug("got", "topicsStr", topicStr, "topics", topics)
390
419
391
-
newRepo := f.Repo
420
+
newRepo := *f
392
421
newRepo.Description = description
393
422
newRepo.Website = website
394
423
newRepo.Topics = topics
+6
-4
appview/repo/tree.go
+6
-4
appview/repo/tree.go
···
9
9
10
10
"tangled.org/core/api/tangled"
11
11
"tangled.org/core/appview/pages"
12
+
"tangled.org/core/appview/reporesolver"
12
13
xrpcclient "tangled.org/core/appview/xrpcclient"
13
14
"tangled.org/core/types"
14
15
···
39
40
xrpcc := &indigoxrpc.Client{
40
41
Host: host,
41
42
}
42
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
43
+
repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
43
44
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
44
45
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
45
46
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
···
79
80
result.ReadmeFileName = xrpcResp.Readme.Filename
80
81
result.Readme = xrpcResp.Readme.Contents
81
82
}
83
+
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
82
84
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
83
85
// so we can safely redirect to the "parent" (which is the same file).
84
86
if len(result.Files) == 0 && result.Parent == treePath {
85
-
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
87
+
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", ownerSlashRepo, url.PathEscape(ref), result.Parent)
86
88
http.Redirect(w, r, redirectTo, http.StatusFound)
87
89
return
88
90
}
89
91
user := rp.oauth.GetUser(r)
90
92
var breadcrumbs [][]string
91
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
93
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))})
92
94
if treePath != "" {
93
95
for idx, elem := range strings.Split(treePath, "/") {
94
96
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
···
100
102
LoggedInUser: user,
101
103
BreadCrumbs: breadcrumbs,
102
104
TreePath: treePath,
103
-
RepoInfo: f.RepoInfo(user),
105
+
RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
104
106
RepoTreeResponse: result,
105
107
})
106
108
}
+76
-164
appview/reporesolver/resolver.go
+76
-164
appview/reporesolver/resolver.go
···
1
1
package reporesolver
2
2
3
3
import (
4
-
"context"
5
-
"database/sql"
6
-
"errors"
7
4
"fmt"
8
5
"log"
9
6
"net/http"
···
12
9
"strings"
13
10
14
11
"github.com/bluesky-social/indigo/atproto/identity"
15
-
securejoin "github.com/cyphar/filepath-securejoin"
16
12
"github.com/go-chi/chi/v5"
17
13
"tangled.org/core/appview/config"
18
14
"tangled.org/core/appview/db"
19
15
"tangled.org/core/appview/models"
20
16
"tangled.org/core/appview/oauth"
21
-
"tangled.org/core/appview/pages"
22
17
"tangled.org/core/appview/pages/repoinfo"
23
-
"tangled.org/core/idresolver"
24
18
"tangled.org/core/rbac"
25
19
)
26
20
27
-
type ResolvedRepo struct {
28
-
models.Repo
29
-
OwnerId identity.Identity
30
-
CurrentDir string
31
-
Ref string
32
-
33
-
rr *RepoResolver
21
+
type RepoResolver struct {
22
+
config *config.Config
23
+
enforcer *rbac.Enforcer
24
+
execer db.Execer
34
25
}
35
26
36
-
type RepoResolver struct {
37
-
config *config.Config
38
-
enforcer *rbac.Enforcer
39
-
idResolver *idresolver.Resolver
40
-
execer db.Execer
27
+
func New(config *config.Config, enforcer *rbac.Enforcer, execer db.Execer) *RepoResolver {
28
+
return &RepoResolver{config: config, enforcer: enforcer, execer: execer}
41
29
}
42
30
43
-
func New(config *config.Config, enforcer *rbac.Enforcer, resolver *idresolver.Resolver, execer db.Execer) *RepoResolver {
44
-
return &RepoResolver{config: config, enforcer: enforcer, idResolver: resolver, execer: execer}
31
+
// NOTE: this... should not even be here. the entire package will be removed in future refactor
32
+
func GetBaseRepoPath(r *http.Request, repo *models.Repo) string {
33
+
var (
34
+
user = chi.URLParam(r, "user")
35
+
name = chi.URLParam(r, "repo")
36
+
)
37
+
if user == "" || name == "" {
38
+
return repo.DidSlashRepo()
39
+
}
40
+
return path.Join(user, name)
45
41
}
46
42
47
-
func (rr *RepoResolver) Resolve(r *http.Request) (*ResolvedRepo, error) {
43
+
// TODO: move this out of `RepoResolver` struct
44
+
func (rr *RepoResolver) Resolve(r *http.Request) (*models.Repo, error) {
48
45
repo, ok := r.Context().Value("repo").(*models.Repo)
49
46
if !ok {
50
47
log.Println("malformed middleware: `repo` not exist in context")
51
48
return nil, fmt.Errorf("malformed middleware")
52
49
}
53
-
id, ok := r.Context().Value("resolvedId").(identity.Identity)
54
-
if !ok {
55
-
log.Println("malformed middleware")
56
-
return nil, fmt.Errorf("malformed middleware")
57
-
}
58
50
59
-
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
60
-
ref := chi.URLParam(r, "ref")
61
-
62
-
return &ResolvedRepo{
63
-
Repo: *repo,
64
-
OwnerId: id,
65
-
CurrentDir: currentDir,
66
-
Ref: ref,
67
-
68
-
rr: rr,
69
-
}, nil
70
-
}
71
-
72
-
func (f *ResolvedRepo) OwnerDid() string {
73
-
return f.OwnerId.DID.String()
74
-
}
75
-
76
-
func (f *ResolvedRepo) OwnerHandle() string {
77
-
return f.OwnerId.Handle.String()
51
+
return repo, nil
78
52
}
79
53
80
-
func (f *ResolvedRepo) OwnerSlashRepo() string {
81
-
handle := f.OwnerId.Handle
82
-
83
-
var p string
84
-
if handle != "" && !handle.IsInvalidHandle() {
85
-
p, _ = securejoin.SecureJoin(fmt.Sprintf("@%s", handle), f.Name)
86
-
} else {
87
-
p, _ = securejoin.SecureJoin(f.OwnerDid(), f.Name)
54
+
// 1. [x] replace `RepoInfo` to `reporesolver.GetRepoInfo(r *http.Request, repo, user)`
55
+
// 2. [x] remove `rr`, `CurrentDir`, `Ref` fields from `ResolvedRepo`
56
+
// 3. [x] remove `ResolvedRepo`
57
+
// 4. [ ] replace reporesolver to reposervice
58
+
func (rr *RepoResolver) GetRepoInfo(r *http.Request, user *oauth.User) repoinfo.RepoInfo {
59
+
ownerId, ook := r.Context().Value("resolvedId").(identity.Identity)
60
+
repo, rok := r.Context().Value("repo").(*models.Repo)
61
+
if !ook || !rok {
62
+
log.Println("malformed request, failed to get repo from context")
88
63
}
89
64
90
-
return p
91
-
}
65
+
// get dir/ref
66
+
currentDir := path.Dir(extractPathAfterRef(r.URL.EscapedPath()))
67
+
ref := chi.URLParam(r, "ref")
92
68
93
-
func (f *ResolvedRepo) Collaborators(ctx context.Context) ([]pages.Collaborator, error) {
94
-
repoCollaborators, err := f.rr.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
95
-
if err != nil {
96
-
return nil, err
69
+
repoAt := repo.RepoAt()
70
+
isStarred := false
71
+
roles := repoinfo.RolesInRepo{}
72
+
if user != nil {
73
+
isStarred = db.GetStarStatus(rr.execer, user.Did, repoAt)
74
+
roles.Roles = rr.enforcer.GetPermissionsInRepo(user.Did, repo.Knot, repo.DidSlashRepo())
97
75
}
98
76
99
-
var collaborators []pages.Collaborator
100
-
for _, item := range repoCollaborators {
101
-
// currently only two roles: owner and member
102
-
var role string
103
-
switch item[3] {
104
-
case "repo:owner":
105
-
role = "owner"
106
-
case "repo:collaborator":
107
-
role = "collaborator"
108
-
default:
109
-
continue
77
+
stats := repo.RepoStats
78
+
if stats == nil {
79
+
starCount, err := db.GetStarCount(rr.execer, repoAt)
80
+
if err != nil {
81
+
log.Println("failed to get star count for ", repoAt)
110
82
}
111
-
112
-
did := item[0]
113
-
114
-
c := pages.Collaborator{
115
-
Did: did,
116
-
Handle: "",
117
-
Role: role,
83
+
issueCount, err := db.GetIssueCount(rr.execer, repoAt)
84
+
if err != nil {
85
+
log.Println("failed to get issue count for ", repoAt)
118
86
}
119
-
collaborators = append(collaborators, c)
120
-
}
121
-
122
-
// populate all collborators with handles
123
-
identsToResolve := make([]string, len(collaborators))
124
-
for i, collab := range collaborators {
125
-
identsToResolve[i] = collab.Did
126
-
}
127
-
128
-
resolvedIdents := f.rr.idResolver.ResolveIdents(ctx, identsToResolve)
129
-
for i, resolved := range resolvedIdents {
130
-
if resolved != nil {
131
-
collaborators[i].Handle = resolved.Handle.String()
87
+
pullCount, err := db.GetPullCount(rr.execer, repoAt)
88
+
if err != nil {
89
+
log.Println("failed to get pull count for ", repoAt)
132
90
}
133
-
}
134
-
135
-
return collaborators, nil
136
-
}
137
-
138
-
// this function is a bit weird since it now returns RepoInfo from an entirely different
139
-
// package. we should refactor this or get rid of RepoInfo entirely.
140
-
func (f *ResolvedRepo) RepoInfo(user *oauth.User) repoinfo.RepoInfo {
141
-
repoAt := f.RepoAt()
142
-
isStarred := false
143
-
if user != nil {
144
-
isStarred = db.GetStarStatus(f.rr.execer, user.Did, repoAt)
145
-
}
146
-
147
-
starCount, err := db.GetStarCount(f.rr.execer, repoAt)
148
-
if err != nil {
149
-
log.Println("failed to get star count for ", repoAt)
150
-
}
151
-
issueCount, err := db.GetIssueCount(f.rr.execer, repoAt)
152
-
if err != nil {
153
-
log.Println("failed to get issue count for ", repoAt)
154
-
}
155
-
pullCount, err := db.GetPullCount(f.rr.execer, repoAt)
156
-
if err != nil {
157
-
log.Println("failed to get issue count for ", repoAt)
158
-
}
159
-
source, err := db.GetRepoSource(f.rr.execer, repoAt)
160
-
if errors.Is(err, sql.ErrNoRows) {
161
-
source = ""
162
-
} else if err != nil {
163
-
log.Println("failed to get repo source for ", repoAt, err)
91
+
stats = &models.RepoStats{
92
+
StarCount: starCount,
93
+
IssueCount: issueCount,
94
+
PullCount: pullCount,
95
+
}
164
96
}
165
97
166
98
var sourceRepo *models.Repo
167
-
if source != "" {
168
-
sourceRepo, err = db.GetRepoByAtUri(f.rr.execer, source)
99
+
var err error
100
+
if repo.Source != "" {
101
+
sourceRepo, err = db.GetRepoByAtUri(rr.execer, repo.Source)
169
102
if err != nil {
170
103
log.Println("failed to get repo by at uri", err)
171
104
}
172
105
}
173
106
174
-
var sourceHandle *identity.Identity
175
-
if sourceRepo != nil {
176
-
sourceHandle, err = f.rr.idResolver.ResolveIdent(context.Background(), sourceRepo.Did)
177
-
if err != nil {
178
-
log.Println("failed to resolve source repo", err)
179
-
}
180
-
}
107
+
repoInfo := repoinfo.RepoInfo{
108
+
// this is basically a models.Repo
109
+
OwnerDid: ownerId.DID.String(),
110
+
OwnerHandle: ownerId.Handle.String(),
111
+
Name: repo.Name,
112
+
Rkey: repo.Rkey,
113
+
Description: repo.Description,
114
+
Website: repo.Website,
115
+
Topics: repo.Topics,
116
+
Knot: repo.Knot,
117
+
Spindle: repo.Spindle,
118
+
Stats: *stats,
181
119
182
-
knot := f.Knot
120
+
// fork repo upstream
121
+
Source: sourceRepo,
183
122
184
-
repoInfo := repoinfo.RepoInfo{
185
-
OwnerDid: f.OwnerDid(),
186
-
OwnerHandle: f.OwnerHandle(),
187
-
Name: f.Name,
188
-
Rkey: f.Repo.Rkey,
189
-
RepoAt: repoAt,
190
-
Description: f.Description,
191
-
Website: f.Website,
192
-
Topics: f.Topics,
193
-
IsStarred: isStarred,
194
-
Knot: knot,
195
-
Spindle: f.Spindle,
196
-
Roles: f.RolesInRepo(user),
197
-
Stats: models.RepoStats{
198
-
StarCount: starCount,
199
-
IssueCount: issueCount,
200
-
PullCount: pullCount,
201
-
},
202
-
CurrentDir: f.CurrentDir,
203
-
Ref: f.Ref,
204
-
}
123
+
// page context
124
+
CurrentDir: currentDir,
125
+
Ref: ref,
205
126
206
-
if sourceRepo != nil {
207
-
repoInfo.Source = sourceRepo
208
-
repoInfo.SourceHandle = sourceHandle.Handle.String()
127
+
// info related to the session
128
+
IsStarred: isStarred,
129
+
Roles: roles,
209
130
}
210
131
211
132
return repoInfo
212
-
}
213
-
214
-
func (f *ResolvedRepo) RolesInRepo(u *oauth.User) repoinfo.RolesInRepo {
215
-
if u != nil {
216
-
r := f.rr.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.DidSlashRepo())
217
-
return repoinfo.RolesInRepo{Roles: r}
218
-
} else {
219
-
return repoinfo.RolesInRepo{}
220
-
}
221
133
}
222
134
223
135
// extractPathAfterRef gets the actual repository path
+5
-4
appview/serververify/verify.go
+5
-4
appview/serververify/verify.go
···
9
9
"tangled.org/core/api/tangled"
10
10
"tangled.org/core/appview/db"
11
11
"tangled.org/core/appview/xrpcclient"
12
+
"tangled.org/core/orm"
12
13
"tangled.org/core/rbac"
13
14
)
14
15
···
76
77
// mark this spindle as verified in the db
77
78
rowId, err := db.VerifySpindle(
78
79
tx,
79
-
db.FilterEq("owner", owner),
80
-
db.FilterEq("instance", instance),
80
+
orm.FilterEq("owner", owner),
81
+
orm.FilterEq("instance", instance),
81
82
)
82
83
if err != nil {
83
84
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
115
116
// mark as registered
116
117
err = db.MarkRegistered(
117
118
tx,
118
-
db.FilterEq("did", owner),
119
-
db.FilterEq("domain", domain),
119
+
orm.FilterEq("did", owner),
120
+
orm.FilterEq("domain", domain),
120
121
)
121
122
if err != nil {
122
123
return fmt.Errorf("failed to register domain: %w", err)
+2
appview/settings/settings.go
+2
appview/settings/settings.go
+44
-26
appview/spindles/spindles.go
+44
-26
appview/spindles/spindles.go
···
20
20
"tangled.org/core/appview/serververify"
21
21
"tangled.org/core/appview/xrpcclient"
22
22
"tangled.org/core/idresolver"
23
+
"tangled.org/core/orm"
23
24
"tangled.org/core/rbac"
24
25
"tangled.org/core/tid"
25
26
···
38
39
Logger *slog.Logger
39
40
}
40
41
42
+
type tab = map[string]any
43
+
44
+
var (
45
+
spindlesTabs []tab = []tab{
46
+
{"Name": "profile", "Icon": "user"},
47
+
{"Name": "keys", "Icon": "key"},
48
+
{"Name": "emails", "Icon": "mail"},
49
+
{"Name": "notifications", "Icon": "bell"},
50
+
{"Name": "knots", "Icon": "volleyball"},
51
+
{"Name": "spindles", "Icon": "spool"},
52
+
}
53
+
)
54
+
41
55
func (s *Spindles) Router() http.Handler {
42
56
r := chi.NewRouter()
43
57
···
58
72
user := s.OAuth.GetUser(r)
59
73
all, err := db.GetSpindles(
60
74
s.Db,
61
-
db.FilterEq("owner", user.Did),
75
+
orm.FilterEq("owner", user.Did),
62
76
)
63
77
if err != nil {
64
78
s.Logger.Error("failed to fetch spindles", "err", err)
···
69
83
s.Pages.Spindles(w, pages.SpindlesParams{
70
84
LoggedInUser: user,
71
85
Spindles: all,
86
+
Tabs: spindlesTabs,
87
+
Tab: "spindles",
72
88
})
73
89
}
74
90
···
86
102
87
103
spindles, err := db.GetSpindles(
88
104
s.Db,
89
-
db.FilterEq("instance", instance),
90
-
db.FilterEq("owner", user.Did),
91
-
db.FilterIsNot("verified", "null"),
105
+
orm.FilterEq("instance", instance),
106
+
orm.FilterEq("owner", user.Did),
107
+
orm.FilterIsNot("verified", "null"),
92
108
)
93
109
if err != nil || len(spindles) != 1 {
94
110
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
108
124
repos, err := db.GetRepos(
109
125
s.Db,
110
126
0,
111
-
db.FilterEq("spindle", instance),
127
+
orm.FilterEq("spindle", instance),
112
128
)
113
129
if err != nil {
114
130
l.Error("failed to get spindle repos", "err", err)
···
127
143
Spindle: spindle,
128
144
Members: members,
129
145
Repos: repoMap,
146
+
Tabs: spindlesTabs,
147
+
Tab: "spindles",
130
148
})
131
149
}
132
150
···
273
291
274
292
spindles, err := db.GetSpindles(
275
293
s.Db,
276
-
db.FilterEq("owner", user.Did),
277
-
db.FilterEq("instance", instance),
294
+
orm.FilterEq("owner", user.Did),
295
+
orm.FilterEq("instance", instance),
278
296
)
279
297
if err != nil || len(spindles) != 1 {
280
298
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
302
320
// remove spindle members first
303
321
err = db.RemoveSpindleMember(
304
322
tx,
305
-
db.FilterEq("did", user.Did),
306
-
db.FilterEq("instance", instance),
323
+
orm.FilterEq("did", user.Did),
324
+
orm.FilterEq("instance", instance),
307
325
)
308
326
if err != nil {
309
327
l.Error("failed to remove spindle members", "err", err)
···
313
331
314
332
err = db.DeleteSpindle(
315
333
tx,
316
-
db.FilterEq("owner", user.Did),
317
-
db.FilterEq("instance", instance),
334
+
orm.FilterEq("owner", user.Did),
335
+
orm.FilterEq("instance", instance),
318
336
)
319
337
if err != nil {
320
338
l.Error("failed to delete spindle", "err", err)
···
365
383
366
384
shouldRedirect := r.Header.Get("shouldRedirect")
367
385
if shouldRedirect == "true" {
368
-
s.Pages.HxRedirect(w, "/spindles")
386
+
s.Pages.HxRedirect(w, "/settings/spindles")
369
387
return
370
388
}
371
389
···
393
411
394
412
spindles, err := db.GetSpindles(
395
413
s.Db,
396
-
db.FilterEq("owner", user.Did),
397
-
db.FilterEq("instance", instance),
414
+
orm.FilterEq("owner", user.Did),
415
+
orm.FilterEq("instance", instance),
398
416
)
399
417
if err != nil || len(spindles) != 1 {
400
418
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
436
454
437
455
verifiedSpindle, err := db.GetSpindles(
438
456
s.Db,
439
-
db.FilterEq("id", rowId),
457
+
orm.FilterEq("id", rowId),
440
458
)
441
459
if err != nil || len(verifiedSpindle) != 1 {
442
460
l.Error("failed get new spindle", "err", err)
···
469
487
470
488
spindles, err := db.GetSpindles(
471
489
s.Db,
472
-
db.FilterEq("owner", user.Did),
473
-
db.FilterEq("instance", instance),
490
+
orm.FilterEq("owner", user.Did),
491
+
orm.FilterEq("instance", instance),
474
492
)
475
493
if err != nil || len(spindles) != 1 {
476
494
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
581
599
}
582
600
583
601
// success
584
-
s.Pages.HxRedirect(w, fmt.Sprintf("/spindles/%s", instance))
602
+
s.Pages.HxRedirect(w, fmt.Sprintf("/settings/spindles/%s", instance))
585
603
}
586
604
587
605
func (s *Spindles) removeMember(w http.ResponseWriter, r *http.Request) {
···
605
623
606
624
spindles, err := db.GetSpindles(
607
625
s.Db,
608
-
db.FilterEq("owner", user.Did),
609
-
db.FilterEq("instance", instance),
626
+
orm.FilterEq("owner", user.Did),
627
+
orm.FilterEq("instance", instance),
610
628
)
611
629
if err != nil || len(spindles) != 1 {
612
630
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
655
673
// get the record from the DB first:
656
674
members, err := db.GetSpindleMembers(
657
675
s.Db,
658
-
db.FilterEq("did", user.Did),
659
-
db.FilterEq("instance", instance),
660
-
db.FilterEq("subject", memberId.DID),
676
+
orm.FilterEq("did", user.Did),
677
+
orm.FilterEq("instance", instance),
678
+
orm.FilterEq("subject", memberId.DID),
661
679
)
662
680
if err != nil || len(members) != 1 {
663
681
l.Error("failed to get member", "err", err)
···
668
686
// remove from db
669
687
if err = db.RemoveSpindleMember(
670
688
tx,
671
-
db.FilterEq("did", user.Did),
672
-
db.FilterEq("instance", instance),
673
-
db.FilterEq("subject", memberId.DID),
689
+
orm.FilterEq("did", user.Did),
690
+
orm.FilterEq("instance", instance),
691
+
orm.FilterEq("subject", memberId.DID),
674
692
); err != nil {
675
693
l.Error("failed to remove spindle member", "err", err)
676
694
fail()
+6
-5
appview/state/gfi.go
+6
-5
appview/state/gfi.go
···
11
11
"tangled.org/core/appview/pages"
12
12
"tangled.org/core/appview/pagination"
13
13
"tangled.org/core/consts"
14
+
"tangled.org/core/orm"
14
15
)
15
16
16
17
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
20
21
21
22
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
22
23
23
-
gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel))
24
+
gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel))
24
25
if err != nil {
25
26
log.Println("failed to get gfi label def", err)
26
27
s.pages.Error500(w)
27
28
return
28
29
}
29
30
30
-
repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
31
+
repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel))
31
32
if err != nil {
32
33
log.Println("failed to get repo labels", err)
33
34
s.pages.Error503(w)
···
55
56
pagination.Page{
56
57
Limit: 500,
57
58
},
58
-
db.FilterIn("repo_at", repoUris),
59
-
db.FilterEq("open", 1),
59
+
orm.FilterIn("repo_at", repoUris),
60
+
orm.FilterEq("open", 1),
60
61
)
61
62
if err != nil {
62
63
log.Println("failed to get issues", err)
···
132
133
}
133
134
134
135
if len(uriList) > 0 {
135
-
allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList))
136
+
allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList))
136
137
if err != nil {
137
138
log.Println("failed to fetch labels", err)
138
139
}
+6
-5
appview/state/knotstream.go
+6
-5
appview/state/knotstream.go
···
16
16
ec "tangled.org/core/eventconsumer"
17
17
"tangled.org/core/eventconsumer/cursor"
18
18
"tangled.org/core/log"
19
+
"tangled.org/core/orm"
19
20
"tangled.org/core/rbac"
20
21
"tangled.org/core/workflow"
21
22
···
30
31
31
32
knots, err := db.GetRegistrations(
32
33
d,
33
-
db.FilterIsNot("registered", "null"),
34
+
orm.FilterIsNot("registered", "null"),
34
35
)
35
36
if err != nil {
36
37
return nil, err
···
143
144
repos, err := db.GetRepos(
144
145
d,
145
146
0,
146
-
db.FilterEq("did", record.RepoDid),
147
-
db.FilterEq("name", record.RepoName),
147
+
orm.FilterEq("did", record.RepoDid),
148
+
orm.FilterEq("name", record.RepoName),
148
149
)
149
150
if err != nil {
150
151
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
209
210
repos, err := db.GetRepos(
210
211
d,
211
212
0,
212
-
db.FilterEq("did", record.TriggerMetadata.Repo.Did),
213
-
db.FilterEq("name", record.TriggerMetadata.Repo.Repo),
213
+
orm.FilterEq("did", record.TriggerMetadata.Repo.Did),
214
+
orm.FilterEq("name", record.TriggerMetadata.Repo.Repo),
214
215
)
215
216
if err != nil {
216
217
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
+152
-21
appview/state/profile.go
+152
-21
appview/state/profile.go
···
19
19
"tangled.org/core/appview/db"
20
20
"tangled.org/core/appview/models"
21
21
"tangled.org/core/appview/pages"
22
+
"tangled.org/core/orm"
22
23
)
23
24
24
25
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
56
57
return nil, fmt.Errorf("failed to get profile: %w", err)
57
58
}
58
59
59
-
repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did))
60
+
repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did))
60
61
if err != nil {
61
62
return nil, fmt.Errorf("failed to get repo count: %w", err)
62
63
}
63
64
64
-
stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did))
65
+
stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did))
65
66
if err != nil {
66
67
return nil, fmt.Errorf("failed to get string count: %w", err)
67
68
}
68
69
69
-
starredCount, err := db.CountStars(s.db, db.FilterEq("starred_by_did", did))
70
+
starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did))
70
71
if err != nil {
71
72
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
72
73
}
···
86
87
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
87
88
punchcard, err := db.MakePunchcard(
88
89
s.db,
89
-
db.FilterEq("did", did),
90
-
db.FilterGte("date", startOfYear.Format(time.DateOnly)),
91
-
db.FilterLte("date", now.Format(time.DateOnly)),
90
+
orm.FilterEq("did", did),
91
+
orm.FilterGte("date", startOfYear.Format(time.DateOnly)),
92
+
orm.FilterLte("date", now.Format(time.DateOnly)),
92
93
)
93
94
if err != nil {
94
95
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
96
97
97
98
return &pages.ProfileCard{
98
99
UserDid: did,
99
-
UserHandle: ident.Handle.String(),
100
100
Profile: profile,
101
101
FollowStatus: followStatus,
102
102
Stats: pages.ProfileStats{
···
119
119
s.pages.Error500(w)
120
120
return
121
121
}
122
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
122
+
l = l.With("profileDid", profile.UserDid)
123
123
124
124
repos, err := db.GetRepos(
125
125
s.db,
126
126
0,
127
-
db.FilterEq("did", profile.UserDid),
127
+
orm.FilterEq("did", profile.UserDid),
128
128
)
129
129
if err != nil {
130
130
l.Error("failed to fetch repos", "err", err)
···
162
162
l.Error("failed to create timeline", "err", err)
163
163
}
164
164
165
+
// populate commit counts in the timeline, using the punchcard
166
+
currentMonth := time.Now().Month()
167
+
for _, p := range profile.Punchcard.Punches {
168
+
idx := currentMonth - p.Date.Month()
169
+
if int(idx) < len(timeline.ByMonth) {
170
+
timeline.ByMonth[idx].Commits += p.Count
171
+
}
172
+
}
173
+
165
174
s.pages.ProfileOverview(w, pages.ProfileOverviewParams{
166
175
LoggedInUser: s.oauth.GetUser(r),
167
176
Card: profile,
···
180
189
s.pages.Error500(w)
181
190
return
182
191
}
183
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
192
+
l = l.With("profileDid", profile.UserDid)
184
193
185
194
repos, err := db.GetRepos(
186
195
s.db,
187
196
0,
188
-
db.FilterEq("did", profile.UserDid),
197
+
orm.FilterEq("did", profile.UserDid),
189
198
)
190
199
if err != nil {
191
200
l.Error("failed to get repos", "err", err)
···
209
218
s.pages.Error500(w)
210
219
return
211
220
}
212
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
221
+
l = l.With("profileDid", profile.UserDid)
213
222
214
-
stars, err := db.GetStars(s.db, 0, db.FilterEq("starred_by_did", profile.UserDid))
223
+
stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid))
215
224
if err != nil {
216
225
l.Error("failed to get stars", "err", err)
217
226
s.pages.Error500(w)
···
219
228
}
220
229
var repos []models.Repo
221
230
for _, s := range stars {
222
-
if s.Repo != nil {
223
-
repos = append(repos, *s.Repo)
224
-
}
231
+
repos = append(repos, *s.Repo)
225
232
}
226
233
227
234
err = s.pages.ProfileStarred(w, pages.ProfileStarredParams{
···
240
247
s.pages.Error500(w)
241
248
return
242
249
}
243
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
250
+
l = l.With("profileDid", profile.UserDid)
244
251
245
-
strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid))
252
+
strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid))
246
253
if err != nil {
247
254
l.Error("failed to get strings", "err", err)
248
255
s.pages.Error500(w)
···
272
279
if err != nil {
273
280
return nil, err
274
281
}
275
-
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
282
+
l = l.With("profileDid", profile.UserDid)
276
283
277
284
loggedInUser := s.oauth.GetUser(r)
278
285
params := FollowsPageParams{
···
294
301
followDids = append(followDids, extractDid(follow))
295
302
}
296
303
297
-
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
304
+
profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids))
298
305
if err != nil {
299
306
l.Error("failed to get profiles", "followDids", followDids, "err", err)
300
307
return ¶ms, err
···
697
704
log.Printf("getting profile data for %s: %s", user.Did, err)
698
705
}
699
706
700
-
repos, err := db.GetRepos(s.db, 0, db.FilterEq("did", user.Did))
707
+
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did))
701
708
if err != nil {
702
709
log.Printf("getting repos for %s: %s", user.Did, err)
703
710
}
···
730
737
AllRepos: allRepos,
731
738
})
732
739
}
740
+
741
+
func (s *State) UploadProfileAvatar(w http.ResponseWriter, r *http.Request) {
742
+
l := s.logger.With("handler", "UploadProfileAvatar")
743
+
user := s.oauth.GetUser(r)
744
+
l = l.With("did", user.Did)
745
+
746
+
// Parse multipart form (10MB max)
747
+
if err := r.ParseMultipartForm(10 << 20); err != nil {
748
+
l.Error("failed to parse form", "err", err)
749
+
w.WriteHeader(http.StatusBadRequest)
750
+
fmt.Fprintf(w, "Failed to parse form")
751
+
return
752
+
}
753
+
754
+
file, handler, err := r.FormFile("avatar")
755
+
if err != nil {
756
+
l.Error("failed to read avatar file", "err", err)
757
+
w.WriteHeader(http.StatusBadRequest)
758
+
fmt.Fprintf(w, "Failed to read avatar file")
759
+
return
760
+
}
761
+
defer file.Close()
762
+
763
+
if handler.Size > 1000000 {
764
+
l.Warn("avatar file too large", "size", handler.Size)
765
+
w.WriteHeader(http.StatusBadRequest)
766
+
fmt.Fprintf(w, "Avatar file too large (max 1MB)")
767
+
return
768
+
}
769
+
770
+
contentType := handler.Header.Get("Content-Type")
771
+
if contentType != "image/png" && contentType != "image/jpeg" {
772
+
l.Warn("invalid image type", "contentType", contentType)
773
+
w.WriteHeader(http.StatusBadRequest)
774
+
fmt.Fprintf(w, "Invalid image type (only PNG and JPEG allowed)")
775
+
return
776
+
}
777
+
778
+
client, err := s.oauth.AuthorizedClient(r)
779
+
if err != nil {
780
+
l.Error("failed to get PDS client", "err", err)
781
+
w.WriteHeader(http.StatusInternalServerError)
782
+
fmt.Fprintf(w, "Failed to connect to your PDS")
783
+
return
784
+
}
785
+
786
+
uploadBlobResp, err := comatproto.RepoUploadBlob(r.Context(), client, file)
787
+
if err != nil {
788
+
l.Error("failed to upload avatar blob", "err", err)
789
+
w.WriteHeader(http.StatusInternalServerError)
790
+
fmt.Fprintf(w, "Failed to upload avatar to your PDS")
791
+
return
792
+
}
793
+
794
+
l.Info("uploaded avatar blob", "cid", uploadBlobResp.Blob.Ref.String())
795
+
796
+
// get current profile record from PDS to get its CID for swap
797
+
getRecordResp, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.ActorProfileNSID, user.Did, "self")
798
+
if err != nil {
799
+
l.Error("failed to get current profile record", "err", err)
800
+
w.WriteHeader(http.StatusInternalServerError)
801
+
fmt.Fprintf(w, "Failed to get current profile from your PDS")
802
+
return
803
+
}
804
+
805
+
var profileRecord *tangled.ActorProfile
806
+
if getRecordResp.Value != nil {
807
+
if val, ok := getRecordResp.Value.Val.(*tangled.ActorProfile); ok {
808
+
profileRecord = val
809
+
} else {
810
+
l.Warn("profile record type assertion failed, creating new record")
811
+
profileRecord = &tangled.ActorProfile{}
812
+
}
813
+
} else {
814
+
l.Warn("no existing profile record, creating new record")
815
+
profileRecord = &tangled.ActorProfile{}
816
+
}
817
+
818
+
profileRecord.Avatar = uploadBlobResp.Blob
819
+
820
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
821
+
Collection: tangled.ActorProfileNSID,
822
+
Repo: user.Did,
823
+
Rkey: "self",
824
+
Record: &lexutil.LexiconTypeDecoder{Val: profileRecord},
825
+
SwapRecord: getRecordResp.Cid,
826
+
})
827
+
828
+
if err != nil {
829
+
l.Error("failed to update profile record", "err", err)
830
+
w.WriteHeader(http.StatusInternalServerError)
831
+
fmt.Fprintf(w, "Failed to update profile on your PDS")
832
+
return
833
+
}
834
+
835
+
l.Info("successfully updated profile with avatar")
836
+
837
+
profile, err := db.GetProfile(s.db, user.Did)
838
+
if err != nil {
839
+
l.Warn("getting profile data from DB", "err", err)
840
+
profile = &models.Profile{Did: user.Did}
841
+
}
842
+
profile.Avatar = uploadBlobResp.Blob.Ref.String()
843
+
844
+
tx, err := s.db.BeginTx(r.Context(), nil)
845
+
if err != nil {
846
+
l.Error("failed to start transaction", "err", err)
847
+
s.pages.HxRefresh(w)
848
+
w.WriteHeader(http.StatusOK)
849
+
return
850
+
}
851
+
852
+
err = db.UpsertProfile(tx, profile)
853
+
if err != nil {
854
+
l.Error("failed to update profile in DB", "err", err)
855
+
tx.Rollback()
856
+
s.pages.HxRefresh(w)
857
+
w.WriteHeader(http.StatusOK)
858
+
return
859
+
}
860
+
861
+
w.Header().Set("HX-Redirect", r.Header.Get("Referer"))
862
+
w.WriteHeader(http.StatusOK)
863
+
}
+8
-2
appview/state/router.go
+8
-2
appview/state/router.go
···
162
162
r.Get("/edit-pins", s.EditPinsFragment)
163
163
r.Post("/bio", s.UpdateProfileBio)
164
164
r.Post("/pins", s.UpdateProfilePins)
165
+
r.Post("/avatar", s.UploadProfileAvatar)
165
166
})
166
167
167
168
r.Mount("/settings", s.SettingsRouter())
168
169
r.Mount("/strings", s.StringsRouter(mw))
169
-
r.Mount("/knots", s.KnotsRouter())
170
-
r.Mount("/spindles", s.SpindlesRouter())
170
+
171
+
r.Mount("/settings/knots", s.KnotsRouter())
172
+
r.Mount("/settings/spindles", s.SpindlesRouter())
173
+
171
174
r.Mount("/notifications", s.NotificationsRouter(mw))
172
175
173
176
r.Mount("/signup", s.SignupRouter())
···
261
264
issues := issues.New(
262
265
s.oauth,
263
266
s.repoResolver,
267
+
s.enforcer,
264
268
s.pages,
265
269
s.idResolver,
270
+
s.mentionsResolver,
266
271
s.db,
267
272
s.config,
268
273
s.notifier,
···
279
284
s.repoResolver,
280
285
s.pages,
281
286
s.idResolver,
287
+
s.mentionsResolver,
282
288
s.db,
283
289
s.config,
284
290
s.notifier,
+2
-1
appview/state/spindlestream.go
+2
-1
appview/state/spindlestream.go
···
17
17
ec "tangled.org/core/eventconsumer"
18
18
"tangled.org/core/eventconsumer/cursor"
19
19
"tangled.org/core/log"
20
+
"tangled.org/core/orm"
20
21
"tangled.org/core/rbac"
21
22
spindle "tangled.org/core/spindle/models"
22
23
)
···
27
28
28
29
spindles, err := db.GetSpindles(
29
30
d,
30
-
db.FilterIsNot("verified", "null"),
31
+
orm.FilterIsNot("verified", "null"),
31
32
)
32
33
if err != nil {
33
34
return nil, err
+9
-13
appview/state/star.go
+9
-13
appview/state/star.go
···
57
57
log.Println("created atproto record: ", resp.Uri)
58
58
59
59
star := &models.Star{
60
-
StarredByDid: currentUser.Did,
61
-
RepoAt: subjectUri,
62
-
Rkey: rkey,
60
+
Did: currentUser.Did,
61
+
RepoAt: subjectUri,
62
+
Rkey: rkey,
63
63
}
64
64
65
65
err = db.AddStar(s.db, star)
···
75
75
76
76
s.notifier.NewStar(r.Context(), star)
77
77
78
-
s.pages.RepoStarFragment(w, pages.RepoStarFragmentParams{
78
+
s.pages.StarBtnFragment(w, pages.StarBtnFragmentParams{
79
79
IsStarred: true,
80
-
RepoAt: subjectUri,
81
-
Stats: models.RepoStats{
82
-
StarCount: starCount,
83
-
},
80
+
SubjectAt: subjectUri,
81
+
StarCount: starCount,
84
82
})
85
83
86
84
return
···
117
115
118
116
s.notifier.DeleteStar(r.Context(), star)
119
117
120
-
s.pages.RepoStarFragment(w, pages.RepoStarFragmentParams{
118
+
s.pages.StarBtnFragment(w, pages.StarBtnFragmentParams{
121
119
IsStarred: false,
122
-
RepoAt: subjectUri,
123
-
Stats: models.RepoStats{
124
-
StarCount: starCount,
125
-
},
120
+
SubjectAt: subjectUri,
121
+
StarCount: starCount,
126
122
})
127
123
128
124
return
+30
-24
appview/state/state.go
+30
-24
appview/state/state.go
···
15
15
"tangled.org/core/appview/config"
16
16
"tangled.org/core/appview/db"
17
17
"tangled.org/core/appview/indexer"
18
+
"tangled.org/core/appview/mentions"
18
19
"tangled.org/core/appview/models"
19
20
"tangled.org/core/appview/notify"
20
21
dbnotify "tangled.org/core/appview/notify/db"
···
29
30
"tangled.org/core/jetstream"
30
31
"tangled.org/core/log"
31
32
tlog "tangled.org/core/log"
33
+
"tangled.org/core/orm"
32
34
"tangled.org/core/rbac"
33
35
"tangled.org/core/tid"
34
36
···
42
44
)
43
45
44
46
type State struct {
45
-
db *db.DB
46
-
notifier notify.Notifier
47
-
indexer *indexer.Indexer
48
-
oauth *oauth.OAuth
49
-
enforcer *rbac.Enforcer
50
-
pages *pages.Pages
51
-
idResolver *idresolver.Resolver
52
-
posthog posthog.Client
53
-
jc *jetstream.JetstreamClient
54
-
config *config.Config
55
-
repoResolver *reporesolver.RepoResolver
56
-
knotstream *eventconsumer.Consumer
57
-
spindlestream *eventconsumer.Consumer
58
-
logger *slog.Logger
59
-
validator *validator.Validator
47
+
db *db.DB
48
+
notifier notify.Notifier
49
+
indexer *indexer.Indexer
50
+
oauth *oauth.OAuth
51
+
enforcer *rbac.Enforcer
52
+
pages *pages.Pages
53
+
idResolver *idresolver.Resolver
54
+
mentionsResolver *mentions.Resolver
55
+
posthog posthog.Client
56
+
jc *jetstream.JetstreamClient
57
+
config *config.Config
58
+
repoResolver *reporesolver.RepoResolver
59
+
knotstream *eventconsumer.Consumer
60
+
spindlestream *eventconsumer.Consumer
61
+
logger *slog.Logger
62
+
validator *validator.Validator
60
63
}
61
64
62
65
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
96
99
}
97
100
validator := validator.New(d, res, enforcer)
98
101
99
-
repoResolver := reporesolver.New(config, enforcer, res, d)
102
+
repoResolver := reporesolver.New(config, enforcer, d)
103
+
104
+
mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver"))
100
105
101
106
wrapper := db.DbWrapper{Execer: d}
102
107
jc, err := jetstream.NewJetstreamClient(
···
178
183
enforcer,
179
184
pages,
180
185
res,
186
+
mentionsResolver,
181
187
posthog,
182
188
jc,
183
189
config,
···
294
300
return
295
301
}
296
302
297
-
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
303
+
gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
298
304
if err != nil {
299
305
// non-fatal
300
306
}
···
318
324
319
325
regs, err := db.GetRegistrations(
320
326
s.db,
321
-
db.FilterEq("did", user.Did),
322
-
db.FilterEq("needs_upgrade", 1),
327
+
orm.FilterEq("did", user.Did),
328
+
orm.FilterEq("needs_upgrade", 1),
323
329
)
324
330
if err != nil {
325
331
l.Error("non-fatal: failed to get registrations", "err", err)
···
327
333
328
334
spindles, err := db.GetSpindles(
329
335
s.db,
330
-
db.FilterEq("owner", user.Did),
331
-
db.FilterEq("needs_upgrade", 1),
336
+
orm.FilterEq("owner", user.Did),
337
+
orm.FilterEq("needs_upgrade", 1),
332
338
)
333
339
if err != nil {
334
340
l.Error("non-fatal: failed to get spindles", "err", err)
···
499
505
// Check for existing repos
500
506
existingRepo, err := db.GetRepo(
501
507
s.db,
502
-
db.FilterEq("did", user.Did),
503
-
db.FilterEq("name", repoName),
508
+
orm.FilterEq("did", user.Did),
509
+
orm.FilterEq("name", repoName),
504
510
)
505
511
if err == nil && existingRepo != nil {
506
512
l.Info("repo exists")
···
660
666
}
661
667
662
668
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
663
-
defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults))
669
+
defaultLabels, err := db.GetLabelDefinitions(e, orm.FilterIn("at_uri", defaults))
664
670
if err != nil {
665
671
return err
666
672
}
+21
-8
appview/strings/strings.go
+21
-8
appview/strings/strings.go
···
17
17
"tangled.org/core/appview/pages"
18
18
"tangled.org/core/appview/pages/markup"
19
19
"tangled.org/core/idresolver"
20
+
"tangled.org/core/orm"
20
21
"tangled.org/core/tid"
21
22
22
23
"github.com/bluesky-social/indigo/api/atproto"
···
108
109
strings, err := db.GetStrings(
109
110
s.Db,
110
111
0,
111
-
db.FilterEq("did", id.DID),
112
-
db.FilterEq("rkey", rkey),
112
+
orm.FilterEq("did", id.DID),
113
+
orm.FilterEq("rkey", rkey),
113
114
)
114
115
if err != nil {
115
116
l.Error("failed to fetch string", "err", err)
···
148
149
showRendered = r.URL.Query().Get("code") != "true"
149
150
}
150
151
152
+
starCount, err := db.GetStarCount(s.Db, string.AtUri())
153
+
if err != nil {
154
+
l.Error("failed to get star count", "err", err)
155
+
}
156
+
user := s.OAuth.GetUser(r)
157
+
isStarred := false
158
+
if user != nil {
159
+
isStarred = db.GetStarStatus(s.Db, user.Did, string.AtUri())
160
+
}
161
+
151
162
s.Pages.SingleString(w, pages.SingleStringParams{
152
-
LoggedInUser: s.OAuth.GetUser(r),
163
+
LoggedInUser: user,
153
164
RenderToggle: renderToggle,
154
165
ShowRendered: showRendered,
155
-
String: string,
166
+
String: &string,
156
167
Stats: string.Stats(),
168
+
IsStarred: isStarred,
169
+
StarCount: starCount,
157
170
Owner: id,
158
171
})
159
172
}
···
187
200
all, err := db.GetStrings(
188
201
s.Db,
189
202
0,
190
-
db.FilterEq("did", id.DID),
191
-
db.FilterEq("rkey", rkey),
203
+
orm.FilterEq("did", id.DID),
204
+
orm.FilterEq("rkey", rkey),
192
205
)
193
206
if err != nil {
194
207
l.Error("failed to fetch string", "err", err)
···
396
409
397
410
if err := db.DeleteString(
398
411
s.Db,
399
-
db.FilterEq("did", user.Did),
400
-
db.FilterEq("rkey", rkey),
412
+
orm.FilterEq("did", user.Did),
413
+
orm.FilterEq("rkey", rkey),
401
414
); err != nil {
402
415
fail("Failed to delete string.", err)
403
416
return
+2
-1
appview/validator/issue.go
+2
-1
appview/validator/issue.go
···
6
6
7
7
"tangled.org/core/appview/db"
8
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
9
10
)
10
11
11
12
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
12
13
// if comments have parents, only ingest ones that are 1 level deep
13
14
if comment.ReplyTo != nil {
14
-
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
15
+
parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
15
16
if err != nil {
16
17
return fmt.Errorf("failed to fetch parent comment: %w", err)
17
18
}
+3137
-3022
avatar/package-lock.json
+3137
-3022
avatar/package-lock.json
···
1
1
{
2
-
"name": "avatar",
3
-
"version": "0.0.0",
4
-
"lockfileVersion": 3,
5
-
"requires": true,
6
-
"packages": {
7
-
"": {
8
-
"name": "avatar",
9
-
"version": "0.0.0",
10
-
"devDependencies": {
11
-
"@cloudflare/vitest-pool-workers": "^0.8.19",
12
-
"vitest": "~3.0.7",
13
-
"wrangler": "^4.14.1"
14
-
}
15
-
},
16
-
"node_modules/@cloudflare/kv-asset-handler": {
17
-
"version": "0.4.0",
18
-
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz",
19
-
"integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==",
20
-
"dev": true,
21
-
"license": "MIT OR Apache-2.0",
22
-
"dependencies": {
23
-
"mime": "^3.0.0"
24
-
},
25
-
"engines": {
26
-
"node": ">=18.0.0"
27
-
}
28
-
},
29
-
"node_modules/@cloudflare/unenv-preset": {
30
-
"version": "2.3.1",
31
-
"resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz",
32
-
"integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==",
33
-
"dev": true,
34
-
"license": "MIT OR Apache-2.0",
35
-
"peerDependencies": {
36
-
"unenv": "2.0.0-rc.15",
37
-
"workerd": "^1.20250320.0"
38
-
},
39
-
"peerDependenciesMeta": {
40
-
"workerd": {
41
-
"optional": true
42
-
}
43
-
}
44
-
},
45
-
"node_modules/@cloudflare/vitest-pool-workers": {
46
-
"version": "0.8.24",
47
-
"resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.24.tgz",
48
-
"integrity": "sha512-wT2PABJQ9YLYWrVu4CRZOjvmjHkdbMyLTZPU9n/7JEMM3pgG8dY41F1Rj31UsXRQaXX39A/CTPGlk58dcMUysA==",
49
-
"dev": true,
50
-
"license": "MIT",
51
-
"dependencies": {
52
-
"birpc": "0.2.14",
53
-
"cjs-module-lexer": "^1.2.3",
54
-
"devalue": "^4.3.0",
55
-
"miniflare": "4.20250428.1",
56
-
"semver": "^7.7.1",
57
-
"wrangler": "4.14.1",
58
-
"zod": "^3.22.3"
59
-
},
60
-
"peerDependencies": {
61
-
"@vitest/runner": "2.0.x - 3.1.x",
62
-
"@vitest/snapshot": "2.0.x - 3.1.x",
63
-
"vitest": "2.0.x - 3.1.x"
64
-
}
65
-
},
66
-
"node_modules/@cloudflare/workerd-darwin-64": {
67
-
"version": "1.20250428.0",
68
-
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250428.0.tgz",
69
-
"integrity": "sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==",
70
-
"cpu": [
71
-
"x64"
72
-
],
73
-
"dev": true,
74
-
"license": "Apache-2.0",
75
-
"optional": true,
76
-
"os": [
77
-
"darwin"
78
-
],
79
-
"engines": {
80
-
"node": ">=16"
81
-
}
82
-
},
83
-
"node_modules/@cloudflare/workerd-darwin-arm64": {
84
-
"version": "1.20250428.0",
85
-
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250428.0.tgz",
86
-
"integrity": "sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==",
87
-
"cpu": [
88
-
"arm64"
89
-
],
90
-
"dev": true,
91
-
"license": "Apache-2.0",
92
-
"optional": true,
93
-
"os": [
94
-
"darwin"
95
-
],
96
-
"engines": {
97
-
"node": ">=16"
98
-
}
99
-
},
100
-
"node_modules/@cloudflare/workerd-linux-64": {
101
-
"version": "1.20250428.0",
102
-
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250428.0.tgz",
103
-
"integrity": "sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==",
104
-
"cpu": [
105
-
"x64"
106
-
],
107
-
"dev": true,
108
-
"license": "Apache-2.0",
109
-
"optional": true,
110
-
"os": [
111
-
"linux"
112
-
],
113
-
"engines": {
114
-
"node": ">=16"
115
-
}
116
-
},
117
-
"node_modules/@cloudflare/workerd-linux-arm64": {
118
-
"version": "1.20250428.0",
119
-
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250428.0.tgz",
120
-
"integrity": "sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==",
121
-
"cpu": [
122
-
"arm64"
123
-
],
124
-
"dev": true,
125
-
"license": "Apache-2.0",
126
-
"optional": true,
127
-
"os": [
128
-
"linux"
129
-
],
130
-
"engines": {
131
-
"node": ">=16"
132
-
}
133
-
},
134
-
"node_modules/@cloudflare/workerd-windows-64": {
135
-
"version": "1.20250428.0",
136
-
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250428.0.tgz",
137
-
"integrity": "sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==",
138
-
"cpu": [
139
-
"x64"
140
-
],
141
-
"dev": true,
142
-
"license": "Apache-2.0",
143
-
"optional": true,
144
-
"os": [
145
-
"win32"
146
-
],
147
-
"engines": {
148
-
"node": ">=16"
149
-
}
150
-
},
151
-
"node_modules/@cspotcode/source-map-support": {
152
-
"version": "0.8.1",
153
-
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
154
-
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
155
-
"dev": true,
156
-
"license": "MIT",
157
-
"dependencies": {
158
-
"@jridgewell/trace-mapping": "0.3.9"
159
-
},
160
-
"engines": {
161
-
"node": ">=12"
162
-
}
163
-
},
164
-
"node_modules/@emnapi/runtime": {
165
-
"version": "1.4.3",
166
-
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
167
-
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
168
-
"dev": true,
169
-
"license": "MIT",
170
-
"optional": true,
171
-
"dependencies": {
172
-
"tslib": "^2.4.0"
173
-
}
174
-
},
175
-
"node_modules/@esbuild/aix-ppc64": {
176
-
"version": "0.25.3",
177
-
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
178
-
"integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
179
-
"cpu": [
180
-
"ppc64"
181
-
],
182
-
"dev": true,
183
-
"license": "MIT",
184
-
"optional": true,
185
-
"os": [
186
-
"aix"
187
-
],
188
-
"engines": {
189
-
"node": ">=18"
190
-
}
191
-
},
192
-
"node_modules/@esbuild/android-arm": {
193
-
"version": "0.25.3",
194
-
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
195
-
"integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
196
-
"cpu": [
197
-
"arm"
198
-
],
199
-
"dev": true,
200
-
"license": "MIT",
201
-
"optional": true,
202
-
"os": [
203
-
"android"
204
-
],
205
-
"engines": {
206
-
"node": ">=18"
207
-
}
208
-
},
209
-
"node_modules/@esbuild/android-arm64": {
210
-
"version": "0.25.3",
211
-
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
212
-
"integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
213
-
"cpu": [
214
-
"arm64"
215
-
],
216
-
"dev": true,
217
-
"license": "MIT",
218
-
"optional": true,
219
-
"os": [
220
-
"android"
221
-
],
222
-
"engines": {
223
-
"node": ">=18"
224
-
}
225
-
},
226
-
"node_modules/@esbuild/android-x64": {
227
-
"version": "0.25.3",
228
-
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
229
-
"integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
230
-
"cpu": [
231
-
"x64"
232
-
],
233
-
"dev": true,
234
-
"license": "MIT",
235
-
"optional": true,
236
-
"os": [
237
-
"android"
238
-
],
239
-
"engines": {
240
-
"node": ">=18"
241
-
}
242
-
},
243
-
"node_modules/@esbuild/darwin-arm64": {
244
-
"version": "0.25.3",
245
-
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
246
-
"integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
247
-
"cpu": [
248
-
"arm64"
249
-
],
250
-
"dev": true,
251
-
"license": "MIT",
252
-
"optional": true,
253
-
"os": [
254
-
"darwin"
255
-
],
256
-
"engines": {
257
-
"node": ">=18"
258
-
}
259
-
},
260
-
"node_modules/@esbuild/darwin-x64": {
261
-
"version": "0.25.3",
262
-
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
263
-
"integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
264
-
"cpu": [
265
-
"x64"
266
-
],
267
-
"dev": true,
268
-
"license": "MIT",
269
-
"optional": true,
270
-
"os": [
271
-
"darwin"
272
-
],
273
-
"engines": {
274
-
"node": ">=18"
275
-
}
276
-
},
277
-
"node_modules/@esbuild/freebsd-arm64": {
278
-
"version": "0.25.3",
279
-
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
280
-
"integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
281
-
"cpu": [
282
-
"arm64"
283
-
],
284
-
"dev": true,
285
-
"license": "MIT",
286
-
"optional": true,
287
-
"os": [
288
-
"freebsd"
289
-
],
290
-
"engines": {
291
-
"node": ">=18"
292
-
}
293
-
},
294
-
"node_modules/@esbuild/freebsd-x64": {
295
-
"version": "0.25.3",
296
-
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
297
-
"integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
298
-
"cpu": [
299
-
"x64"
300
-
],
301
-
"dev": true,
302
-
"license": "MIT",
303
-
"optional": true,
304
-
"os": [
305
-
"freebsd"
306
-
],
307
-
"engines": {
308
-
"node": ">=18"
309
-
}
310
-
},
311
-
"node_modules/@esbuild/linux-arm": {
312
-
"version": "0.25.3",
313
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
314
-
"integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
315
-
"cpu": [
316
-
"arm"
317
-
],
318
-
"dev": true,
319
-
"license": "MIT",
320
-
"optional": true,
321
-
"os": [
322
-
"linux"
323
-
],
324
-
"engines": {
325
-
"node": ">=18"
326
-
}
327
-
},
328
-
"node_modules/@esbuild/linux-arm64": {
329
-
"version": "0.25.3",
330
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
331
-
"integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
332
-
"cpu": [
333
-
"arm64"
334
-
],
335
-
"dev": true,
336
-
"license": "MIT",
337
-
"optional": true,
338
-
"os": [
339
-
"linux"
340
-
],
341
-
"engines": {
342
-
"node": ">=18"
343
-
}
344
-
},
345
-
"node_modules/@esbuild/linux-ia32": {
346
-
"version": "0.25.3",
347
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
348
-
"integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
349
-
"cpu": [
350
-
"ia32"
351
-
],
352
-
"dev": true,
353
-
"license": "MIT",
354
-
"optional": true,
355
-
"os": [
356
-
"linux"
357
-
],
358
-
"engines": {
359
-
"node": ">=18"
360
-
}
361
-
},
362
-
"node_modules/@esbuild/linux-loong64": {
363
-
"version": "0.25.3",
364
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
365
-
"integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
366
-
"cpu": [
367
-
"loong64"
368
-
],
369
-
"dev": true,
370
-
"license": "MIT",
371
-
"optional": true,
372
-
"os": [
373
-
"linux"
374
-
],
375
-
"engines": {
376
-
"node": ">=18"
377
-
}
378
-
},
379
-
"node_modules/@esbuild/linux-mips64el": {
380
-
"version": "0.25.3",
381
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
382
-
"integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
383
-
"cpu": [
384
-
"mips64el"
385
-
],
386
-
"dev": true,
387
-
"license": "MIT",
388
-
"optional": true,
389
-
"os": [
390
-
"linux"
391
-
],
392
-
"engines": {
393
-
"node": ">=18"
394
-
}
395
-
},
396
-
"node_modules/@esbuild/linux-ppc64": {
397
-
"version": "0.25.3",
398
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
399
-
"integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
400
-
"cpu": [
401
-
"ppc64"
402
-
],
403
-
"dev": true,
404
-
"license": "MIT",
405
-
"optional": true,
406
-
"os": [
407
-
"linux"
408
-
],
409
-
"engines": {
410
-
"node": ">=18"
411
-
}
412
-
},
413
-
"node_modules/@esbuild/linux-riscv64": {
414
-
"version": "0.25.3",
415
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
416
-
"integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
417
-
"cpu": [
418
-
"riscv64"
419
-
],
420
-
"dev": true,
421
-
"license": "MIT",
422
-
"optional": true,
423
-
"os": [
424
-
"linux"
425
-
],
426
-
"engines": {
427
-
"node": ">=18"
428
-
}
429
-
},
430
-
"node_modules/@esbuild/linux-s390x": {
431
-
"version": "0.25.3",
432
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
433
-
"integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
434
-
"cpu": [
435
-
"s390x"
436
-
],
437
-
"dev": true,
438
-
"license": "MIT",
439
-
"optional": true,
440
-
"os": [
441
-
"linux"
442
-
],
443
-
"engines": {
444
-
"node": ">=18"
445
-
}
446
-
},
447
-
"node_modules/@esbuild/linux-x64": {
448
-
"version": "0.25.3",
449
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
450
-
"integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
451
-
"cpu": [
452
-
"x64"
453
-
],
454
-
"dev": true,
455
-
"license": "MIT",
456
-
"optional": true,
457
-
"os": [
458
-
"linux"
459
-
],
460
-
"engines": {
461
-
"node": ">=18"
462
-
}
463
-
},
464
-
"node_modules/@esbuild/netbsd-arm64": {
465
-
"version": "0.25.3",
466
-
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
467
-
"integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
468
-
"cpu": [
469
-
"arm64"
470
-
],
471
-
"dev": true,
472
-
"license": "MIT",
473
-
"optional": true,
474
-
"os": [
475
-
"netbsd"
476
-
],
477
-
"engines": {
478
-
"node": ">=18"
479
-
}
480
-
},
481
-
"node_modules/@esbuild/netbsd-x64": {
482
-
"version": "0.25.3",
483
-
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
484
-
"integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
485
-
"cpu": [
486
-
"x64"
487
-
],
488
-
"dev": true,
489
-
"license": "MIT",
490
-
"optional": true,
491
-
"os": [
492
-
"netbsd"
493
-
],
494
-
"engines": {
495
-
"node": ">=18"
496
-
}
497
-
},
498
-
"node_modules/@esbuild/openbsd-arm64": {
499
-
"version": "0.25.3",
500
-
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
501
-
"integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
502
-
"cpu": [
503
-
"arm64"
504
-
],
505
-
"dev": true,
506
-
"license": "MIT",
507
-
"optional": true,
508
-
"os": [
509
-
"openbsd"
510
-
],
511
-
"engines": {
512
-
"node": ">=18"
513
-
}
514
-
},
515
-
"node_modules/@esbuild/openbsd-x64": {
516
-
"version": "0.25.3",
517
-
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
518
-
"integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
519
-
"cpu": [
520
-
"x64"
521
-
],
522
-
"dev": true,
523
-
"license": "MIT",
524
-
"optional": true,
525
-
"os": [
526
-
"openbsd"
527
-
],
528
-
"engines": {
529
-
"node": ">=18"
530
-
}
531
-
},
532
-
"node_modules/@esbuild/sunos-x64": {
533
-
"version": "0.25.3",
534
-
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
535
-
"integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
536
-
"cpu": [
537
-
"x64"
538
-
],
539
-
"dev": true,
540
-
"license": "MIT",
541
-
"optional": true,
542
-
"os": [
543
-
"sunos"
544
-
],
545
-
"engines": {
546
-
"node": ">=18"
547
-
}
548
-
},
549
-
"node_modules/@esbuild/win32-arm64": {
550
-
"version": "0.25.3",
551
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
552
-
"integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
553
-
"cpu": [
554
-
"arm64"
555
-
],
556
-
"dev": true,
557
-
"license": "MIT",
558
-
"optional": true,
559
-
"os": [
560
-
"win32"
561
-
],
562
-
"engines": {
563
-
"node": ">=18"
564
-
}
565
-
},
566
-
"node_modules/@esbuild/win32-ia32": {
567
-
"version": "0.25.3",
568
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
569
-
"integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
570
-
"cpu": [
571
-
"ia32"
572
-
],
573
-
"dev": true,
574
-
"license": "MIT",
575
-
"optional": true,
576
-
"os": [
577
-
"win32"
578
-
],
579
-
"engines": {
580
-
"node": ">=18"
581
-
}
582
-
},
583
-
"node_modules/@esbuild/win32-x64": {
584
-
"version": "0.25.3",
585
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
586
-
"integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
587
-
"cpu": [
588
-
"x64"
589
-
],
590
-
"dev": true,
591
-
"license": "MIT",
592
-
"optional": true,
593
-
"os": [
594
-
"win32"
595
-
],
596
-
"engines": {
597
-
"node": ">=18"
598
-
}
599
-
},
600
-
"node_modules/@fastify/busboy": {
601
-
"version": "2.1.1",
602
-
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
603
-
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
604
-
"dev": true,
605
-
"license": "MIT",
606
-
"engines": {
607
-
"node": ">=14"
608
-
}
609
-
},
610
-
"node_modules/@img/sharp-darwin-arm64": {
611
-
"version": "0.33.5",
612
-
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
613
-
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
614
-
"cpu": [
615
-
"arm64"
616
-
],
617
-
"dev": true,
618
-
"license": "Apache-2.0",
619
-
"optional": true,
620
-
"os": [
621
-
"darwin"
622
-
],
623
-
"engines": {
624
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
625
-
},
626
-
"funding": {
627
-
"url": "https://opencollective.com/libvips"
628
-
},
629
-
"optionalDependencies": {
630
-
"@img/sharp-libvips-darwin-arm64": "1.0.4"
631
-
}
632
-
},
633
-
"node_modules/@img/sharp-darwin-x64": {
634
-
"version": "0.33.5",
635
-
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
636
-
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
637
-
"cpu": [
638
-
"x64"
639
-
],
640
-
"dev": true,
641
-
"license": "Apache-2.0",
642
-
"optional": true,
643
-
"os": [
644
-
"darwin"
645
-
],
646
-
"engines": {
647
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
648
-
},
649
-
"funding": {
650
-
"url": "https://opencollective.com/libvips"
651
-
},
652
-
"optionalDependencies": {
653
-
"@img/sharp-libvips-darwin-x64": "1.0.4"
654
-
}
655
-
},
656
-
"node_modules/@img/sharp-libvips-darwin-arm64": {
657
-
"version": "1.0.4",
658
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
659
-
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
660
-
"cpu": [
661
-
"arm64"
662
-
],
663
-
"dev": true,
664
-
"license": "LGPL-3.0-or-later",
665
-
"optional": true,
666
-
"os": [
667
-
"darwin"
668
-
],
669
-
"funding": {
670
-
"url": "https://opencollective.com/libvips"
671
-
}
672
-
},
673
-
"node_modules/@img/sharp-libvips-darwin-x64": {
674
-
"version": "1.0.4",
675
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
676
-
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
677
-
"cpu": [
678
-
"x64"
679
-
],
680
-
"dev": true,
681
-
"license": "LGPL-3.0-or-later",
682
-
"optional": true,
683
-
"os": [
684
-
"darwin"
685
-
],
686
-
"funding": {
687
-
"url": "https://opencollective.com/libvips"
688
-
}
689
-
},
690
-
"node_modules/@img/sharp-libvips-linux-arm": {
691
-
"version": "1.0.5",
692
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
693
-
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
694
-
"cpu": [
695
-
"arm"
696
-
],
697
-
"dev": true,
698
-
"license": "LGPL-3.0-or-later",
699
-
"optional": true,
700
-
"os": [
701
-
"linux"
702
-
],
703
-
"funding": {
704
-
"url": "https://opencollective.com/libvips"
705
-
}
706
-
},
707
-
"node_modules/@img/sharp-libvips-linux-arm64": {
708
-
"version": "1.0.4",
709
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
710
-
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
711
-
"cpu": [
712
-
"arm64"
713
-
],
714
-
"dev": true,
715
-
"license": "LGPL-3.0-or-later",
716
-
"optional": true,
717
-
"os": [
718
-
"linux"
719
-
],
720
-
"funding": {
721
-
"url": "https://opencollective.com/libvips"
722
-
}
723
-
},
724
-
"node_modules/@img/sharp-libvips-linux-s390x": {
725
-
"version": "1.0.4",
726
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
727
-
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
728
-
"cpu": [
729
-
"s390x"
730
-
],
731
-
"dev": true,
732
-
"license": "LGPL-3.0-or-later",
733
-
"optional": true,
734
-
"os": [
735
-
"linux"
736
-
],
737
-
"funding": {
738
-
"url": "https://opencollective.com/libvips"
739
-
}
740
-
},
741
-
"node_modules/@img/sharp-libvips-linux-x64": {
742
-
"version": "1.0.4",
743
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
744
-
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
745
-
"cpu": [
746
-
"x64"
747
-
],
748
-
"dev": true,
749
-
"license": "LGPL-3.0-or-later",
750
-
"optional": true,
751
-
"os": [
752
-
"linux"
753
-
],
754
-
"funding": {
755
-
"url": "https://opencollective.com/libvips"
756
-
}
757
-
},
758
-
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
759
-
"version": "1.0.4",
760
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
761
-
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
762
-
"cpu": [
763
-
"arm64"
764
-
],
765
-
"dev": true,
766
-
"license": "LGPL-3.0-or-later",
767
-
"optional": true,
768
-
"os": [
769
-
"linux"
770
-
],
771
-
"funding": {
772
-
"url": "https://opencollective.com/libvips"
773
-
}
774
-
},
775
-
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
776
-
"version": "1.0.4",
777
-
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
778
-
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
779
-
"cpu": [
780
-
"x64"
781
-
],
782
-
"dev": true,
783
-
"license": "LGPL-3.0-or-later",
784
-
"optional": true,
785
-
"os": [
786
-
"linux"
787
-
],
788
-
"funding": {
789
-
"url": "https://opencollective.com/libvips"
790
-
}
791
-
},
792
-
"node_modules/@img/sharp-linux-arm": {
793
-
"version": "0.33.5",
794
-
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
795
-
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
796
-
"cpu": [
797
-
"arm"
798
-
],
799
-
"dev": true,
800
-
"license": "Apache-2.0",
801
-
"optional": true,
802
-
"os": [
803
-
"linux"
804
-
],
805
-
"engines": {
806
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
807
-
},
808
-
"funding": {
809
-
"url": "https://opencollective.com/libvips"
810
-
},
811
-
"optionalDependencies": {
812
-
"@img/sharp-libvips-linux-arm": "1.0.5"
813
-
}
814
-
},
815
-
"node_modules/@img/sharp-linux-arm64": {
816
-
"version": "0.33.5",
817
-
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
818
-
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
819
-
"cpu": [
820
-
"arm64"
821
-
],
822
-
"dev": true,
823
-
"license": "Apache-2.0",
824
-
"optional": true,
825
-
"os": [
826
-
"linux"
827
-
],
828
-
"engines": {
829
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
830
-
},
831
-
"funding": {
832
-
"url": "https://opencollective.com/libvips"
833
-
},
834
-
"optionalDependencies": {
835
-
"@img/sharp-libvips-linux-arm64": "1.0.4"
836
-
}
837
-
},
838
-
"node_modules/@img/sharp-linux-s390x": {
839
-
"version": "0.33.5",
840
-
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
841
-
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
842
-
"cpu": [
843
-
"s390x"
844
-
],
845
-
"dev": true,
846
-
"license": "Apache-2.0",
847
-
"optional": true,
848
-
"os": [
849
-
"linux"
850
-
],
851
-
"engines": {
852
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
853
-
},
854
-
"funding": {
855
-
"url": "https://opencollective.com/libvips"
856
-
},
857
-
"optionalDependencies": {
858
-
"@img/sharp-libvips-linux-s390x": "1.0.4"
859
-
}
860
-
},
861
-
"node_modules/@img/sharp-linux-x64": {
862
-
"version": "0.33.5",
863
-
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
864
-
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
865
-
"cpu": [
866
-
"x64"
867
-
],
868
-
"dev": true,
869
-
"license": "Apache-2.0",
870
-
"optional": true,
871
-
"os": [
872
-
"linux"
873
-
],
874
-
"engines": {
875
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
876
-
},
877
-
"funding": {
878
-
"url": "https://opencollective.com/libvips"
879
-
},
880
-
"optionalDependencies": {
881
-
"@img/sharp-libvips-linux-x64": "1.0.4"
882
-
}
883
-
},
884
-
"node_modules/@img/sharp-linuxmusl-arm64": {
885
-
"version": "0.33.5",
886
-
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
887
-
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
888
-
"cpu": [
889
-
"arm64"
890
-
],
891
-
"dev": true,
892
-
"license": "Apache-2.0",
893
-
"optional": true,
894
-
"os": [
895
-
"linux"
896
-
],
897
-
"engines": {
898
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
899
-
},
900
-
"funding": {
901
-
"url": "https://opencollective.com/libvips"
902
-
},
903
-
"optionalDependencies": {
904
-
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
905
-
}
906
-
},
907
-
"node_modules/@img/sharp-linuxmusl-x64": {
908
-
"version": "0.33.5",
909
-
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
910
-
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
911
-
"cpu": [
912
-
"x64"
913
-
],
914
-
"dev": true,
915
-
"license": "Apache-2.0",
916
-
"optional": true,
917
-
"os": [
918
-
"linux"
919
-
],
920
-
"engines": {
921
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
922
-
},
923
-
"funding": {
924
-
"url": "https://opencollective.com/libvips"
925
-
},
926
-
"optionalDependencies": {
927
-
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
928
-
}
929
-
},
930
-
"node_modules/@img/sharp-wasm32": {
931
-
"version": "0.33.5",
932
-
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
933
-
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
934
-
"cpu": [
935
-
"wasm32"
936
-
],
937
-
"dev": true,
938
-
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
939
-
"optional": true,
940
-
"dependencies": {
941
-
"@emnapi/runtime": "^1.2.0"
942
-
},
943
-
"engines": {
944
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
945
-
},
946
-
"funding": {
947
-
"url": "https://opencollective.com/libvips"
948
-
}
949
-
},
950
-
"node_modules/@img/sharp-win32-ia32": {
951
-
"version": "0.33.5",
952
-
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
953
-
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
954
-
"cpu": [
955
-
"ia32"
956
-
],
957
-
"dev": true,
958
-
"license": "Apache-2.0 AND LGPL-3.0-or-later",
959
-
"optional": true,
960
-
"os": [
961
-
"win32"
962
-
],
963
-
"engines": {
964
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
965
-
},
966
-
"funding": {
967
-
"url": "https://opencollective.com/libvips"
968
-
}
969
-
},
970
-
"node_modules/@img/sharp-win32-x64": {
971
-
"version": "0.33.5",
972
-
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
973
-
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
974
-
"cpu": [
975
-
"x64"
976
-
],
977
-
"dev": true,
978
-
"license": "Apache-2.0 AND LGPL-3.0-or-later",
979
-
"optional": true,
980
-
"os": [
981
-
"win32"
982
-
],
983
-
"engines": {
984
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
985
-
},
986
-
"funding": {
987
-
"url": "https://opencollective.com/libvips"
988
-
}
989
-
},
990
-
"node_modules/@jridgewell/resolve-uri": {
991
-
"version": "3.1.2",
992
-
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
993
-
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
994
-
"dev": true,
995
-
"license": "MIT",
996
-
"engines": {
997
-
"node": ">=6.0.0"
998
-
}
999
-
},
1000
-
"node_modules/@jridgewell/sourcemap-codec": {
1001
-
"version": "1.5.0",
1002
-
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
1003
-
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
1004
-
"dev": true,
1005
-
"license": "MIT"
1006
-
},
1007
-
"node_modules/@jridgewell/trace-mapping": {
1008
-
"version": "0.3.9",
1009
-
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
1010
-
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
1011
-
"dev": true,
1012
-
"license": "MIT",
1013
-
"dependencies": {
1014
-
"@jridgewell/resolve-uri": "^3.0.3",
1015
-
"@jridgewell/sourcemap-codec": "^1.4.10"
1016
-
}
1017
-
},
1018
-
"node_modules/@rollup/rollup-android-arm-eabi": {
1019
-
"version": "4.40.1",
1020
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
1021
-
"integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==",
1022
-
"cpu": [
1023
-
"arm"
1024
-
],
1025
-
"dev": true,
1026
-
"license": "MIT",
1027
-
"optional": true,
1028
-
"os": [
1029
-
"android"
1030
-
]
1031
-
},
1032
-
"node_modules/@rollup/rollup-android-arm64": {
1033
-
"version": "4.40.1",
1034
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz",
1035
-
"integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==",
1036
-
"cpu": [
1037
-
"arm64"
1038
-
],
1039
-
"dev": true,
1040
-
"license": "MIT",
1041
-
"optional": true,
1042
-
"os": [
1043
-
"android"
1044
-
]
1045
-
},
1046
-
"node_modules/@rollup/rollup-darwin-arm64": {
1047
-
"version": "4.40.1",
1048
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz",
1049
-
"integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==",
1050
-
"cpu": [
1051
-
"arm64"
1052
-
],
1053
-
"dev": true,
1054
-
"license": "MIT",
1055
-
"optional": true,
1056
-
"os": [
1057
-
"darwin"
1058
-
]
1059
-
},
1060
-
"node_modules/@rollup/rollup-darwin-x64": {
1061
-
"version": "4.40.1",
1062
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz",
1063
-
"integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==",
1064
-
"cpu": [
1065
-
"x64"
1066
-
],
1067
-
"dev": true,
1068
-
"license": "MIT",
1069
-
"optional": true,
1070
-
"os": [
1071
-
"darwin"
1072
-
]
1073
-
},
1074
-
"node_modules/@rollup/rollup-freebsd-arm64": {
1075
-
"version": "4.40.1",
1076
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz",
1077
-
"integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==",
1078
-
"cpu": [
1079
-
"arm64"
1080
-
],
1081
-
"dev": true,
1082
-
"license": "MIT",
1083
-
"optional": true,
1084
-
"os": [
1085
-
"freebsd"
1086
-
]
1087
-
},
1088
-
"node_modules/@rollup/rollup-freebsd-x64": {
1089
-
"version": "4.40.1",
1090
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz",
1091
-
"integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==",
1092
-
"cpu": [
1093
-
"x64"
1094
-
],
1095
-
"dev": true,
1096
-
"license": "MIT",
1097
-
"optional": true,
1098
-
"os": [
1099
-
"freebsd"
1100
-
]
1101
-
},
1102
-
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1103
-
"version": "4.40.1",
1104
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz",
1105
-
"integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==",
1106
-
"cpu": [
1107
-
"arm"
1108
-
],
1109
-
"dev": true,
1110
-
"license": "MIT",
1111
-
"optional": true,
1112
-
"os": [
1113
-
"linux"
1114
-
]
1115
-
},
1116
-
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
1117
-
"version": "4.40.1",
1118
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz",
1119
-
"integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==",
1120
-
"cpu": [
1121
-
"arm"
1122
-
],
1123
-
"dev": true,
1124
-
"license": "MIT",
1125
-
"optional": true,
1126
-
"os": [
1127
-
"linux"
1128
-
]
1129
-
},
1130
-
"node_modules/@rollup/rollup-linux-arm64-gnu": {
1131
-
"version": "4.40.1",
1132
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz",
1133
-
"integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==",
1134
-
"cpu": [
1135
-
"arm64"
1136
-
],
1137
-
"dev": true,
1138
-
"license": "MIT",
1139
-
"optional": true,
1140
-
"os": [
1141
-
"linux"
1142
-
]
1143
-
},
1144
-
"node_modules/@rollup/rollup-linux-arm64-musl": {
1145
-
"version": "4.40.1",
1146
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz",
1147
-
"integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==",
1148
-
"cpu": [
1149
-
"arm64"
1150
-
],
1151
-
"dev": true,
1152
-
"license": "MIT",
1153
-
"optional": true,
1154
-
"os": [
1155
-
"linux"
1156
-
]
1157
-
},
1158
-
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
1159
-
"version": "4.40.1",
1160
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz",
1161
-
"integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==",
1162
-
"cpu": [
1163
-
"loong64"
1164
-
],
1165
-
"dev": true,
1166
-
"license": "MIT",
1167
-
"optional": true,
1168
-
"os": [
1169
-
"linux"
1170
-
]
1171
-
},
1172
-
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
1173
-
"version": "4.40.1",
1174
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz",
1175
-
"integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==",
1176
-
"cpu": [
1177
-
"ppc64"
1178
-
],
1179
-
"dev": true,
1180
-
"license": "MIT",
1181
-
"optional": true,
1182
-
"os": [
1183
-
"linux"
1184
-
]
1185
-
},
1186
-
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
1187
-
"version": "4.40.1",
1188
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz",
1189
-
"integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==",
1190
-
"cpu": [
1191
-
"riscv64"
1192
-
],
1193
-
"dev": true,
1194
-
"license": "MIT",
1195
-
"optional": true,
1196
-
"os": [
1197
-
"linux"
1198
-
]
1199
-
},
1200
-
"node_modules/@rollup/rollup-linux-riscv64-musl": {
1201
-
"version": "4.40.1",
1202
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz",
1203
-
"integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==",
1204
-
"cpu": [
1205
-
"riscv64"
1206
-
],
1207
-
"dev": true,
1208
-
"license": "MIT",
1209
-
"optional": true,
1210
-
"os": [
1211
-
"linux"
1212
-
]
1213
-
},
1214
-
"node_modules/@rollup/rollup-linux-s390x-gnu": {
1215
-
"version": "4.40.1",
1216
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz",
1217
-
"integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==",
1218
-
"cpu": [
1219
-
"s390x"
1220
-
],
1221
-
"dev": true,
1222
-
"license": "MIT",
1223
-
"optional": true,
1224
-
"os": [
1225
-
"linux"
1226
-
]
1227
-
},
1228
-
"node_modules/@rollup/rollup-linux-x64-gnu": {
1229
-
"version": "4.40.1",
1230
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz",
1231
-
"integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==",
1232
-
"cpu": [
1233
-
"x64"
1234
-
],
1235
-
"dev": true,
1236
-
"license": "MIT",
1237
-
"optional": true,
1238
-
"os": [
1239
-
"linux"
1240
-
]
1241
-
},
1242
-
"node_modules/@rollup/rollup-linux-x64-musl": {
1243
-
"version": "4.40.1",
1244
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
1245
-
"integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
1246
-
"cpu": [
1247
-
"x64"
1248
-
],
1249
-
"dev": true,
1250
-
"license": "MIT",
1251
-
"optional": true,
1252
-
"os": [
1253
-
"linux"
1254
-
]
1255
-
},
1256
-
"node_modules/@rollup/rollup-win32-arm64-msvc": {
1257
-
"version": "4.40.1",
1258
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
1259
-
"integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
1260
-
"cpu": [
1261
-
"arm64"
1262
-
],
1263
-
"dev": true,
1264
-
"license": "MIT",
1265
-
"optional": true,
1266
-
"os": [
1267
-
"win32"
1268
-
]
1269
-
},
1270
-
"node_modules/@rollup/rollup-win32-ia32-msvc": {
1271
-
"version": "4.40.1",
1272
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
1273
-
"integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
1274
-
"cpu": [
1275
-
"ia32"
1276
-
],
1277
-
"dev": true,
1278
-
"license": "MIT",
1279
-
"optional": true,
1280
-
"os": [
1281
-
"win32"
1282
-
]
1283
-
},
1284
-
"node_modules/@rollup/rollup-win32-x64-msvc": {
1285
-
"version": "4.40.1",
1286
-
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
1287
-
"integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
1288
-
"cpu": [
1289
-
"x64"
1290
-
],
1291
-
"dev": true,
1292
-
"license": "MIT",
1293
-
"optional": true,
1294
-
"os": [
1295
-
"win32"
1296
-
]
1297
-
},
1298
-
"node_modules/@types/estree": {
1299
-
"version": "1.0.7",
1300
-
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
1301
-
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
1302
-
"dev": true,
1303
-
"license": "MIT"
1304
-
},
1305
-
"node_modules/@vitest/expect": {
1306
-
"version": "3.0.9",
1307
-
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz",
1308
-
"integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==",
1309
-
"dev": true,
1310
-
"license": "MIT",
1311
-
"dependencies": {
1312
-
"@vitest/spy": "3.0.9",
1313
-
"@vitest/utils": "3.0.9",
1314
-
"chai": "^5.2.0",
1315
-
"tinyrainbow": "^2.0.0"
1316
-
},
1317
-
"funding": {
1318
-
"url": "https://opencollective.com/vitest"
1319
-
}
1320
-
},
1321
-
"node_modules/@vitest/mocker": {
1322
-
"version": "3.0.9",
1323
-
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz",
1324
-
"integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==",
1325
-
"dev": true,
1326
-
"license": "MIT",
1327
-
"dependencies": {
1328
-
"@vitest/spy": "3.0.9",
1329
-
"estree-walker": "^3.0.3",
1330
-
"magic-string": "^0.30.17"
1331
-
},
1332
-
"funding": {
1333
-
"url": "https://opencollective.com/vitest"
1334
-
},
1335
-
"peerDependencies": {
1336
-
"msw": "^2.4.9",
1337
-
"vite": "^5.0.0 || ^6.0.0"
1338
-
},
1339
-
"peerDependenciesMeta": {
1340
-
"msw": {
1341
-
"optional": true
1342
-
},
1343
-
"vite": {
1344
-
"optional": true
1345
-
}
1346
-
}
1347
-
},
1348
-
"node_modules/@vitest/pretty-format": {
1349
-
"version": "3.1.2",
1350
-
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
1351
-
"integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
1352
-
"dev": true,
1353
-
"license": "MIT",
1354
-
"dependencies": {
1355
-
"tinyrainbow": "^2.0.0"
1356
-
},
1357
-
"funding": {
1358
-
"url": "https://opencollective.com/vitest"
1359
-
}
1360
-
},
1361
-
"node_modules/@vitest/runner": {
1362
-
"version": "3.0.9",
1363
-
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz",
1364
-
"integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==",
1365
-
"dev": true,
1366
-
"license": "MIT",
1367
-
"dependencies": {
1368
-
"@vitest/utils": "3.0.9",
1369
-
"pathe": "^2.0.3"
1370
-
},
1371
-
"funding": {
1372
-
"url": "https://opencollective.com/vitest"
1373
-
}
1374
-
},
1375
-
"node_modules/@vitest/snapshot": {
1376
-
"version": "3.0.9",
1377
-
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz",
1378
-
"integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==",
1379
-
"dev": true,
1380
-
"license": "MIT",
1381
-
"dependencies": {
1382
-
"@vitest/pretty-format": "3.0.9",
1383
-
"magic-string": "^0.30.17",
1384
-
"pathe": "^2.0.3"
1385
-
},
1386
-
"funding": {
1387
-
"url": "https://opencollective.com/vitest"
1388
-
}
1389
-
},
1390
-
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
1391
-
"version": "3.0.9",
1392
-
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz",
1393
-
"integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==",
1394
-
"dev": true,
1395
-
"license": "MIT",
1396
-
"dependencies": {
1397
-
"tinyrainbow": "^2.0.0"
1398
-
},
1399
-
"funding": {
1400
-
"url": "https://opencollective.com/vitest"
1401
-
}
1402
-
},
1403
-
"node_modules/@vitest/spy": {
1404
-
"version": "3.0.9",
1405
-
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz",
1406
-
"integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==",
1407
-
"dev": true,
1408
-
"license": "MIT",
1409
-
"dependencies": {
1410
-
"tinyspy": "^3.0.2"
1411
-
},
1412
-
"funding": {
1413
-
"url": "https://opencollective.com/vitest"
1414
-
}
1415
-
},
1416
-
"node_modules/@vitest/utils": {
1417
-
"version": "3.0.9",
1418
-
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz",
1419
-
"integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==",
1420
-
"dev": true,
1421
-
"license": "MIT",
1422
-
"dependencies": {
1423
-
"@vitest/pretty-format": "3.0.9",
1424
-
"loupe": "^3.1.3",
1425
-
"tinyrainbow": "^2.0.0"
1426
-
},
1427
-
"funding": {
1428
-
"url": "https://opencollective.com/vitest"
1429
-
}
1430
-
},
1431
-
"node_modules/@vitest/utils/node_modules/@vitest/pretty-format": {
1432
-
"version": "3.0.9",
1433
-
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz",
1434
-
"integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==",
1435
-
"dev": true,
1436
-
"license": "MIT",
1437
-
"dependencies": {
1438
-
"tinyrainbow": "^2.0.0"
1439
-
},
1440
-
"funding": {
1441
-
"url": "https://opencollective.com/vitest"
1442
-
}
1443
-
},
1444
-
"node_modules/acorn": {
1445
-
"version": "8.14.0",
1446
-
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
1447
-
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
1448
-
"dev": true,
1449
-
"license": "MIT",
1450
-
"bin": {
1451
-
"acorn": "bin/acorn"
1452
-
},
1453
-
"engines": {
1454
-
"node": ">=0.4.0"
1455
-
}
1456
-
},
1457
-
"node_modules/acorn-walk": {
1458
-
"version": "8.3.2",
1459
-
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
1460
-
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
1461
-
"dev": true,
1462
-
"license": "MIT",
1463
-
"engines": {
1464
-
"node": ">=0.4.0"
1465
-
}
1466
-
},
1467
-
"node_modules/as-table": {
1468
-
"version": "1.0.55",
1469
-
"resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz",
1470
-
"integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==",
1471
-
"dev": true,
1472
-
"license": "MIT",
1473
-
"dependencies": {
1474
-
"printable-characters": "^1.0.42"
1475
-
}
1476
-
},
1477
-
"node_modules/assertion-error": {
1478
-
"version": "2.0.1",
1479
-
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
1480
-
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1481
-
"dev": true,
1482
-
"license": "MIT",
1483
-
"engines": {
1484
-
"node": ">=12"
1485
-
}
1486
-
},
1487
-
"node_modules/birpc": {
1488
-
"version": "0.2.14",
1489
-
"resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz",
1490
-
"integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==",
1491
-
"dev": true,
1492
-
"license": "MIT",
1493
-
"funding": {
1494
-
"url": "https://github.com/sponsors/antfu"
1495
-
}
1496
-
},
1497
-
"node_modules/blake3-wasm": {
1498
-
"version": "2.1.5",
1499
-
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
1500
-
"integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
1501
-
"dev": true,
1502
-
"license": "MIT"
1503
-
},
1504
-
"node_modules/cac": {
1505
-
"version": "6.7.14",
1506
-
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
1507
-
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
1508
-
"dev": true,
1509
-
"license": "MIT",
1510
-
"engines": {
1511
-
"node": ">=8"
1512
-
}
1513
-
},
1514
-
"node_modules/chai": {
1515
-
"version": "5.2.0",
1516
-
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
1517
-
"integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
1518
-
"dev": true,
1519
-
"license": "MIT",
1520
-
"dependencies": {
1521
-
"assertion-error": "^2.0.1",
1522
-
"check-error": "^2.1.1",
1523
-
"deep-eql": "^5.0.1",
1524
-
"loupe": "^3.1.0",
1525
-
"pathval": "^2.0.0"
1526
-
},
1527
-
"engines": {
1528
-
"node": ">=12"
1529
-
}
1530
-
},
1531
-
"node_modules/check-error": {
1532
-
"version": "2.1.1",
1533
-
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
1534
-
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
1535
-
"dev": true,
1536
-
"license": "MIT",
1537
-
"engines": {
1538
-
"node": ">= 16"
1539
-
}
1540
-
},
1541
-
"node_modules/cjs-module-lexer": {
1542
-
"version": "1.4.3",
1543
-
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
1544
-
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
1545
-
"dev": true,
1546
-
"license": "MIT"
1547
-
},
1548
-
"node_modules/color": {
1549
-
"version": "4.2.3",
1550
-
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
1551
-
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
1552
-
"dev": true,
1553
-
"license": "MIT",
1554
-
"optional": true,
1555
-
"dependencies": {
1556
-
"color-convert": "^2.0.1",
1557
-
"color-string": "^1.9.0"
1558
-
},
1559
-
"engines": {
1560
-
"node": ">=12.5.0"
1561
-
}
1562
-
},
1563
-
"node_modules/color-convert": {
1564
-
"version": "2.0.1",
1565
-
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1566
-
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1567
-
"dev": true,
1568
-
"license": "MIT",
1569
-
"optional": true,
1570
-
"dependencies": {
1571
-
"color-name": "~1.1.4"
1572
-
},
1573
-
"engines": {
1574
-
"node": ">=7.0.0"
1575
-
}
1576
-
},
1577
-
"node_modules/color-name": {
1578
-
"version": "1.1.4",
1579
-
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1580
-
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1581
-
"dev": true,
1582
-
"license": "MIT",
1583
-
"optional": true
1584
-
},
1585
-
"node_modules/color-string": {
1586
-
"version": "1.9.1",
1587
-
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
1588
-
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
1589
-
"dev": true,
1590
-
"license": "MIT",
1591
-
"optional": true,
1592
-
"dependencies": {
1593
-
"color-name": "^1.0.0",
1594
-
"simple-swizzle": "^0.2.2"
1595
-
}
1596
-
},
1597
-
"node_modules/cookie": {
1598
-
"version": "0.7.2",
1599
-
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
1600
-
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
1601
-
"dev": true,
1602
-
"license": "MIT",
1603
-
"engines": {
1604
-
"node": ">= 0.6"
1605
-
}
1606
-
},
1607
-
"node_modules/data-uri-to-buffer": {
1608
-
"version": "2.0.2",
1609
-
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
1610
-
"integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
1611
-
"dev": true,
1612
-
"license": "MIT"
1613
-
},
1614
-
"node_modules/debug": {
1615
-
"version": "4.4.0",
1616
-
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
1617
-
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
1618
-
"dev": true,
1619
-
"license": "MIT",
1620
-
"dependencies": {
1621
-
"ms": "^2.1.3"
1622
-
},
1623
-
"engines": {
1624
-
"node": ">=6.0"
1625
-
},
1626
-
"peerDependenciesMeta": {
1627
-
"supports-color": {
1628
-
"optional": true
1629
-
}
1630
-
}
1631
-
},
1632
-
"node_modules/deep-eql": {
1633
-
"version": "5.0.2",
1634
-
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
1635
-
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
1636
-
"dev": true,
1637
-
"license": "MIT",
1638
-
"engines": {
1639
-
"node": ">=6"
1640
-
}
1641
-
},
1642
-
"node_modules/defu": {
1643
-
"version": "6.1.4",
1644
-
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
1645
-
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
1646
-
"dev": true,
1647
-
"license": "MIT"
1648
-
},
1649
-
"node_modules/detect-libc": {
1650
-
"version": "2.0.4",
1651
-
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
1652
-
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
1653
-
"dev": true,
1654
-
"license": "Apache-2.0",
1655
-
"optional": true,
1656
-
"engines": {
1657
-
"node": ">=8"
1658
-
}
1659
-
},
1660
-
"node_modules/devalue": {
1661
-
"version": "4.3.3",
1662
-
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz",
1663
-
"integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==",
1664
-
"dev": true,
1665
-
"license": "MIT"
1666
-
},
1667
-
"node_modules/es-module-lexer": {
1668
-
"version": "1.7.0",
1669
-
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
1670
-
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
1671
-
"dev": true,
1672
-
"license": "MIT"
1673
-
},
1674
-
"node_modules/esbuild": {
1675
-
"version": "0.25.3",
1676
-
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
1677
-
"integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
1678
-
"dev": true,
1679
-
"hasInstallScript": true,
1680
-
"license": "MIT",
1681
-
"bin": {
1682
-
"esbuild": "bin/esbuild"
1683
-
},
1684
-
"engines": {
1685
-
"node": ">=18"
1686
-
},
1687
-
"optionalDependencies": {
1688
-
"@esbuild/aix-ppc64": "0.25.3",
1689
-
"@esbuild/android-arm": "0.25.3",
1690
-
"@esbuild/android-arm64": "0.25.3",
1691
-
"@esbuild/android-x64": "0.25.3",
1692
-
"@esbuild/darwin-arm64": "0.25.3",
1693
-
"@esbuild/darwin-x64": "0.25.3",
1694
-
"@esbuild/freebsd-arm64": "0.25.3",
1695
-
"@esbuild/freebsd-x64": "0.25.3",
1696
-
"@esbuild/linux-arm": "0.25.3",
1697
-
"@esbuild/linux-arm64": "0.25.3",
1698
-
"@esbuild/linux-ia32": "0.25.3",
1699
-
"@esbuild/linux-loong64": "0.25.3",
1700
-
"@esbuild/linux-mips64el": "0.25.3",
1701
-
"@esbuild/linux-ppc64": "0.25.3",
1702
-
"@esbuild/linux-riscv64": "0.25.3",
1703
-
"@esbuild/linux-s390x": "0.25.3",
1704
-
"@esbuild/linux-x64": "0.25.3",
1705
-
"@esbuild/netbsd-arm64": "0.25.3",
1706
-
"@esbuild/netbsd-x64": "0.25.3",
1707
-
"@esbuild/openbsd-arm64": "0.25.3",
1708
-
"@esbuild/openbsd-x64": "0.25.3",
1709
-
"@esbuild/sunos-x64": "0.25.3",
1710
-
"@esbuild/win32-arm64": "0.25.3",
1711
-
"@esbuild/win32-ia32": "0.25.3",
1712
-
"@esbuild/win32-x64": "0.25.3"
1713
-
}
1714
-
},
1715
-
"node_modules/estree-walker": {
1716
-
"version": "3.0.3",
1717
-
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
1718
-
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
1719
-
"dev": true,
1720
-
"license": "MIT",
1721
-
"dependencies": {
1722
-
"@types/estree": "^1.0.0"
1723
-
}
1724
-
},
1725
-
"node_modules/exit-hook": {
1726
-
"version": "2.2.1",
1727
-
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
1728
-
"integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
1729
-
"dev": true,
1730
-
"license": "MIT",
1731
-
"engines": {
1732
-
"node": ">=6"
1733
-
},
1734
-
"funding": {
1735
-
"url": "https://github.com/sponsors/sindresorhus"
1736
-
}
1737
-
},
1738
-
"node_modules/expect-type": {
1739
-
"version": "1.2.1",
1740
-
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
1741
-
"integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==",
1742
-
"dev": true,
1743
-
"license": "Apache-2.0",
1744
-
"engines": {
1745
-
"node": ">=12.0.0"
1746
-
}
1747
-
},
1748
-
"node_modules/exsolve": {
1749
-
"version": "1.0.5",
1750
-
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz",
1751
-
"integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==",
1752
-
"dev": true,
1753
-
"license": "MIT"
1754
-
},
1755
-
"node_modules/fdir": {
1756
-
"version": "6.4.4",
1757
-
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
1758
-
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
1759
-
"dev": true,
1760
-
"license": "MIT",
1761
-
"peerDependencies": {
1762
-
"picomatch": "^3 || ^4"
1763
-
},
1764
-
"peerDependenciesMeta": {
1765
-
"picomatch": {
1766
-
"optional": true
1767
-
}
1768
-
}
1769
-
},
1770
-
"node_modules/fsevents": {
1771
-
"version": "2.3.3",
1772
-
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1773
-
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1774
-
"dev": true,
1775
-
"hasInstallScript": true,
1776
-
"license": "MIT",
1777
-
"optional": true,
1778
-
"os": [
1779
-
"darwin"
1780
-
],
1781
-
"engines": {
1782
-
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1783
-
}
1784
-
},
1785
-
"node_modules/get-source": {
1786
-
"version": "2.0.12",
1787
-
"resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
1788
-
"integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==",
1789
-
"dev": true,
1790
-
"license": "Unlicense",
1791
-
"dependencies": {
1792
-
"data-uri-to-buffer": "^2.0.0",
1793
-
"source-map": "^0.6.1"
1794
-
}
1795
-
},
1796
-
"node_modules/glob-to-regexp": {
1797
-
"version": "0.4.1",
1798
-
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
1799
-
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
1800
-
"dev": true,
1801
-
"license": "BSD-2-Clause"
1802
-
},
1803
-
"node_modules/is-arrayish": {
1804
-
"version": "0.3.2",
1805
-
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
1806
-
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
1807
-
"dev": true,
1808
-
"license": "MIT",
1809
-
"optional": true
1810
-
},
1811
-
"node_modules/loupe": {
1812
-
"version": "3.1.3",
1813
-
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
1814
-
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
1815
-
"dev": true,
1816
-
"license": "MIT"
1817
-
},
1818
-
"node_modules/magic-string": {
1819
-
"version": "0.30.17",
1820
-
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
1821
-
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
1822
-
"dev": true,
1823
-
"license": "MIT",
1824
-
"dependencies": {
1825
-
"@jridgewell/sourcemap-codec": "^1.5.0"
1826
-
}
1827
-
},
1828
-
"node_modules/mime": {
1829
-
"version": "3.0.0",
1830
-
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
1831
-
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
1832
-
"dev": true,
1833
-
"license": "MIT",
1834
-
"bin": {
1835
-
"mime": "cli.js"
1836
-
},
1837
-
"engines": {
1838
-
"node": ">=10.0.0"
1839
-
}
1840
-
},
1841
-
"node_modules/miniflare": {
1842
-
"version": "4.20250428.1",
1843
-
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250428.1.tgz",
1844
-
"integrity": "sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==",
1845
-
"dev": true,
1846
-
"license": "MIT",
1847
-
"dependencies": {
1848
-
"@cspotcode/source-map-support": "0.8.1",
1849
-
"acorn": "8.14.0",
1850
-
"acorn-walk": "8.3.2",
1851
-
"exit-hook": "2.2.1",
1852
-
"glob-to-regexp": "0.4.1",
1853
-
"stoppable": "1.1.0",
1854
-
"undici": "^5.28.5",
1855
-
"workerd": "1.20250428.0",
1856
-
"ws": "8.18.0",
1857
-
"youch": "3.3.4",
1858
-
"zod": "3.22.3"
1859
-
},
1860
-
"bin": {
1861
-
"miniflare": "bootstrap.js"
1862
-
},
1863
-
"engines": {
1864
-
"node": ">=18.0.0"
1865
-
}
1866
-
},
1867
-
"node_modules/miniflare/node_modules/zod": {
1868
-
"version": "3.22.3",
1869
-
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
1870
-
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
1871
-
"dev": true,
1872
-
"license": "MIT",
1873
-
"funding": {
1874
-
"url": "https://github.com/sponsors/colinhacks"
1875
-
}
1876
-
},
1877
-
"node_modules/ms": {
1878
-
"version": "2.1.3",
1879
-
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1880
-
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1881
-
"dev": true,
1882
-
"license": "MIT"
1883
-
},
1884
-
"node_modules/mustache": {
1885
-
"version": "4.2.0",
1886
-
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
1887
-
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
1888
-
"dev": true,
1889
-
"license": "MIT",
1890
-
"bin": {
1891
-
"mustache": "bin/mustache"
1892
-
}
1893
-
},
1894
-
"node_modules/nanoid": {
1895
-
"version": "3.3.11",
1896
-
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1897
-
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1898
-
"dev": true,
1899
-
"funding": [
1900
-
{
1901
-
"type": "github",
1902
-
"url": "https://github.com/sponsors/ai"
1903
-
}
1904
-
],
1905
-
"license": "MIT",
1906
-
"bin": {
1907
-
"nanoid": "bin/nanoid.cjs"
1908
-
},
1909
-
"engines": {
1910
-
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1911
-
}
1912
-
},
1913
-
"node_modules/ohash": {
1914
-
"version": "2.0.11",
1915
-
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
1916
-
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
1917
-
"dev": true,
1918
-
"license": "MIT"
1919
-
},
1920
-
"node_modules/path-to-regexp": {
1921
-
"version": "6.3.0",
1922
-
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
1923
-
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
1924
-
"dev": true,
1925
-
"license": "MIT"
1926
-
},
1927
-
"node_modules/pathe": {
1928
-
"version": "2.0.3",
1929
-
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
1930
-
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
1931
-
"dev": true,
1932
-
"license": "MIT"
1933
-
},
1934
-
"node_modules/pathval": {
1935
-
"version": "2.0.0",
1936
-
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
1937
-
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
1938
-
"dev": true,
1939
-
"license": "MIT",
1940
-
"engines": {
1941
-
"node": ">= 14.16"
1942
-
}
1943
-
},
1944
-
"node_modules/picocolors": {
1945
-
"version": "1.1.1",
1946
-
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1947
-
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1948
-
"dev": true,
1949
-
"license": "ISC"
1950
-
},
1951
-
"node_modules/picomatch": {
1952
-
"version": "4.0.2",
1953
-
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
1954
-
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
1955
-
"dev": true,
1956
-
"license": "MIT",
1957
-
"engines": {
1958
-
"node": ">=12"
1959
-
},
1960
-
"funding": {
1961
-
"url": "https://github.com/sponsors/jonschlinkert"
1962
-
}
1963
-
},
1964
-
"node_modules/postcss": {
1965
-
"version": "8.5.3",
1966
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
1967
-
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
1968
-
"dev": true,
1969
-
"funding": [
1970
-
{
1971
-
"type": "opencollective",
1972
-
"url": "https://opencollective.com/postcss/"
1973
-
},
1974
-
{
1975
-
"type": "tidelift",
1976
-
"url": "https://tidelift.com/funding/github/npm/postcss"
1977
-
},
1978
-
{
1979
-
"type": "github",
1980
-
"url": "https://github.com/sponsors/ai"
1981
-
}
1982
-
],
1983
-
"license": "MIT",
1984
-
"dependencies": {
1985
-
"nanoid": "^3.3.8",
1986
-
"picocolors": "^1.1.1",
1987
-
"source-map-js": "^1.2.1"
1988
-
},
1989
-
"engines": {
1990
-
"node": "^10 || ^12 || >=14"
1991
-
}
1992
-
},
1993
-
"node_modules/printable-characters": {
1994
-
"version": "1.0.42",
1995
-
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
1996
-
"integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
1997
-
"dev": true,
1998
-
"license": "Unlicense"
1999
-
},
2000
-
"node_modules/rollup": {
2001
-
"version": "4.40.1",
2002
-
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz",
2003
-
"integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
2004
-
"dev": true,
2005
-
"license": "MIT",
2006
-
"dependencies": {
2007
-
"@types/estree": "1.0.7"
2008
-
},
2009
-
"bin": {
2010
-
"rollup": "dist/bin/rollup"
2011
-
},
2012
-
"engines": {
2013
-
"node": ">=18.0.0",
2014
-
"npm": ">=8.0.0"
2015
-
},
2016
-
"optionalDependencies": {
2017
-
"@rollup/rollup-android-arm-eabi": "4.40.1",
2018
-
"@rollup/rollup-android-arm64": "4.40.1",
2019
-
"@rollup/rollup-darwin-arm64": "4.40.1",
2020
-
"@rollup/rollup-darwin-x64": "4.40.1",
2021
-
"@rollup/rollup-freebsd-arm64": "4.40.1",
2022
-
"@rollup/rollup-freebsd-x64": "4.40.1",
2023
-
"@rollup/rollup-linux-arm-gnueabihf": "4.40.1",
2024
-
"@rollup/rollup-linux-arm-musleabihf": "4.40.1",
2025
-
"@rollup/rollup-linux-arm64-gnu": "4.40.1",
2026
-
"@rollup/rollup-linux-arm64-musl": "4.40.1",
2027
-
"@rollup/rollup-linux-loongarch64-gnu": "4.40.1",
2028
-
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.1",
2029
-
"@rollup/rollup-linux-riscv64-gnu": "4.40.1",
2030
-
"@rollup/rollup-linux-riscv64-musl": "4.40.1",
2031
-
"@rollup/rollup-linux-s390x-gnu": "4.40.1",
2032
-
"@rollup/rollup-linux-x64-gnu": "4.40.1",
2033
-
"@rollup/rollup-linux-x64-musl": "4.40.1",
2034
-
"@rollup/rollup-win32-arm64-msvc": "4.40.1",
2035
-
"@rollup/rollup-win32-ia32-msvc": "4.40.1",
2036
-
"@rollup/rollup-win32-x64-msvc": "4.40.1",
2037
-
"fsevents": "~2.3.2"
2038
-
}
2039
-
},
2040
-
"node_modules/semver": {
2041
-
"version": "7.7.1",
2042
-
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
2043
-
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
2044
-
"dev": true,
2045
-
"license": "ISC",
2046
-
"bin": {
2047
-
"semver": "bin/semver.js"
2048
-
},
2049
-
"engines": {
2050
-
"node": ">=10"
2051
-
}
2052
-
},
2053
-
"node_modules/sharp": {
2054
-
"version": "0.33.5",
2055
-
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
2056
-
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
2057
-
"dev": true,
2058
-
"hasInstallScript": true,
2059
-
"license": "Apache-2.0",
2060
-
"optional": true,
2061
-
"dependencies": {
2062
-
"color": "^4.2.3",
2063
-
"detect-libc": "^2.0.3",
2064
-
"semver": "^7.6.3"
2065
-
},
2066
-
"engines": {
2067
-
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
2068
-
},
2069
-
"funding": {
2070
-
"url": "https://opencollective.com/libvips"
2071
-
},
2072
-
"optionalDependencies": {
2073
-
"@img/sharp-darwin-arm64": "0.33.5",
2074
-
"@img/sharp-darwin-x64": "0.33.5",
2075
-
"@img/sharp-libvips-darwin-arm64": "1.0.4",
2076
-
"@img/sharp-libvips-darwin-x64": "1.0.4",
2077
-
"@img/sharp-libvips-linux-arm": "1.0.5",
2078
-
"@img/sharp-libvips-linux-arm64": "1.0.4",
2079
-
"@img/sharp-libvips-linux-s390x": "1.0.4",
2080
-
"@img/sharp-libvips-linux-x64": "1.0.4",
2081
-
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
2082
-
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
2083
-
"@img/sharp-linux-arm": "0.33.5",
2084
-
"@img/sharp-linux-arm64": "0.33.5",
2085
-
"@img/sharp-linux-s390x": "0.33.5",
2086
-
"@img/sharp-linux-x64": "0.33.5",
2087
-
"@img/sharp-linuxmusl-arm64": "0.33.5",
2088
-
"@img/sharp-linuxmusl-x64": "0.33.5",
2089
-
"@img/sharp-wasm32": "0.33.5",
2090
-
"@img/sharp-win32-ia32": "0.33.5",
2091
-
"@img/sharp-win32-x64": "0.33.5"
2092
-
}
2093
-
},
2094
-
"node_modules/siginfo": {
2095
-
"version": "2.0.0",
2096
-
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
2097
-
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
2098
-
"dev": true,
2099
-
"license": "ISC"
2100
-
},
2101
-
"node_modules/simple-swizzle": {
2102
-
"version": "0.2.2",
2103
-
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
2104
-
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
2105
-
"dev": true,
2106
-
"license": "MIT",
2107
-
"optional": true,
2108
-
"dependencies": {
2109
-
"is-arrayish": "^0.3.1"
2110
-
}
2111
-
},
2112
-
"node_modules/source-map": {
2113
-
"version": "0.6.1",
2114
-
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
2115
-
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
2116
-
"dev": true,
2117
-
"license": "BSD-3-Clause",
2118
-
"engines": {
2119
-
"node": ">=0.10.0"
2120
-
}
2121
-
},
2122
-
"node_modules/source-map-js": {
2123
-
"version": "1.2.1",
2124
-
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2125
-
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2126
-
"dev": true,
2127
-
"license": "BSD-3-Clause",
2128
-
"engines": {
2129
-
"node": ">=0.10.0"
2130
-
}
2131
-
},
2132
-
"node_modules/stackback": {
2133
-
"version": "0.0.2",
2134
-
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
2135
-
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
2136
-
"dev": true,
2137
-
"license": "MIT"
2138
-
},
2139
-
"node_modules/stacktracey": {
2140
-
"version": "2.1.8",
2141
-
"resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz",
2142
-
"integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==",
2143
-
"dev": true,
2144
-
"license": "Unlicense",
2145
-
"dependencies": {
2146
-
"as-table": "^1.0.36",
2147
-
"get-source": "^2.0.12"
2148
-
}
2149
-
},
2150
-
"node_modules/std-env": {
2151
-
"version": "3.9.0",
2152
-
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
2153
-
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
2154
-
"dev": true,
2155
-
"license": "MIT"
2156
-
},
2157
-
"node_modules/stoppable": {
2158
-
"version": "1.1.0",
2159
-
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
2160
-
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
2161
-
"dev": true,
2162
-
"license": "MIT",
2163
-
"engines": {
2164
-
"node": ">=4",
2165
-
"npm": ">=6"
2166
-
}
2167
-
},
2168
-
"node_modules/tinybench": {
2169
-
"version": "2.9.0",
2170
-
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
2171
-
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
2172
-
"dev": true,
2173
-
"license": "MIT"
2174
-
},
2175
-
"node_modules/tinyexec": {
2176
-
"version": "0.3.2",
2177
-
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
2178
-
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
2179
-
"dev": true,
2180
-
"license": "MIT"
2181
-
},
2182
-
"node_modules/tinyglobby": {
2183
-
"version": "0.2.13",
2184
-
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
2185
-
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
2186
-
"dev": true,
2187
-
"license": "MIT",
2188
-
"dependencies": {
2189
-
"fdir": "^6.4.4",
2190
-
"picomatch": "^4.0.2"
2191
-
},
2192
-
"engines": {
2193
-
"node": ">=12.0.0"
2194
-
},
2195
-
"funding": {
2196
-
"url": "https://github.com/sponsors/SuperchupuDev"
2197
-
}
2198
-
},
2199
-
"node_modules/tinypool": {
2200
-
"version": "1.0.2",
2201
-
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
2202
-
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
2203
-
"dev": true,
2204
-
"license": "MIT",
2205
-
"engines": {
2206
-
"node": "^18.0.0 || >=20.0.0"
2207
-
}
2208
-
},
2209
-
"node_modules/tinyrainbow": {
2210
-
"version": "2.0.0",
2211
-
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
2212
-
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
2213
-
"dev": true,
2214
-
"license": "MIT",
2215
-
"engines": {
2216
-
"node": ">=14.0.0"
2217
-
}
2218
-
},
2219
-
"node_modules/tinyspy": {
2220
-
"version": "3.0.2",
2221
-
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
2222
-
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
2223
-
"dev": true,
2224
-
"license": "MIT",
2225
-
"engines": {
2226
-
"node": ">=14.0.0"
2227
-
}
2228
-
},
2229
-
"node_modules/tslib": {
2230
-
"version": "2.8.1",
2231
-
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2232
-
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2233
-
"dev": true,
2234
-
"license": "0BSD",
2235
-
"optional": true
2236
-
},
2237
-
"node_modules/ufo": {
2238
-
"version": "1.6.1",
2239
-
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
2240
-
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
2241
-
"dev": true,
2242
-
"license": "MIT"
2243
-
},
2244
-
"node_modules/undici": {
2245
-
"version": "5.29.0",
2246
-
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
2247
-
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
2248
-
"dev": true,
2249
-
"license": "MIT",
2250
-
"dependencies": {
2251
-
"@fastify/busboy": "^2.0.0"
2252
-
},
2253
-
"engines": {
2254
-
"node": ">=14.0"
2255
-
}
2256
-
},
2257
-
"node_modules/unenv": {
2258
-
"version": "2.0.0-rc.15",
2259
-
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz",
2260
-
"integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==",
2261
-
"dev": true,
2262
-
"license": "MIT",
2263
-
"dependencies": {
2264
-
"defu": "^6.1.4",
2265
-
"exsolve": "^1.0.4",
2266
-
"ohash": "^2.0.11",
2267
-
"pathe": "^2.0.3",
2268
-
"ufo": "^1.5.4"
2269
-
}
2270
-
},
2271
-
"node_modules/vite": {
2272
-
"version": "6.3.4",
2273
-
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
2274
-
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
2275
-
"dev": true,
2276
-
"license": "MIT",
2277
-
"dependencies": {
2278
-
"esbuild": "^0.25.0",
2279
-
"fdir": "^6.4.4",
2280
-
"picomatch": "^4.0.2",
2281
-
"postcss": "^8.5.3",
2282
-
"rollup": "^4.34.9",
2283
-
"tinyglobby": "^0.2.13"
2284
-
},
2285
-
"bin": {
2286
-
"vite": "bin/vite.js"
2287
-
},
2288
-
"engines": {
2289
-
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2290
-
},
2291
-
"funding": {
2292
-
"url": "https://github.com/vitejs/vite?sponsor=1"
2293
-
},
2294
-
"optionalDependencies": {
2295
-
"fsevents": "~2.3.3"
2296
-
},
2297
-
"peerDependencies": {
2298
-
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2299
-
"jiti": ">=1.21.0",
2300
-
"less": "*",
2301
-
"lightningcss": "^1.21.0",
2302
-
"sass": "*",
2303
-
"sass-embedded": "*",
2304
-
"stylus": "*",
2305
-
"sugarss": "*",
2306
-
"terser": "^5.16.0",
2307
-
"tsx": "^4.8.1",
2308
-
"yaml": "^2.4.2"
2309
-
},
2310
-
"peerDependenciesMeta": {
2311
-
"@types/node": {
2312
-
"optional": true
2313
-
},
2314
-
"jiti": {
2315
-
"optional": true
2316
-
},
2317
-
"less": {
2318
-
"optional": true
2319
-
},
2320
-
"lightningcss": {
2321
-
"optional": true
2322
-
},
2323
-
"sass": {
2324
-
"optional": true
2325
-
},
2326
-
"sass-embedded": {
2327
-
"optional": true
2328
-
},
2329
-
"stylus": {
2330
-
"optional": true
2331
-
},
2332
-
"sugarss": {
2333
-
"optional": true
2334
-
},
2335
-
"terser": {
2336
-
"optional": true
2337
-
},
2338
-
"tsx": {
2339
-
"optional": true
2340
-
},
2341
-
"yaml": {
2342
-
"optional": true
2343
-
}
2344
-
}
2345
-
},
2346
-
"node_modules/vite-node": {
2347
-
"version": "3.0.9",
2348
-
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz",
2349
-
"integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==",
2350
-
"dev": true,
2351
-
"license": "MIT",
2352
-
"dependencies": {
2353
-
"cac": "^6.7.14",
2354
-
"debug": "^4.4.0",
2355
-
"es-module-lexer": "^1.6.0",
2356
-
"pathe": "^2.0.3",
2357
-
"vite": "^5.0.0 || ^6.0.0"
2358
-
},
2359
-
"bin": {
2360
-
"vite-node": "vite-node.mjs"
2361
-
},
2362
-
"engines": {
2363
-
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2364
-
},
2365
-
"funding": {
2366
-
"url": "https://opencollective.com/vitest"
2367
-
}
2368
-
},
2369
-
"node_modules/vitest": {
2370
-
"version": "3.0.9",
2371
-
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz",
2372
-
"integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==",
2373
-
"dev": true,
2374
-
"license": "MIT",
2375
-
"dependencies": {
2376
-
"@vitest/expect": "3.0.9",
2377
-
"@vitest/mocker": "3.0.9",
2378
-
"@vitest/pretty-format": "^3.0.9",
2379
-
"@vitest/runner": "3.0.9",
2380
-
"@vitest/snapshot": "3.0.9",
2381
-
"@vitest/spy": "3.0.9",
2382
-
"@vitest/utils": "3.0.9",
2383
-
"chai": "^5.2.0",
2384
-
"debug": "^4.4.0",
2385
-
"expect-type": "^1.1.0",
2386
-
"magic-string": "^0.30.17",
2387
-
"pathe": "^2.0.3",
2388
-
"std-env": "^3.8.0",
2389
-
"tinybench": "^2.9.0",
2390
-
"tinyexec": "^0.3.2",
2391
-
"tinypool": "^1.0.2",
2392
-
"tinyrainbow": "^2.0.0",
2393
-
"vite": "^5.0.0 || ^6.0.0",
2394
-
"vite-node": "3.0.9",
2395
-
"why-is-node-running": "^2.3.0"
2396
-
},
2397
-
"bin": {
2398
-
"vitest": "vitest.mjs"
2399
-
},
2400
-
"engines": {
2401
-
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2402
-
},
2403
-
"funding": {
2404
-
"url": "https://opencollective.com/vitest"
2405
-
},
2406
-
"peerDependencies": {
2407
-
"@edge-runtime/vm": "*",
2408
-
"@types/debug": "^4.1.12",
2409
-
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2410
-
"@vitest/browser": "3.0.9",
2411
-
"@vitest/ui": "3.0.9",
2412
-
"happy-dom": "*",
2413
-
"jsdom": "*"
2414
-
},
2415
-
"peerDependenciesMeta": {
2416
-
"@edge-runtime/vm": {
2417
-
"optional": true
2418
-
},
2419
-
"@types/debug": {
2420
-
"optional": true
2421
-
},
2422
-
"@types/node": {
2423
-
"optional": true
2424
-
},
2425
-
"@vitest/browser": {
2426
-
"optional": true
2427
-
},
2428
-
"@vitest/ui": {
2429
-
"optional": true
2430
-
},
2431
-
"happy-dom": {
2432
-
"optional": true
2433
-
},
2434
-
"jsdom": {
2435
-
"optional": true
2436
-
}
2437
-
}
2438
-
},
2439
-
"node_modules/why-is-node-running": {
2440
-
"version": "2.3.0",
2441
-
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
2442
-
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
2443
-
"dev": true,
2444
-
"license": "MIT",
2445
-
"dependencies": {
2446
-
"siginfo": "^2.0.0",
2447
-
"stackback": "0.0.2"
2448
-
},
2449
-
"bin": {
2450
-
"why-is-node-running": "cli.js"
2451
-
},
2452
-
"engines": {
2453
-
"node": ">=8"
2454
-
}
2455
-
},
2456
-
"node_modules/workerd": {
2457
-
"version": "1.20250428.0",
2458
-
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250428.0.tgz",
2459
-
"integrity": "sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==",
2460
-
"dev": true,
2461
-
"hasInstallScript": true,
2462
-
"license": "Apache-2.0",
2463
-
"bin": {
2464
-
"workerd": "bin/workerd"
2465
-
},
2466
-
"engines": {
2467
-
"node": ">=16"
2468
-
},
2469
-
"optionalDependencies": {
2470
-
"@cloudflare/workerd-darwin-64": "1.20250428.0",
2471
-
"@cloudflare/workerd-darwin-arm64": "1.20250428.0",
2472
-
"@cloudflare/workerd-linux-64": "1.20250428.0",
2473
-
"@cloudflare/workerd-linux-arm64": "1.20250428.0",
2474
-
"@cloudflare/workerd-windows-64": "1.20250428.0"
2475
-
}
2476
-
},
2477
-
"node_modules/wrangler": {
2478
-
"version": "4.14.1",
2479
-
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.14.1.tgz",
2480
-
"integrity": "sha512-EU7IThP7i68TBftJJSveogvWZ5k/WRijcJh3UclDWiWWhDZTPbL6LOJEFhHKqFzHOaC4Y2Aewt48rfTz0e7oCw==",
2481
-
"dev": true,
2482
-
"license": "MIT OR Apache-2.0",
2483
-
"dependencies": {
2484
-
"@cloudflare/kv-asset-handler": "0.4.0",
2485
-
"@cloudflare/unenv-preset": "2.3.1",
2486
-
"blake3-wasm": "2.1.5",
2487
-
"esbuild": "0.25.2",
2488
-
"miniflare": "4.20250428.1",
2489
-
"path-to-regexp": "6.3.0",
2490
-
"unenv": "2.0.0-rc.15",
2491
-
"workerd": "1.20250428.0"
2492
-
},
2493
-
"bin": {
2494
-
"wrangler": "bin/wrangler.js",
2495
-
"wrangler2": "bin/wrangler.js"
2496
-
},
2497
-
"engines": {
2498
-
"node": ">=18.0.0"
2499
-
},
2500
-
"optionalDependencies": {
2501
-
"fsevents": "~2.3.2",
2502
-
"sharp": "^0.33.5"
2503
-
},
2504
-
"peerDependencies": {
2505
-
"@cloudflare/workers-types": "^4.20250428.0"
2506
-
},
2507
-
"peerDependenciesMeta": {
2508
-
"@cloudflare/workers-types": {
2509
-
"optional": true
2510
-
}
2511
-
}
2512
-
},
2513
-
"node_modules/wrangler/node_modules/@esbuild/aix-ppc64": {
2514
-
"version": "0.25.2",
2515
-
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
2516
-
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
2517
-
"cpu": [
2518
-
"ppc64"
2519
-
],
2520
-
"dev": true,
2521
-
"license": "MIT",
2522
-
"optional": true,
2523
-
"os": [
2524
-
"aix"
2525
-
],
2526
-
"engines": {
2527
-
"node": ">=18"
2528
-
}
2529
-
},
2530
-
"node_modules/wrangler/node_modules/@esbuild/android-arm": {
2531
-
"version": "0.25.2",
2532
-
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
2533
-
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
2534
-
"cpu": [
2535
-
"arm"
2536
-
],
2537
-
"dev": true,
2538
-
"license": "MIT",
2539
-
"optional": true,
2540
-
"os": [
2541
-
"android"
2542
-
],
2543
-
"engines": {
2544
-
"node": ">=18"
2545
-
}
2546
-
},
2547
-
"node_modules/wrangler/node_modules/@esbuild/android-arm64": {
2548
-
"version": "0.25.2",
2549
-
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
2550
-
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
2551
-
"cpu": [
2552
-
"arm64"
2553
-
],
2554
-
"dev": true,
2555
-
"license": "MIT",
2556
-
"optional": true,
2557
-
"os": [
2558
-
"android"
2559
-
],
2560
-
"engines": {
2561
-
"node": ">=18"
2562
-
}
2563
-
},
2564
-
"node_modules/wrangler/node_modules/@esbuild/android-x64": {
2565
-
"version": "0.25.2",
2566
-
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
2567
-
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
2568
-
"cpu": [
2569
-
"x64"
2570
-
],
2571
-
"dev": true,
2572
-
"license": "MIT",
2573
-
"optional": true,
2574
-
"os": [
2575
-
"android"
2576
-
],
2577
-
"engines": {
2578
-
"node": ">=18"
2579
-
}
2580
-
},
2581
-
"node_modules/wrangler/node_modules/@esbuild/darwin-arm64": {
2582
-
"version": "0.25.2",
2583
-
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
2584
-
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
2585
-
"cpu": [
2586
-
"arm64"
2587
-
],
2588
-
"dev": true,
2589
-
"license": "MIT",
2590
-
"optional": true,
2591
-
"os": [
2592
-
"darwin"
2593
-
],
2594
-
"engines": {
2595
-
"node": ">=18"
2596
-
}
2597
-
},
2598
-
"node_modules/wrangler/node_modules/@esbuild/darwin-x64": {
2599
-
"version": "0.25.2",
2600
-
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
2601
-
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
2602
-
"cpu": [
2603
-
"x64"
2604
-
],
2605
-
"dev": true,
2606
-
"license": "MIT",
2607
-
"optional": true,
2608
-
"os": [
2609
-
"darwin"
2610
-
],
2611
-
"engines": {
2612
-
"node": ">=18"
2613
-
}
2614
-
},
2615
-
"node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": {
2616
-
"version": "0.25.2",
2617
-
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
2618
-
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
2619
-
"cpu": [
2620
-
"arm64"
2621
-
],
2622
-
"dev": true,
2623
-
"license": "MIT",
2624
-
"optional": true,
2625
-
"os": [
2626
-
"freebsd"
2627
-
],
2628
-
"engines": {
2629
-
"node": ">=18"
2630
-
}
2631
-
},
2632
-
"node_modules/wrangler/node_modules/@esbuild/freebsd-x64": {
2633
-
"version": "0.25.2",
2634
-
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
2635
-
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
2636
-
"cpu": [
2637
-
"x64"
2638
-
],
2639
-
"dev": true,
2640
-
"license": "MIT",
2641
-
"optional": true,
2642
-
"os": [
2643
-
"freebsd"
2644
-
],
2645
-
"engines": {
2646
-
"node": ">=18"
2647
-
}
2648
-
},
2649
-
"node_modules/wrangler/node_modules/@esbuild/linux-arm": {
2650
-
"version": "0.25.2",
2651
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
2652
-
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
2653
-
"cpu": [
2654
-
"arm"
2655
-
],
2656
-
"dev": true,
2657
-
"license": "MIT",
2658
-
"optional": true,
2659
-
"os": [
2660
-
"linux"
2661
-
],
2662
-
"engines": {
2663
-
"node": ">=18"
2664
-
}
2665
-
},
2666
-
"node_modules/wrangler/node_modules/@esbuild/linux-arm64": {
2667
-
"version": "0.25.2",
2668
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
2669
-
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
2670
-
"cpu": [
2671
-
"arm64"
2672
-
],
2673
-
"dev": true,
2674
-
"license": "MIT",
2675
-
"optional": true,
2676
-
"os": [
2677
-
"linux"
2678
-
],
2679
-
"engines": {
2680
-
"node": ">=18"
2681
-
}
2682
-
},
2683
-
"node_modules/wrangler/node_modules/@esbuild/linux-ia32": {
2684
-
"version": "0.25.2",
2685
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
2686
-
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
2687
-
"cpu": [
2688
-
"ia32"
2689
-
],
2690
-
"dev": true,
2691
-
"license": "MIT",
2692
-
"optional": true,
2693
-
"os": [
2694
-
"linux"
2695
-
],
2696
-
"engines": {
2697
-
"node": ">=18"
2698
-
}
2699
-
},
2700
-
"node_modules/wrangler/node_modules/@esbuild/linux-loong64": {
2701
-
"version": "0.25.2",
2702
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
2703
-
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
2704
-
"cpu": [
2705
-
"loong64"
2706
-
],
2707
-
"dev": true,
2708
-
"license": "MIT",
2709
-
"optional": true,
2710
-
"os": [
2711
-
"linux"
2712
-
],
2713
-
"engines": {
2714
-
"node": ">=18"
2715
-
}
2716
-
},
2717
-
"node_modules/wrangler/node_modules/@esbuild/linux-mips64el": {
2718
-
"version": "0.25.2",
2719
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
2720
-
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
2721
-
"cpu": [
2722
-
"mips64el"
2723
-
],
2724
-
"dev": true,
2725
-
"license": "MIT",
2726
-
"optional": true,
2727
-
"os": [
2728
-
"linux"
2729
-
],
2730
-
"engines": {
2731
-
"node": ">=18"
2732
-
}
2733
-
},
2734
-
"node_modules/wrangler/node_modules/@esbuild/linux-ppc64": {
2735
-
"version": "0.25.2",
2736
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
2737
-
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
2738
-
"cpu": [
2739
-
"ppc64"
2740
-
],
2741
-
"dev": true,
2742
-
"license": "MIT",
2743
-
"optional": true,
2744
-
"os": [
2745
-
"linux"
2746
-
],
2747
-
"engines": {
2748
-
"node": ">=18"
2749
-
}
2750
-
},
2751
-
"node_modules/wrangler/node_modules/@esbuild/linux-riscv64": {
2752
-
"version": "0.25.2",
2753
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
2754
-
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
2755
-
"cpu": [
2756
-
"riscv64"
2757
-
],
2758
-
"dev": true,
2759
-
"license": "MIT",
2760
-
"optional": true,
2761
-
"os": [
2762
-
"linux"
2763
-
],
2764
-
"engines": {
2765
-
"node": ">=18"
2766
-
}
2767
-
},
2768
-
"node_modules/wrangler/node_modules/@esbuild/linux-s390x": {
2769
-
"version": "0.25.2",
2770
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
2771
-
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
2772
-
"cpu": [
2773
-
"s390x"
2774
-
],
2775
-
"dev": true,
2776
-
"license": "MIT",
2777
-
"optional": true,
2778
-
"os": [
2779
-
"linux"
2780
-
],
2781
-
"engines": {
2782
-
"node": ">=18"
2783
-
}
2784
-
},
2785
-
"node_modules/wrangler/node_modules/@esbuild/linux-x64": {
2786
-
"version": "0.25.2",
2787
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
2788
-
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
2789
-
"cpu": [
2790
-
"x64"
2791
-
],
2792
-
"dev": true,
2793
-
"license": "MIT",
2794
-
"optional": true,
2795
-
"os": [
2796
-
"linux"
2797
-
],
2798
-
"engines": {
2799
-
"node": ">=18"
2800
-
}
2801
-
},
2802
-
"node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": {
2803
-
"version": "0.25.2",
2804
-
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
2805
-
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
2806
-
"cpu": [
2807
-
"arm64"
2808
-
],
2809
-
"dev": true,
2810
-
"license": "MIT",
2811
-
"optional": true,
2812
-
"os": [
2813
-
"netbsd"
2814
-
],
2815
-
"engines": {
2816
-
"node": ">=18"
2817
-
}
2818
-
},
2819
-
"node_modules/wrangler/node_modules/@esbuild/netbsd-x64": {
2820
-
"version": "0.25.2",
2821
-
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
2822
-
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
2823
-
"cpu": [
2824
-
"x64"
2825
-
],
2826
-
"dev": true,
2827
-
"license": "MIT",
2828
-
"optional": true,
2829
-
"os": [
2830
-
"netbsd"
2831
-
],
2832
-
"engines": {
2833
-
"node": ">=18"
2834
-
}
2835
-
},
2836
-
"node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": {
2837
-
"version": "0.25.2",
2838
-
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
2839
-
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
2840
-
"cpu": [
2841
-
"arm64"
2842
-
],
2843
-
"dev": true,
2844
-
"license": "MIT",
2845
-
"optional": true,
2846
-
"os": [
2847
-
"openbsd"
2848
-
],
2849
-
"engines": {
2850
-
"node": ">=18"
2851
-
}
2852
-
},
2853
-
"node_modules/wrangler/node_modules/@esbuild/openbsd-x64": {
2854
-
"version": "0.25.2",
2855
-
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
2856
-
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
2857
-
"cpu": [
2858
-
"x64"
2859
-
],
2860
-
"dev": true,
2861
-
"license": "MIT",
2862
-
"optional": true,
2863
-
"os": [
2864
-
"openbsd"
2865
-
],
2866
-
"engines": {
2867
-
"node": ">=18"
2868
-
}
2869
-
},
2870
-
"node_modules/wrangler/node_modules/@esbuild/sunos-x64": {
2871
-
"version": "0.25.2",
2872
-
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
2873
-
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
2874
-
"cpu": [
2875
-
"x64"
2876
-
],
2877
-
"dev": true,
2878
-
"license": "MIT",
2879
-
"optional": true,
2880
-
"os": [
2881
-
"sunos"
2882
-
],
2883
-
"engines": {
2884
-
"node": ">=18"
2885
-
}
2886
-
},
2887
-
"node_modules/wrangler/node_modules/@esbuild/win32-arm64": {
2888
-
"version": "0.25.2",
2889
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
2890
-
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
2891
-
"cpu": [
2892
-
"arm64"
2893
-
],
2894
-
"dev": true,
2895
-
"license": "MIT",
2896
-
"optional": true,
2897
-
"os": [
2898
-
"win32"
2899
-
],
2900
-
"engines": {
2901
-
"node": ">=18"
2902
-
}
2903
-
},
2904
-
"node_modules/wrangler/node_modules/@esbuild/win32-ia32": {
2905
-
"version": "0.25.2",
2906
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
2907
-
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
2908
-
"cpu": [
2909
-
"ia32"
2910
-
],
2911
-
"dev": true,
2912
-
"license": "MIT",
2913
-
"optional": true,
2914
-
"os": [
2915
-
"win32"
2916
-
],
2917
-
"engines": {
2918
-
"node": ">=18"
2919
-
}
2920
-
},
2921
-
"node_modules/wrangler/node_modules/@esbuild/win32-x64": {
2922
-
"version": "0.25.2",
2923
-
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
2924
-
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
2925
-
"cpu": [
2926
-
"x64"
2927
-
],
2928
-
"dev": true,
2929
-
"license": "MIT",
2930
-
"optional": true,
2931
-
"os": [
2932
-
"win32"
2933
-
],
2934
-
"engines": {
2935
-
"node": ">=18"
2936
-
}
2937
-
},
2938
-
"node_modules/wrangler/node_modules/esbuild": {
2939
-
"version": "0.25.2",
2940
-
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
2941
-
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
2942
-
"dev": true,
2943
-
"hasInstallScript": true,
2944
-
"license": "MIT",
2945
-
"bin": {
2946
-
"esbuild": "bin/esbuild"
2947
-
},
2948
-
"engines": {
2949
-
"node": ">=18"
2950
-
},
2951
-
"optionalDependencies": {
2952
-
"@esbuild/aix-ppc64": "0.25.2",
2953
-
"@esbuild/android-arm": "0.25.2",
2954
-
"@esbuild/android-arm64": "0.25.2",
2955
-
"@esbuild/android-x64": "0.25.2",
2956
-
"@esbuild/darwin-arm64": "0.25.2",
2957
-
"@esbuild/darwin-x64": "0.25.2",
2958
-
"@esbuild/freebsd-arm64": "0.25.2",
2959
-
"@esbuild/freebsd-x64": "0.25.2",
2960
-
"@esbuild/linux-arm": "0.25.2",
2961
-
"@esbuild/linux-arm64": "0.25.2",
2962
-
"@esbuild/linux-ia32": "0.25.2",
2963
-
"@esbuild/linux-loong64": "0.25.2",
2964
-
"@esbuild/linux-mips64el": "0.25.2",
2965
-
"@esbuild/linux-ppc64": "0.25.2",
2966
-
"@esbuild/linux-riscv64": "0.25.2",
2967
-
"@esbuild/linux-s390x": "0.25.2",
2968
-
"@esbuild/linux-x64": "0.25.2",
2969
-
"@esbuild/netbsd-arm64": "0.25.2",
2970
-
"@esbuild/netbsd-x64": "0.25.2",
2971
-
"@esbuild/openbsd-arm64": "0.25.2",
2972
-
"@esbuild/openbsd-x64": "0.25.2",
2973
-
"@esbuild/sunos-x64": "0.25.2",
2974
-
"@esbuild/win32-arm64": "0.25.2",
2975
-
"@esbuild/win32-ia32": "0.25.2",
2976
-
"@esbuild/win32-x64": "0.25.2"
2977
-
}
2978
-
},
2979
-
"node_modules/ws": {
2980
-
"version": "8.18.0",
2981
-
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
2982
-
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
2983
-
"dev": true,
2984
-
"license": "MIT",
2985
-
"engines": {
2986
-
"node": ">=10.0.0"
2987
-
},
2988
-
"peerDependencies": {
2989
-
"bufferutil": "^4.0.1",
2990
-
"utf-8-validate": ">=5.0.2"
2991
-
},
2992
-
"peerDependenciesMeta": {
2993
-
"bufferutil": {
2994
-
"optional": true
2995
-
},
2996
-
"utf-8-validate": {
2997
-
"optional": true
2998
-
}
2999
-
}
3000
-
},
3001
-
"node_modules/youch": {
3002
-
"version": "3.3.4",
3003
-
"resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz",
3004
-
"integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==",
3005
-
"dev": true,
3006
-
"license": "MIT",
3007
-
"dependencies": {
3008
-
"cookie": "^0.7.1",
3009
-
"mustache": "^4.2.0",
3010
-
"stacktracey": "^2.1.8"
3011
-
}
3012
-
},
3013
-
"node_modules/zod": {
3014
-
"version": "3.24.3",
3015
-
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
3016
-
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
3017
-
"dev": true,
3018
-
"license": "MIT",
3019
-
"funding": {
3020
-
"url": "https://github.com/sponsors/colinhacks"
3021
-
}
3022
-
}
3023
-
}
2
+
"name": "avatar",
3
+
"version": "0.0.0",
4
+
"lockfileVersion": 3,
5
+
"requires": true,
6
+
"packages": {
7
+
"": {
8
+
"name": "avatar",
9
+
"version": "0.0.0",
10
+
"dependencies": {
11
+
"@atproto/identity": "^0.4.1"
12
+
},
13
+
"devDependencies": {
14
+
"@cloudflare/vitest-pool-workers": "^0.8.19",
15
+
"vitest": "~3.0.7",
16
+
"wrangler": "^4.14.1"
17
+
}
18
+
},
19
+
"node_modules/@atproto/common-web": {
20
+
"version": "0.4.7",
21
+
"resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.7.tgz",
22
+
"integrity": "sha512-vjw2+81KPo2/SAbbARGn64Ln+6JTI0FTI4xk8if0ebBfDxFRmHb2oSN1y77hzNq/ybGHqA2mecfhS03pxC5+lg==",
23
+
"license": "MIT",
24
+
"dependencies": {
25
+
"@atproto/lex-data": "0.0.3",
26
+
"@atproto/lex-json": "0.0.3",
27
+
"zod": "^3.23.8"
28
+
}
29
+
},
30
+
"node_modules/@atproto/crypto": {
31
+
"version": "0.4.5",
32
+
"resolved": "https://registry.npmjs.org/@atproto/crypto/-/crypto-0.4.5.tgz",
33
+
"integrity": "sha512-n40aKkMoCatP0u9Yvhrdk6fXyOHFDDbkdm4h4HCyWW+KlKl8iXfD5iV+ECq+w5BM+QH25aIpt3/j6EUNerhLxw==",
34
+
"license": "MIT",
35
+
"dependencies": {
36
+
"@noble/curves": "^1.7.0",
37
+
"@noble/hashes": "^1.6.1",
38
+
"uint8arrays": "3.0.0"
39
+
},
40
+
"engines": {
41
+
"node": ">=18.7.0"
42
+
}
43
+
},
44
+
"node_modules/@atproto/identity": {
45
+
"version": "0.4.10",
46
+
"resolved": "https://registry.npmjs.org/@atproto/identity/-/identity-0.4.10.tgz",
47
+
"integrity": "sha512-nQbzDLXOhM8p/wo0cTh5DfMSOSHzj6jizpodX37LJ4S1TZzumSxAjHEZa5Rev3JaoD5uSWMVE0MmKEGWkPPvfQ==",
48
+
"license": "MIT",
49
+
"dependencies": {
50
+
"@atproto/common-web": "^0.4.4",
51
+
"@atproto/crypto": "^0.4.4"
52
+
},
53
+
"engines": {
54
+
"node": ">=18.7.0"
55
+
}
56
+
},
57
+
"node_modules/@atproto/lex-data": {
58
+
"version": "0.0.3",
59
+
"resolved": "https://registry.npmjs.org/@atproto/lex-data/-/lex-data-0.0.3.tgz",
60
+
"integrity": "sha512-ivo1IpY/EX+RIpxPgCf4cPhQo5bfu4nrpa1vJCt8hCm9SfoonJkDFGa0n4SMw4JnXZoUcGcrJ46L+D8bH6GI2g==",
61
+
"license": "MIT",
62
+
"dependencies": {
63
+
"@atproto/syntax": "0.4.2",
64
+
"multiformats": "^9.9.0",
65
+
"tslib": "^2.8.1",
66
+
"uint8arrays": "3.0.0",
67
+
"unicode-segmenter": "^0.14.0"
68
+
}
69
+
},
70
+
"node_modules/@atproto/lex-json": {
71
+
"version": "0.0.3",
72
+
"resolved": "https://registry.npmjs.org/@atproto/lex-json/-/lex-json-0.0.3.tgz",
73
+
"integrity": "sha512-ZVcY7XlRfdPYvQQ2WroKUepee0+NCovrSXgXURM3Xv+n5jflJCoczguROeRr8sN0xvT0ZbzMrDNHCUYKNnxcjw==",
74
+
"license": "MIT",
75
+
"dependencies": {
76
+
"@atproto/lex-data": "0.0.3",
77
+
"tslib": "^2.8.1"
78
+
}
79
+
},
80
+
"node_modules/@atproto/syntax": {
81
+
"version": "0.4.2",
82
+
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.2.tgz",
83
+
"integrity": "sha512-X9XSRPinBy/0VQ677j8VXlBsYSsUXaiqxWVpGGxJYsAhugdQRb0jqaVKJFtm6RskeNkV6y9xclSUi9UYG/COrA==",
84
+
"license": "MIT"
85
+
},
86
+
"node_modules/@cloudflare/kv-asset-handler": {
87
+
"version": "0.4.0",
88
+
"resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz",
89
+
"integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==",
90
+
"dev": true,
91
+
"license": "MIT OR Apache-2.0",
92
+
"dependencies": {
93
+
"mime": "^3.0.0"
94
+
},
95
+
"engines": {
96
+
"node": ">=18.0.0"
97
+
}
98
+
},
99
+
"node_modules/@cloudflare/unenv-preset": {
100
+
"version": "2.3.1",
101
+
"resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.1.tgz",
102
+
"integrity": "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==",
103
+
"dev": true,
104
+
"license": "MIT OR Apache-2.0",
105
+
"peerDependencies": {
106
+
"unenv": "2.0.0-rc.15",
107
+
"workerd": "^1.20250320.0"
108
+
},
109
+
"peerDependenciesMeta": {
110
+
"workerd": {
111
+
"optional": true
112
+
}
113
+
}
114
+
},
115
+
"node_modules/@cloudflare/vitest-pool-workers": {
116
+
"version": "0.8.24",
117
+
"resolved": "https://registry.npmjs.org/@cloudflare/vitest-pool-workers/-/vitest-pool-workers-0.8.24.tgz",
118
+
"integrity": "sha512-wT2PABJQ9YLYWrVu4CRZOjvmjHkdbMyLTZPU9n/7JEMM3pgG8dY41F1Rj31UsXRQaXX39A/CTPGlk58dcMUysA==",
119
+
"dev": true,
120
+
"license": "MIT",
121
+
"dependencies": {
122
+
"birpc": "0.2.14",
123
+
"cjs-module-lexer": "^1.2.3",
124
+
"devalue": "^4.3.0",
125
+
"miniflare": "4.20250428.1",
126
+
"semver": "^7.7.1",
127
+
"wrangler": "4.14.1",
128
+
"zod": "^3.22.3"
129
+
},
130
+
"peerDependencies": {
131
+
"@vitest/runner": "2.0.x - 3.1.x",
132
+
"@vitest/snapshot": "2.0.x - 3.1.x",
133
+
"vitest": "2.0.x - 3.1.x"
134
+
}
135
+
},
136
+
"node_modules/@cloudflare/workerd-darwin-64": {
137
+
"version": "1.20250428.0",
138
+
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250428.0.tgz",
139
+
"integrity": "sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==",
140
+
"cpu": [
141
+
"x64"
142
+
],
143
+
"dev": true,
144
+
"license": "Apache-2.0",
145
+
"optional": true,
146
+
"os": [
147
+
"darwin"
148
+
],
149
+
"engines": {
150
+
"node": ">=16"
151
+
}
152
+
},
153
+
"node_modules/@cloudflare/workerd-darwin-arm64": {
154
+
"version": "1.20250428.0",
155
+
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250428.0.tgz",
156
+
"integrity": "sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==",
157
+
"cpu": [
158
+
"arm64"
159
+
],
160
+
"dev": true,
161
+
"license": "Apache-2.0",
162
+
"optional": true,
163
+
"os": [
164
+
"darwin"
165
+
],
166
+
"engines": {
167
+
"node": ">=16"
168
+
}
169
+
},
170
+
"node_modules/@cloudflare/workerd-linux-64": {
171
+
"version": "1.20250428.0",
172
+
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250428.0.tgz",
173
+
"integrity": "sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==",
174
+
"cpu": [
175
+
"x64"
176
+
],
177
+
"dev": true,
178
+
"license": "Apache-2.0",
179
+
"optional": true,
180
+
"os": [
181
+
"linux"
182
+
],
183
+
"engines": {
184
+
"node": ">=16"
185
+
}
186
+
},
187
+
"node_modules/@cloudflare/workerd-linux-arm64": {
188
+
"version": "1.20250428.0",
189
+
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250428.0.tgz",
190
+
"integrity": "sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==",
191
+
"cpu": [
192
+
"arm64"
193
+
],
194
+
"dev": true,
195
+
"license": "Apache-2.0",
196
+
"optional": true,
197
+
"os": [
198
+
"linux"
199
+
],
200
+
"engines": {
201
+
"node": ">=16"
202
+
}
203
+
},
204
+
"node_modules/@cloudflare/workerd-windows-64": {
205
+
"version": "1.20250428.0",
206
+
"resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250428.0.tgz",
207
+
"integrity": "sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==",
208
+
"cpu": [
209
+
"x64"
210
+
],
211
+
"dev": true,
212
+
"license": "Apache-2.0",
213
+
"optional": true,
214
+
"os": [
215
+
"win32"
216
+
],
217
+
"engines": {
218
+
"node": ">=16"
219
+
}
220
+
},
221
+
"node_modules/@cspotcode/source-map-support": {
222
+
"version": "0.8.1",
223
+
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
224
+
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
225
+
"dev": true,
226
+
"license": "MIT",
227
+
"dependencies": {
228
+
"@jridgewell/trace-mapping": "0.3.9"
229
+
},
230
+
"engines": {
231
+
"node": ">=12"
232
+
}
233
+
},
234
+
"node_modules/@emnapi/runtime": {
235
+
"version": "1.4.3",
236
+
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
237
+
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
238
+
"dev": true,
239
+
"license": "MIT",
240
+
"optional": true,
241
+
"dependencies": {
242
+
"tslib": "^2.4.0"
243
+
}
244
+
},
245
+
"node_modules/@esbuild/aix-ppc64": {
246
+
"version": "0.25.3",
247
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz",
248
+
"integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==",
249
+
"cpu": [
250
+
"ppc64"
251
+
],
252
+
"dev": true,
253
+
"license": "MIT",
254
+
"optional": true,
255
+
"os": [
256
+
"aix"
257
+
],
258
+
"engines": {
259
+
"node": ">=18"
260
+
}
261
+
},
262
+
"node_modules/@esbuild/android-arm": {
263
+
"version": "0.25.3",
264
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz",
265
+
"integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==",
266
+
"cpu": [
267
+
"arm"
268
+
],
269
+
"dev": true,
270
+
"license": "MIT",
271
+
"optional": true,
272
+
"os": [
273
+
"android"
274
+
],
275
+
"engines": {
276
+
"node": ">=18"
277
+
}
278
+
},
279
+
"node_modules/@esbuild/android-arm64": {
280
+
"version": "0.25.3",
281
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz",
282
+
"integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==",
283
+
"cpu": [
284
+
"arm64"
285
+
],
286
+
"dev": true,
287
+
"license": "MIT",
288
+
"optional": true,
289
+
"os": [
290
+
"android"
291
+
],
292
+
"engines": {
293
+
"node": ">=18"
294
+
}
295
+
},
296
+
"node_modules/@esbuild/android-x64": {
297
+
"version": "0.25.3",
298
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz",
299
+
"integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==",
300
+
"cpu": [
301
+
"x64"
302
+
],
303
+
"dev": true,
304
+
"license": "MIT",
305
+
"optional": true,
306
+
"os": [
307
+
"android"
308
+
],
309
+
"engines": {
310
+
"node": ">=18"
311
+
}
312
+
},
313
+
"node_modules/@esbuild/darwin-arm64": {
314
+
"version": "0.25.3",
315
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz",
316
+
"integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==",
317
+
"cpu": [
318
+
"arm64"
319
+
],
320
+
"dev": true,
321
+
"license": "MIT",
322
+
"optional": true,
323
+
"os": [
324
+
"darwin"
325
+
],
326
+
"engines": {
327
+
"node": ">=18"
328
+
}
329
+
},
330
+
"node_modules/@esbuild/darwin-x64": {
331
+
"version": "0.25.3",
332
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz",
333
+
"integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==",
334
+
"cpu": [
335
+
"x64"
336
+
],
337
+
"dev": true,
338
+
"license": "MIT",
339
+
"optional": true,
340
+
"os": [
341
+
"darwin"
342
+
],
343
+
"engines": {
344
+
"node": ">=18"
345
+
}
346
+
},
347
+
"node_modules/@esbuild/freebsd-arm64": {
348
+
"version": "0.25.3",
349
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz",
350
+
"integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==",
351
+
"cpu": [
352
+
"arm64"
353
+
],
354
+
"dev": true,
355
+
"license": "MIT",
356
+
"optional": true,
357
+
"os": [
358
+
"freebsd"
359
+
],
360
+
"engines": {
361
+
"node": ">=18"
362
+
}
363
+
},
364
+
"node_modules/@esbuild/freebsd-x64": {
365
+
"version": "0.25.3",
366
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz",
367
+
"integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==",
368
+
"cpu": [
369
+
"x64"
370
+
],
371
+
"dev": true,
372
+
"license": "MIT",
373
+
"optional": true,
374
+
"os": [
375
+
"freebsd"
376
+
],
377
+
"engines": {
378
+
"node": ">=18"
379
+
}
380
+
},
381
+
"node_modules/@esbuild/linux-arm": {
382
+
"version": "0.25.3",
383
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz",
384
+
"integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==",
385
+
"cpu": [
386
+
"arm"
387
+
],
388
+
"dev": true,
389
+
"license": "MIT",
390
+
"optional": true,
391
+
"os": [
392
+
"linux"
393
+
],
394
+
"engines": {
395
+
"node": ">=18"
396
+
}
397
+
},
398
+
"node_modules/@esbuild/linux-arm64": {
399
+
"version": "0.25.3",
400
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz",
401
+
"integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==",
402
+
"cpu": [
403
+
"arm64"
404
+
],
405
+
"dev": true,
406
+
"license": "MIT",
407
+
"optional": true,
408
+
"os": [
409
+
"linux"
410
+
],
411
+
"engines": {
412
+
"node": ">=18"
413
+
}
414
+
},
415
+
"node_modules/@esbuild/linux-ia32": {
416
+
"version": "0.25.3",
417
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz",
418
+
"integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==",
419
+
"cpu": [
420
+
"ia32"
421
+
],
422
+
"dev": true,
423
+
"license": "MIT",
424
+
"optional": true,
425
+
"os": [
426
+
"linux"
427
+
],
428
+
"engines": {
429
+
"node": ">=18"
430
+
}
431
+
},
432
+
"node_modules/@esbuild/linux-loong64": {
433
+
"version": "0.25.3",
434
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz",
435
+
"integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==",
436
+
"cpu": [
437
+
"loong64"
438
+
],
439
+
"dev": true,
440
+
"license": "MIT",
441
+
"optional": true,
442
+
"os": [
443
+
"linux"
444
+
],
445
+
"engines": {
446
+
"node": ">=18"
447
+
}
448
+
},
449
+
"node_modules/@esbuild/linux-mips64el": {
450
+
"version": "0.25.3",
451
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz",
452
+
"integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==",
453
+
"cpu": [
454
+
"mips64el"
455
+
],
456
+
"dev": true,
457
+
"license": "MIT",
458
+
"optional": true,
459
+
"os": [
460
+
"linux"
461
+
],
462
+
"engines": {
463
+
"node": ">=18"
464
+
}
465
+
},
466
+
"node_modules/@esbuild/linux-ppc64": {
467
+
"version": "0.25.3",
468
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz",
469
+
"integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==",
470
+
"cpu": [
471
+
"ppc64"
472
+
],
473
+
"dev": true,
474
+
"license": "MIT",
475
+
"optional": true,
476
+
"os": [
477
+
"linux"
478
+
],
479
+
"engines": {
480
+
"node": ">=18"
481
+
}
482
+
},
483
+
"node_modules/@esbuild/linux-riscv64": {
484
+
"version": "0.25.3",
485
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz",
486
+
"integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==",
487
+
"cpu": [
488
+
"riscv64"
489
+
],
490
+
"dev": true,
491
+
"license": "MIT",
492
+
"optional": true,
493
+
"os": [
494
+
"linux"
495
+
],
496
+
"engines": {
497
+
"node": ">=18"
498
+
}
499
+
},
500
+
"node_modules/@esbuild/linux-s390x": {
501
+
"version": "0.25.3",
502
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz",
503
+
"integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==",
504
+
"cpu": [
505
+
"s390x"
506
+
],
507
+
"dev": true,
508
+
"license": "MIT",
509
+
"optional": true,
510
+
"os": [
511
+
"linux"
512
+
],
513
+
"engines": {
514
+
"node": ">=18"
515
+
}
516
+
},
517
+
"node_modules/@esbuild/linux-x64": {
518
+
"version": "0.25.3",
519
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz",
520
+
"integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==",
521
+
"cpu": [
522
+
"x64"
523
+
],
524
+
"dev": true,
525
+
"license": "MIT",
526
+
"optional": true,
527
+
"os": [
528
+
"linux"
529
+
],
530
+
"engines": {
531
+
"node": ">=18"
532
+
}
533
+
},
534
+
"node_modules/@esbuild/netbsd-arm64": {
535
+
"version": "0.25.3",
536
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz",
537
+
"integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==",
538
+
"cpu": [
539
+
"arm64"
540
+
],
541
+
"dev": true,
542
+
"license": "MIT",
543
+
"optional": true,
544
+
"os": [
545
+
"netbsd"
546
+
],
547
+
"engines": {
548
+
"node": ">=18"
549
+
}
550
+
},
551
+
"node_modules/@esbuild/netbsd-x64": {
552
+
"version": "0.25.3",
553
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz",
554
+
"integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==",
555
+
"cpu": [
556
+
"x64"
557
+
],
558
+
"dev": true,
559
+
"license": "MIT",
560
+
"optional": true,
561
+
"os": [
562
+
"netbsd"
563
+
],
564
+
"engines": {
565
+
"node": ">=18"
566
+
}
567
+
},
568
+
"node_modules/@esbuild/openbsd-arm64": {
569
+
"version": "0.25.3",
570
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz",
571
+
"integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==",
572
+
"cpu": [
573
+
"arm64"
574
+
],
575
+
"dev": true,
576
+
"license": "MIT",
577
+
"optional": true,
578
+
"os": [
579
+
"openbsd"
580
+
],
581
+
"engines": {
582
+
"node": ">=18"
583
+
}
584
+
},
585
+
"node_modules/@esbuild/openbsd-x64": {
586
+
"version": "0.25.3",
587
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz",
588
+
"integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==",
589
+
"cpu": [
590
+
"x64"
591
+
],
592
+
"dev": true,
593
+
"license": "MIT",
594
+
"optional": true,
595
+
"os": [
596
+
"openbsd"
597
+
],
598
+
"engines": {
599
+
"node": ">=18"
600
+
}
601
+
},
602
+
"node_modules/@esbuild/sunos-x64": {
603
+
"version": "0.25.3",
604
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz",
605
+
"integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==",
606
+
"cpu": [
607
+
"x64"
608
+
],
609
+
"dev": true,
610
+
"license": "MIT",
611
+
"optional": true,
612
+
"os": [
613
+
"sunos"
614
+
],
615
+
"engines": {
616
+
"node": ">=18"
617
+
}
618
+
},
619
+
"node_modules/@esbuild/win32-arm64": {
620
+
"version": "0.25.3",
621
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz",
622
+
"integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==",
623
+
"cpu": [
624
+
"arm64"
625
+
],
626
+
"dev": true,
627
+
"license": "MIT",
628
+
"optional": true,
629
+
"os": [
630
+
"win32"
631
+
],
632
+
"engines": {
633
+
"node": ">=18"
634
+
}
635
+
},
636
+
"node_modules/@esbuild/win32-ia32": {
637
+
"version": "0.25.3",
638
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz",
639
+
"integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==",
640
+
"cpu": [
641
+
"ia32"
642
+
],
643
+
"dev": true,
644
+
"license": "MIT",
645
+
"optional": true,
646
+
"os": [
647
+
"win32"
648
+
],
649
+
"engines": {
650
+
"node": ">=18"
651
+
}
652
+
},
653
+
"node_modules/@esbuild/win32-x64": {
654
+
"version": "0.25.3",
655
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz",
656
+
"integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==",
657
+
"cpu": [
658
+
"x64"
659
+
],
660
+
"dev": true,
661
+
"license": "MIT",
662
+
"optional": true,
663
+
"os": [
664
+
"win32"
665
+
],
666
+
"engines": {
667
+
"node": ">=18"
668
+
}
669
+
},
670
+
"node_modules/@fastify/busboy": {
671
+
"version": "2.1.1",
672
+
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
673
+
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
674
+
"dev": true,
675
+
"license": "MIT",
676
+
"engines": {
677
+
"node": ">=14"
678
+
}
679
+
},
680
+
"node_modules/@img/sharp-darwin-arm64": {
681
+
"version": "0.33.5",
682
+
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
683
+
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
684
+
"cpu": [
685
+
"arm64"
686
+
],
687
+
"dev": true,
688
+
"license": "Apache-2.0",
689
+
"optional": true,
690
+
"os": [
691
+
"darwin"
692
+
],
693
+
"engines": {
694
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
695
+
},
696
+
"funding": {
697
+
"url": "https://opencollective.com/libvips"
698
+
},
699
+
"optionalDependencies": {
700
+
"@img/sharp-libvips-darwin-arm64": "1.0.4"
701
+
}
702
+
},
703
+
"node_modules/@img/sharp-darwin-x64": {
704
+
"version": "0.33.5",
705
+
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
706
+
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
707
+
"cpu": [
708
+
"x64"
709
+
],
710
+
"dev": true,
711
+
"license": "Apache-2.0",
712
+
"optional": true,
713
+
"os": [
714
+
"darwin"
715
+
],
716
+
"engines": {
717
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
718
+
},
719
+
"funding": {
720
+
"url": "https://opencollective.com/libvips"
721
+
},
722
+
"optionalDependencies": {
723
+
"@img/sharp-libvips-darwin-x64": "1.0.4"
724
+
}
725
+
},
726
+
"node_modules/@img/sharp-libvips-darwin-arm64": {
727
+
"version": "1.0.4",
728
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
729
+
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
730
+
"cpu": [
731
+
"arm64"
732
+
],
733
+
"dev": true,
734
+
"license": "LGPL-3.0-or-later",
735
+
"optional": true,
736
+
"os": [
737
+
"darwin"
738
+
],
739
+
"funding": {
740
+
"url": "https://opencollective.com/libvips"
741
+
}
742
+
},
743
+
"node_modules/@img/sharp-libvips-darwin-x64": {
744
+
"version": "1.0.4",
745
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
746
+
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
747
+
"cpu": [
748
+
"x64"
749
+
],
750
+
"dev": true,
751
+
"license": "LGPL-3.0-or-later",
752
+
"optional": true,
753
+
"os": [
754
+
"darwin"
755
+
],
756
+
"funding": {
757
+
"url": "https://opencollective.com/libvips"
758
+
}
759
+
},
760
+
"node_modules/@img/sharp-libvips-linux-arm": {
761
+
"version": "1.0.5",
762
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
763
+
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
764
+
"cpu": [
765
+
"arm"
766
+
],
767
+
"dev": true,
768
+
"license": "LGPL-3.0-or-later",
769
+
"optional": true,
770
+
"os": [
771
+
"linux"
772
+
],
773
+
"funding": {
774
+
"url": "https://opencollective.com/libvips"
775
+
}
776
+
},
777
+
"node_modules/@img/sharp-libvips-linux-arm64": {
778
+
"version": "1.0.4",
779
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
780
+
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
781
+
"cpu": [
782
+
"arm64"
783
+
],
784
+
"dev": true,
785
+
"license": "LGPL-3.0-or-later",
786
+
"optional": true,
787
+
"os": [
788
+
"linux"
789
+
],
790
+
"funding": {
791
+
"url": "https://opencollective.com/libvips"
792
+
}
793
+
},
794
+
"node_modules/@img/sharp-libvips-linux-s390x": {
795
+
"version": "1.0.4",
796
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
797
+
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
798
+
"cpu": [
799
+
"s390x"
800
+
],
801
+
"dev": true,
802
+
"license": "LGPL-3.0-or-later",
803
+
"optional": true,
804
+
"os": [
805
+
"linux"
806
+
],
807
+
"funding": {
808
+
"url": "https://opencollective.com/libvips"
809
+
}
810
+
},
811
+
"node_modules/@img/sharp-libvips-linux-x64": {
812
+
"version": "1.0.4",
813
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
814
+
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
815
+
"cpu": [
816
+
"x64"
817
+
],
818
+
"dev": true,
819
+
"license": "LGPL-3.0-or-later",
820
+
"optional": true,
821
+
"os": [
822
+
"linux"
823
+
],
824
+
"funding": {
825
+
"url": "https://opencollective.com/libvips"
826
+
}
827
+
},
828
+
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
829
+
"version": "1.0.4",
830
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
831
+
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
832
+
"cpu": [
833
+
"arm64"
834
+
],
835
+
"dev": true,
836
+
"license": "LGPL-3.0-or-later",
837
+
"optional": true,
838
+
"os": [
839
+
"linux"
840
+
],
841
+
"funding": {
842
+
"url": "https://opencollective.com/libvips"
843
+
}
844
+
},
845
+
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
846
+
"version": "1.0.4",
847
+
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
848
+
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
849
+
"cpu": [
850
+
"x64"
851
+
],
852
+
"dev": true,
853
+
"license": "LGPL-3.0-or-later",
854
+
"optional": true,
855
+
"os": [
856
+
"linux"
857
+
],
858
+
"funding": {
859
+
"url": "https://opencollective.com/libvips"
860
+
}
861
+
},
862
+
"node_modules/@img/sharp-linux-arm": {
863
+
"version": "0.33.5",
864
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
865
+
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
866
+
"cpu": [
867
+
"arm"
868
+
],
869
+
"dev": true,
870
+
"license": "Apache-2.0",
871
+
"optional": true,
872
+
"os": [
873
+
"linux"
874
+
],
875
+
"engines": {
876
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
877
+
},
878
+
"funding": {
879
+
"url": "https://opencollective.com/libvips"
880
+
},
881
+
"optionalDependencies": {
882
+
"@img/sharp-libvips-linux-arm": "1.0.5"
883
+
}
884
+
},
885
+
"node_modules/@img/sharp-linux-arm64": {
886
+
"version": "0.33.5",
887
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
888
+
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
889
+
"cpu": [
890
+
"arm64"
891
+
],
892
+
"dev": true,
893
+
"license": "Apache-2.0",
894
+
"optional": true,
895
+
"os": [
896
+
"linux"
897
+
],
898
+
"engines": {
899
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
900
+
},
901
+
"funding": {
902
+
"url": "https://opencollective.com/libvips"
903
+
},
904
+
"optionalDependencies": {
905
+
"@img/sharp-libvips-linux-arm64": "1.0.4"
906
+
}
907
+
},
908
+
"node_modules/@img/sharp-linux-s390x": {
909
+
"version": "0.33.5",
910
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
911
+
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
912
+
"cpu": [
913
+
"s390x"
914
+
],
915
+
"dev": true,
916
+
"license": "Apache-2.0",
917
+
"optional": true,
918
+
"os": [
919
+
"linux"
920
+
],
921
+
"engines": {
922
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
923
+
},
924
+
"funding": {
925
+
"url": "https://opencollective.com/libvips"
926
+
},
927
+
"optionalDependencies": {
928
+
"@img/sharp-libvips-linux-s390x": "1.0.4"
929
+
}
930
+
},
931
+
"node_modules/@img/sharp-linux-x64": {
932
+
"version": "0.33.5",
933
+
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
934
+
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
935
+
"cpu": [
936
+
"x64"
937
+
],
938
+
"dev": true,
939
+
"license": "Apache-2.0",
940
+
"optional": true,
941
+
"os": [
942
+
"linux"
943
+
],
944
+
"engines": {
945
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
946
+
},
947
+
"funding": {
948
+
"url": "https://opencollective.com/libvips"
949
+
},
950
+
"optionalDependencies": {
951
+
"@img/sharp-libvips-linux-x64": "1.0.4"
952
+
}
953
+
},
954
+
"node_modules/@img/sharp-linuxmusl-arm64": {
955
+
"version": "0.33.5",
956
+
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
957
+
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
958
+
"cpu": [
959
+
"arm64"
960
+
],
961
+
"dev": true,
962
+
"license": "Apache-2.0",
963
+
"optional": true,
964
+
"os": [
965
+
"linux"
966
+
],
967
+
"engines": {
968
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
969
+
},
970
+
"funding": {
971
+
"url": "https://opencollective.com/libvips"
972
+
},
973
+
"optionalDependencies": {
974
+
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
975
+
}
976
+
},
977
+
"node_modules/@img/sharp-linuxmusl-x64": {
978
+
"version": "0.33.5",
979
+
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
980
+
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
981
+
"cpu": [
982
+
"x64"
983
+
],
984
+
"dev": true,
985
+
"license": "Apache-2.0",
986
+
"optional": true,
987
+
"os": [
988
+
"linux"
989
+
],
990
+
"engines": {
991
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
992
+
},
993
+
"funding": {
994
+
"url": "https://opencollective.com/libvips"
995
+
},
996
+
"optionalDependencies": {
997
+
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
998
+
}
999
+
},
1000
+
"node_modules/@img/sharp-wasm32": {
1001
+
"version": "0.33.5",
1002
+
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
1003
+
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
1004
+
"cpu": [
1005
+
"wasm32"
1006
+
],
1007
+
"dev": true,
1008
+
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
1009
+
"optional": true,
1010
+
"dependencies": {
1011
+
"@emnapi/runtime": "^1.2.0"
1012
+
},
1013
+
"engines": {
1014
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1015
+
},
1016
+
"funding": {
1017
+
"url": "https://opencollective.com/libvips"
1018
+
}
1019
+
},
1020
+
"node_modules/@img/sharp-win32-ia32": {
1021
+
"version": "0.33.5",
1022
+
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
1023
+
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
1024
+
"cpu": [
1025
+
"ia32"
1026
+
],
1027
+
"dev": true,
1028
+
"license": "Apache-2.0 AND LGPL-3.0-or-later",
1029
+
"optional": true,
1030
+
"os": [
1031
+
"win32"
1032
+
],
1033
+
"engines": {
1034
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1035
+
},
1036
+
"funding": {
1037
+
"url": "https://opencollective.com/libvips"
1038
+
}
1039
+
},
1040
+
"node_modules/@img/sharp-win32-x64": {
1041
+
"version": "0.33.5",
1042
+
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
1043
+
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
1044
+
"cpu": [
1045
+
"x64"
1046
+
],
1047
+
"dev": true,
1048
+
"license": "Apache-2.0 AND LGPL-3.0-or-later",
1049
+
"optional": true,
1050
+
"os": [
1051
+
"win32"
1052
+
],
1053
+
"engines": {
1054
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
1055
+
},
1056
+
"funding": {
1057
+
"url": "https://opencollective.com/libvips"
1058
+
}
1059
+
},
1060
+
"node_modules/@jridgewell/resolve-uri": {
1061
+
"version": "3.1.2",
1062
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
1063
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
1064
+
"dev": true,
1065
+
"license": "MIT",
1066
+
"engines": {
1067
+
"node": ">=6.0.0"
1068
+
}
1069
+
},
1070
+
"node_modules/@jridgewell/sourcemap-codec": {
1071
+
"version": "1.5.0",
1072
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
1073
+
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
1074
+
"dev": true,
1075
+
"license": "MIT"
1076
+
},
1077
+
"node_modules/@jridgewell/trace-mapping": {
1078
+
"version": "0.3.9",
1079
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
1080
+
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
1081
+
"dev": true,
1082
+
"license": "MIT",
1083
+
"dependencies": {
1084
+
"@jridgewell/resolve-uri": "^3.0.3",
1085
+
"@jridgewell/sourcemap-codec": "^1.4.10"
1086
+
}
1087
+
},
1088
+
"node_modules/@noble/curves": {
1089
+
"version": "1.9.7",
1090
+
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
1091
+
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
1092
+
"license": "MIT",
1093
+
"dependencies": {
1094
+
"@noble/hashes": "1.8.0"
1095
+
},
1096
+
"engines": {
1097
+
"node": "^14.21.3 || >=16"
1098
+
},
1099
+
"funding": {
1100
+
"url": "https://paulmillr.com/funding/"
1101
+
}
1102
+
},
1103
+
"node_modules/@noble/hashes": {
1104
+
"version": "1.8.0",
1105
+
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
1106
+
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
1107
+
"license": "MIT",
1108
+
"engines": {
1109
+
"node": "^14.21.3 || >=16"
1110
+
},
1111
+
"funding": {
1112
+
"url": "https://paulmillr.com/funding/"
1113
+
}
1114
+
},
1115
+
"node_modules/@rollup/rollup-android-arm-eabi": {
1116
+
"version": "4.40.1",
1117
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz",
1118
+
"integrity": "sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw==",
1119
+
"cpu": [
1120
+
"arm"
1121
+
],
1122
+
"dev": true,
1123
+
"license": "MIT",
1124
+
"optional": true,
1125
+
"os": [
1126
+
"android"
1127
+
]
1128
+
},
1129
+
"node_modules/@rollup/rollup-android-arm64": {
1130
+
"version": "4.40.1",
1131
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz",
1132
+
"integrity": "sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw==",
1133
+
"cpu": [
1134
+
"arm64"
1135
+
],
1136
+
"dev": true,
1137
+
"license": "MIT",
1138
+
"optional": true,
1139
+
"os": [
1140
+
"android"
1141
+
]
1142
+
},
1143
+
"node_modules/@rollup/rollup-darwin-arm64": {
1144
+
"version": "4.40.1",
1145
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz",
1146
+
"integrity": "sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA==",
1147
+
"cpu": [
1148
+
"arm64"
1149
+
],
1150
+
"dev": true,
1151
+
"license": "MIT",
1152
+
"optional": true,
1153
+
"os": [
1154
+
"darwin"
1155
+
]
1156
+
},
1157
+
"node_modules/@rollup/rollup-darwin-x64": {
1158
+
"version": "4.40.1",
1159
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz",
1160
+
"integrity": "sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw==",
1161
+
"cpu": [
1162
+
"x64"
1163
+
],
1164
+
"dev": true,
1165
+
"license": "MIT",
1166
+
"optional": true,
1167
+
"os": [
1168
+
"darwin"
1169
+
]
1170
+
},
1171
+
"node_modules/@rollup/rollup-freebsd-arm64": {
1172
+
"version": "4.40.1",
1173
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz",
1174
+
"integrity": "sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw==",
1175
+
"cpu": [
1176
+
"arm64"
1177
+
],
1178
+
"dev": true,
1179
+
"license": "MIT",
1180
+
"optional": true,
1181
+
"os": [
1182
+
"freebsd"
1183
+
]
1184
+
},
1185
+
"node_modules/@rollup/rollup-freebsd-x64": {
1186
+
"version": "4.40.1",
1187
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz",
1188
+
"integrity": "sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q==",
1189
+
"cpu": [
1190
+
"x64"
1191
+
],
1192
+
"dev": true,
1193
+
"license": "MIT",
1194
+
"optional": true,
1195
+
"os": [
1196
+
"freebsd"
1197
+
]
1198
+
},
1199
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1200
+
"version": "4.40.1",
1201
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz",
1202
+
"integrity": "sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==",
1203
+
"cpu": [
1204
+
"arm"
1205
+
],
1206
+
"dev": true,
1207
+
"license": "MIT",
1208
+
"optional": true,
1209
+
"os": [
1210
+
"linux"
1211
+
]
1212
+
},
1213
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
1214
+
"version": "4.40.1",
1215
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz",
1216
+
"integrity": "sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==",
1217
+
"cpu": [
1218
+
"arm"
1219
+
],
1220
+
"dev": true,
1221
+
"license": "MIT",
1222
+
"optional": true,
1223
+
"os": [
1224
+
"linux"
1225
+
]
1226
+
},
1227
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
1228
+
"version": "4.40.1",
1229
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz",
1230
+
"integrity": "sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==",
1231
+
"cpu": [
1232
+
"arm64"
1233
+
],
1234
+
"dev": true,
1235
+
"license": "MIT",
1236
+
"optional": true,
1237
+
"os": [
1238
+
"linux"
1239
+
]
1240
+
},
1241
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
1242
+
"version": "4.40.1",
1243
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz",
1244
+
"integrity": "sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==",
1245
+
"cpu": [
1246
+
"arm64"
1247
+
],
1248
+
"dev": true,
1249
+
"license": "MIT",
1250
+
"optional": true,
1251
+
"os": [
1252
+
"linux"
1253
+
]
1254
+
},
1255
+
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
1256
+
"version": "4.40.1",
1257
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz",
1258
+
"integrity": "sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==",
1259
+
"cpu": [
1260
+
"loong64"
1261
+
],
1262
+
"dev": true,
1263
+
"license": "MIT",
1264
+
"optional": true,
1265
+
"os": [
1266
+
"linux"
1267
+
]
1268
+
},
1269
+
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
1270
+
"version": "4.40.1",
1271
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz",
1272
+
"integrity": "sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==",
1273
+
"cpu": [
1274
+
"ppc64"
1275
+
],
1276
+
"dev": true,
1277
+
"license": "MIT",
1278
+
"optional": true,
1279
+
"os": [
1280
+
"linux"
1281
+
]
1282
+
},
1283
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
1284
+
"version": "4.40.1",
1285
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz",
1286
+
"integrity": "sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==",
1287
+
"cpu": [
1288
+
"riscv64"
1289
+
],
1290
+
"dev": true,
1291
+
"license": "MIT",
1292
+
"optional": true,
1293
+
"os": [
1294
+
"linux"
1295
+
]
1296
+
},
1297
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
1298
+
"version": "4.40.1",
1299
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz",
1300
+
"integrity": "sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==",
1301
+
"cpu": [
1302
+
"riscv64"
1303
+
],
1304
+
"dev": true,
1305
+
"license": "MIT",
1306
+
"optional": true,
1307
+
"os": [
1308
+
"linux"
1309
+
]
1310
+
},
1311
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
1312
+
"version": "4.40.1",
1313
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz",
1314
+
"integrity": "sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==",
1315
+
"cpu": [
1316
+
"s390x"
1317
+
],
1318
+
"dev": true,
1319
+
"license": "MIT",
1320
+
"optional": true,
1321
+
"os": [
1322
+
"linux"
1323
+
]
1324
+
},
1325
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
1326
+
"version": "4.40.1",
1327
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz",
1328
+
"integrity": "sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==",
1329
+
"cpu": [
1330
+
"x64"
1331
+
],
1332
+
"dev": true,
1333
+
"license": "MIT",
1334
+
"optional": true,
1335
+
"os": [
1336
+
"linux"
1337
+
]
1338
+
},
1339
+
"node_modules/@rollup/rollup-linux-x64-musl": {
1340
+
"version": "4.40.1",
1341
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz",
1342
+
"integrity": "sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==",
1343
+
"cpu": [
1344
+
"x64"
1345
+
],
1346
+
"dev": true,
1347
+
"license": "MIT",
1348
+
"optional": true,
1349
+
"os": [
1350
+
"linux"
1351
+
]
1352
+
},
1353
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
1354
+
"version": "4.40.1",
1355
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz",
1356
+
"integrity": "sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==",
1357
+
"cpu": [
1358
+
"arm64"
1359
+
],
1360
+
"dev": true,
1361
+
"license": "MIT",
1362
+
"optional": true,
1363
+
"os": [
1364
+
"win32"
1365
+
]
1366
+
},
1367
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
1368
+
"version": "4.40.1",
1369
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz",
1370
+
"integrity": "sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA==",
1371
+
"cpu": [
1372
+
"ia32"
1373
+
],
1374
+
"dev": true,
1375
+
"license": "MIT",
1376
+
"optional": true,
1377
+
"os": [
1378
+
"win32"
1379
+
]
1380
+
},
1381
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
1382
+
"version": "4.40.1",
1383
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz",
1384
+
"integrity": "sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA==",
1385
+
"cpu": [
1386
+
"x64"
1387
+
],
1388
+
"dev": true,
1389
+
"license": "MIT",
1390
+
"optional": true,
1391
+
"os": [
1392
+
"win32"
1393
+
]
1394
+
},
1395
+
"node_modules/@types/estree": {
1396
+
"version": "1.0.7",
1397
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
1398
+
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
1399
+
"dev": true,
1400
+
"license": "MIT"
1401
+
},
1402
+
"node_modules/@vitest/expect": {
1403
+
"version": "3.0.9",
1404
+
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz",
1405
+
"integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==",
1406
+
"dev": true,
1407
+
"license": "MIT",
1408
+
"dependencies": {
1409
+
"@vitest/spy": "3.0.9",
1410
+
"@vitest/utils": "3.0.9",
1411
+
"chai": "^5.2.0",
1412
+
"tinyrainbow": "^2.0.0"
1413
+
},
1414
+
"funding": {
1415
+
"url": "https://opencollective.com/vitest"
1416
+
}
1417
+
},
1418
+
"node_modules/@vitest/mocker": {
1419
+
"version": "3.0.9",
1420
+
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz",
1421
+
"integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==",
1422
+
"dev": true,
1423
+
"license": "MIT",
1424
+
"dependencies": {
1425
+
"@vitest/spy": "3.0.9",
1426
+
"estree-walker": "^3.0.3",
1427
+
"magic-string": "^0.30.17"
1428
+
},
1429
+
"funding": {
1430
+
"url": "https://opencollective.com/vitest"
1431
+
},
1432
+
"peerDependencies": {
1433
+
"msw": "^2.4.9",
1434
+
"vite": "^5.0.0 || ^6.0.0"
1435
+
},
1436
+
"peerDependenciesMeta": {
1437
+
"msw": {
1438
+
"optional": true
1439
+
},
1440
+
"vite": {
1441
+
"optional": true
1442
+
}
1443
+
}
1444
+
},
1445
+
"node_modules/@vitest/pretty-format": {
1446
+
"version": "3.1.2",
1447
+
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
1448
+
"integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
1449
+
"dev": true,
1450
+
"license": "MIT",
1451
+
"dependencies": {
1452
+
"tinyrainbow": "^2.0.0"
1453
+
},
1454
+
"funding": {
1455
+
"url": "https://opencollective.com/vitest"
1456
+
}
1457
+
},
1458
+
"node_modules/@vitest/runner": {
1459
+
"version": "3.0.9",
1460
+
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz",
1461
+
"integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==",
1462
+
"dev": true,
1463
+
"license": "MIT",
1464
+
"dependencies": {
1465
+
"@vitest/utils": "3.0.9",
1466
+
"pathe": "^2.0.3"
1467
+
},
1468
+
"funding": {
1469
+
"url": "https://opencollective.com/vitest"
1470
+
}
1471
+
},
1472
+
"node_modules/@vitest/snapshot": {
1473
+
"version": "3.0.9",
1474
+
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz",
1475
+
"integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==",
1476
+
"dev": true,
1477
+
"license": "MIT",
1478
+
"dependencies": {
1479
+
"@vitest/pretty-format": "3.0.9",
1480
+
"magic-string": "^0.30.17",
1481
+
"pathe": "^2.0.3"
1482
+
},
1483
+
"funding": {
1484
+
"url": "https://opencollective.com/vitest"
1485
+
}
1486
+
},
1487
+
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
1488
+
"version": "3.0.9",
1489
+
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz",
1490
+
"integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==",
1491
+
"dev": true,
1492
+
"license": "MIT",
1493
+
"dependencies": {
1494
+
"tinyrainbow": "^2.0.0"
1495
+
},
1496
+
"funding": {
1497
+
"url": "https://opencollective.com/vitest"
1498
+
}
1499
+
},
1500
+
"node_modules/@vitest/spy": {
1501
+
"version": "3.0.9",
1502
+
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz",
1503
+
"integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==",
1504
+
"dev": true,
1505
+
"license": "MIT",
1506
+
"dependencies": {
1507
+
"tinyspy": "^3.0.2"
1508
+
},
1509
+
"funding": {
1510
+
"url": "https://opencollective.com/vitest"
1511
+
}
1512
+
},
1513
+
"node_modules/@vitest/utils": {
1514
+
"version": "3.0.9",
1515
+
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz",
1516
+
"integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==",
1517
+
"dev": true,
1518
+
"license": "MIT",
1519
+
"dependencies": {
1520
+
"@vitest/pretty-format": "3.0.9",
1521
+
"loupe": "^3.1.3",
1522
+
"tinyrainbow": "^2.0.0"
1523
+
},
1524
+
"funding": {
1525
+
"url": "https://opencollective.com/vitest"
1526
+
}
1527
+
},
1528
+
"node_modules/@vitest/utils/node_modules/@vitest/pretty-format": {
1529
+
"version": "3.0.9",
1530
+
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz",
1531
+
"integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==",
1532
+
"dev": true,
1533
+
"license": "MIT",
1534
+
"dependencies": {
1535
+
"tinyrainbow": "^2.0.0"
1536
+
},
1537
+
"funding": {
1538
+
"url": "https://opencollective.com/vitest"
1539
+
}
1540
+
},
1541
+
"node_modules/acorn": {
1542
+
"version": "8.14.0",
1543
+
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
1544
+
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
1545
+
"dev": true,
1546
+
"license": "MIT",
1547
+
"bin": {
1548
+
"acorn": "bin/acorn"
1549
+
},
1550
+
"engines": {
1551
+
"node": ">=0.4.0"
1552
+
}
1553
+
},
1554
+
"node_modules/acorn-walk": {
1555
+
"version": "8.3.2",
1556
+
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
1557
+
"integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
1558
+
"dev": true,
1559
+
"license": "MIT",
1560
+
"engines": {
1561
+
"node": ">=0.4.0"
1562
+
}
1563
+
},
1564
+
"node_modules/as-table": {
1565
+
"version": "1.0.55",
1566
+
"resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz",
1567
+
"integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==",
1568
+
"dev": true,
1569
+
"license": "MIT",
1570
+
"dependencies": {
1571
+
"printable-characters": "^1.0.42"
1572
+
}
1573
+
},
1574
+
"node_modules/assertion-error": {
1575
+
"version": "2.0.1",
1576
+
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
1577
+
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
1578
+
"dev": true,
1579
+
"license": "MIT",
1580
+
"engines": {
1581
+
"node": ">=12"
1582
+
}
1583
+
},
1584
+
"node_modules/birpc": {
1585
+
"version": "0.2.14",
1586
+
"resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz",
1587
+
"integrity": "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==",
1588
+
"dev": true,
1589
+
"license": "MIT",
1590
+
"funding": {
1591
+
"url": "https://github.com/sponsors/antfu"
1592
+
}
1593
+
},
1594
+
"node_modules/blake3-wasm": {
1595
+
"version": "2.1.5",
1596
+
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
1597
+
"integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
1598
+
"dev": true,
1599
+
"license": "MIT"
1600
+
},
1601
+
"node_modules/cac": {
1602
+
"version": "6.7.14",
1603
+
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
1604
+
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
1605
+
"dev": true,
1606
+
"license": "MIT",
1607
+
"engines": {
1608
+
"node": ">=8"
1609
+
}
1610
+
},
1611
+
"node_modules/chai": {
1612
+
"version": "5.2.0",
1613
+
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
1614
+
"integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==",
1615
+
"dev": true,
1616
+
"license": "MIT",
1617
+
"dependencies": {
1618
+
"assertion-error": "^2.0.1",
1619
+
"check-error": "^2.1.1",
1620
+
"deep-eql": "^5.0.1",
1621
+
"loupe": "^3.1.0",
1622
+
"pathval": "^2.0.0"
1623
+
},
1624
+
"engines": {
1625
+
"node": ">=12"
1626
+
}
1627
+
},
1628
+
"node_modules/check-error": {
1629
+
"version": "2.1.1",
1630
+
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
1631
+
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
1632
+
"dev": true,
1633
+
"license": "MIT",
1634
+
"engines": {
1635
+
"node": ">= 16"
1636
+
}
1637
+
},
1638
+
"node_modules/cjs-module-lexer": {
1639
+
"version": "1.4.3",
1640
+
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
1641
+
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
1642
+
"dev": true,
1643
+
"license": "MIT"
1644
+
},
1645
+
"node_modules/color": {
1646
+
"version": "4.2.3",
1647
+
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
1648
+
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
1649
+
"dev": true,
1650
+
"license": "MIT",
1651
+
"optional": true,
1652
+
"dependencies": {
1653
+
"color-convert": "^2.0.1",
1654
+
"color-string": "^1.9.0"
1655
+
},
1656
+
"engines": {
1657
+
"node": ">=12.5.0"
1658
+
}
1659
+
},
1660
+
"node_modules/color-convert": {
1661
+
"version": "2.0.1",
1662
+
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1663
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1664
+
"dev": true,
1665
+
"license": "MIT",
1666
+
"optional": true,
1667
+
"dependencies": {
1668
+
"color-name": "~1.1.4"
1669
+
},
1670
+
"engines": {
1671
+
"node": ">=7.0.0"
1672
+
}
1673
+
},
1674
+
"node_modules/color-name": {
1675
+
"version": "1.1.4",
1676
+
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1677
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
1678
+
"dev": true,
1679
+
"license": "MIT",
1680
+
"optional": true
1681
+
},
1682
+
"node_modules/color-string": {
1683
+
"version": "1.9.1",
1684
+
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
1685
+
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
1686
+
"dev": true,
1687
+
"license": "MIT",
1688
+
"optional": true,
1689
+
"dependencies": {
1690
+
"color-name": "^1.0.0",
1691
+
"simple-swizzle": "^0.2.2"
1692
+
}
1693
+
},
1694
+
"node_modules/cookie": {
1695
+
"version": "0.7.2",
1696
+
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
1697
+
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
1698
+
"dev": true,
1699
+
"license": "MIT",
1700
+
"engines": {
1701
+
"node": ">= 0.6"
1702
+
}
1703
+
},
1704
+
"node_modules/data-uri-to-buffer": {
1705
+
"version": "2.0.2",
1706
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
1707
+
"integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
1708
+
"dev": true,
1709
+
"license": "MIT"
1710
+
},
1711
+
"node_modules/debug": {
1712
+
"version": "4.4.0",
1713
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
1714
+
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
1715
+
"dev": true,
1716
+
"license": "MIT",
1717
+
"dependencies": {
1718
+
"ms": "^2.1.3"
1719
+
},
1720
+
"engines": {
1721
+
"node": ">=6.0"
1722
+
},
1723
+
"peerDependenciesMeta": {
1724
+
"supports-color": {
1725
+
"optional": true
1726
+
}
1727
+
}
1728
+
},
1729
+
"node_modules/deep-eql": {
1730
+
"version": "5.0.2",
1731
+
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
1732
+
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
1733
+
"dev": true,
1734
+
"license": "MIT",
1735
+
"engines": {
1736
+
"node": ">=6"
1737
+
}
1738
+
},
1739
+
"node_modules/defu": {
1740
+
"version": "6.1.4",
1741
+
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
1742
+
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
1743
+
"dev": true,
1744
+
"license": "MIT"
1745
+
},
1746
+
"node_modules/detect-libc": {
1747
+
"version": "2.0.4",
1748
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
1749
+
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
1750
+
"dev": true,
1751
+
"license": "Apache-2.0",
1752
+
"optional": true,
1753
+
"engines": {
1754
+
"node": ">=8"
1755
+
}
1756
+
},
1757
+
"node_modules/devalue": {
1758
+
"version": "4.3.3",
1759
+
"resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz",
1760
+
"integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==",
1761
+
"dev": true,
1762
+
"license": "MIT"
1763
+
},
1764
+
"node_modules/es-module-lexer": {
1765
+
"version": "1.7.0",
1766
+
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
1767
+
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
1768
+
"dev": true,
1769
+
"license": "MIT"
1770
+
},
1771
+
"node_modules/esbuild": {
1772
+
"version": "0.25.3",
1773
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz",
1774
+
"integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==",
1775
+
"dev": true,
1776
+
"hasInstallScript": true,
1777
+
"license": "MIT",
1778
+
"bin": {
1779
+
"esbuild": "bin/esbuild"
1780
+
},
1781
+
"engines": {
1782
+
"node": ">=18"
1783
+
},
1784
+
"optionalDependencies": {
1785
+
"@esbuild/aix-ppc64": "0.25.3",
1786
+
"@esbuild/android-arm": "0.25.3",
1787
+
"@esbuild/android-arm64": "0.25.3",
1788
+
"@esbuild/android-x64": "0.25.3",
1789
+
"@esbuild/darwin-arm64": "0.25.3",
1790
+
"@esbuild/darwin-x64": "0.25.3",
1791
+
"@esbuild/freebsd-arm64": "0.25.3",
1792
+
"@esbuild/freebsd-x64": "0.25.3",
1793
+
"@esbuild/linux-arm": "0.25.3",
1794
+
"@esbuild/linux-arm64": "0.25.3",
1795
+
"@esbuild/linux-ia32": "0.25.3",
1796
+
"@esbuild/linux-loong64": "0.25.3",
1797
+
"@esbuild/linux-mips64el": "0.25.3",
1798
+
"@esbuild/linux-ppc64": "0.25.3",
1799
+
"@esbuild/linux-riscv64": "0.25.3",
1800
+
"@esbuild/linux-s390x": "0.25.3",
1801
+
"@esbuild/linux-x64": "0.25.3",
1802
+
"@esbuild/netbsd-arm64": "0.25.3",
1803
+
"@esbuild/netbsd-x64": "0.25.3",
1804
+
"@esbuild/openbsd-arm64": "0.25.3",
1805
+
"@esbuild/openbsd-x64": "0.25.3",
1806
+
"@esbuild/sunos-x64": "0.25.3",
1807
+
"@esbuild/win32-arm64": "0.25.3",
1808
+
"@esbuild/win32-ia32": "0.25.3",
1809
+
"@esbuild/win32-x64": "0.25.3"
1810
+
}
1811
+
},
1812
+
"node_modules/estree-walker": {
1813
+
"version": "3.0.3",
1814
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
1815
+
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
1816
+
"dev": true,
1817
+
"license": "MIT",
1818
+
"dependencies": {
1819
+
"@types/estree": "^1.0.0"
1820
+
}
1821
+
},
1822
+
"node_modules/exit-hook": {
1823
+
"version": "2.2.1",
1824
+
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
1825
+
"integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
1826
+
"dev": true,
1827
+
"license": "MIT",
1828
+
"engines": {
1829
+
"node": ">=6"
1830
+
},
1831
+
"funding": {
1832
+
"url": "https://github.com/sponsors/sindresorhus"
1833
+
}
1834
+
},
1835
+
"node_modules/expect-type": {
1836
+
"version": "1.2.1",
1837
+
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz",
1838
+
"integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==",
1839
+
"dev": true,
1840
+
"license": "Apache-2.0",
1841
+
"engines": {
1842
+
"node": ">=12.0.0"
1843
+
}
1844
+
},
1845
+
"node_modules/exsolve": {
1846
+
"version": "1.0.5",
1847
+
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.5.tgz",
1848
+
"integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==",
1849
+
"dev": true,
1850
+
"license": "MIT"
1851
+
},
1852
+
"node_modules/fdir": {
1853
+
"version": "6.4.4",
1854
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
1855
+
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
1856
+
"dev": true,
1857
+
"license": "MIT",
1858
+
"peerDependencies": {
1859
+
"picomatch": "^3 || ^4"
1860
+
},
1861
+
"peerDependenciesMeta": {
1862
+
"picomatch": {
1863
+
"optional": true
1864
+
}
1865
+
}
1866
+
},
1867
+
"node_modules/fsevents": {
1868
+
"version": "2.3.3",
1869
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1870
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1871
+
"dev": true,
1872
+
"hasInstallScript": true,
1873
+
"license": "MIT",
1874
+
"optional": true,
1875
+
"os": [
1876
+
"darwin"
1877
+
],
1878
+
"engines": {
1879
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1880
+
}
1881
+
},
1882
+
"node_modules/get-source": {
1883
+
"version": "2.0.12",
1884
+
"resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
1885
+
"integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==",
1886
+
"dev": true,
1887
+
"license": "Unlicense",
1888
+
"dependencies": {
1889
+
"data-uri-to-buffer": "^2.0.0",
1890
+
"source-map": "^0.6.1"
1891
+
}
1892
+
},
1893
+
"node_modules/glob-to-regexp": {
1894
+
"version": "0.4.1",
1895
+
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
1896
+
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
1897
+
"dev": true,
1898
+
"license": "BSD-2-Clause"
1899
+
},
1900
+
"node_modules/is-arrayish": {
1901
+
"version": "0.3.2",
1902
+
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
1903
+
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
1904
+
"dev": true,
1905
+
"license": "MIT",
1906
+
"optional": true
1907
+
},
1908
+
"node_modules/loupe": {
1909
+
"version": "3.1.3",
1910
+
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz",
1911
+
"integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==",
1912
+
"dev": true,
1913
+
"license": "MIT"
1914
+
},
1915
+
"node_modules/magic-string": {
1916
+
"version": "0.30.17",
1917
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
1918
+
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
1919
+
"dev": true,
1920
+
"license": "MIT",
1921
+
"dependencies": {
1922
+
"@jridgewell/sourcemap-codec": "^1.5.0"
1923
+
}
1924
+
},
1925
+
"node_modules/mime": {
1926
+
"version": "3.0.0",
1927
+
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
1928
+
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
1929
+
"dev": true,
1930
+
"license": "MIT",
1931
+
"bin": {
1932
+
"mime": "cli.js"
1933
+
},
1934
+
"engines": {
1935
+
"node": ">=10.0.0"
1936
+
}
1937
+
},
1938
+
"node_modules/miniflare": {
1939
+
"version": "4.20250428.1",
1940
+
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250428.1.tgz",
1941
+
"integrity": "sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==",
1942
+
"dev": true,
1943
+
"license": "MIT",
1944
+
"dependencies": {
1945
+
"@cspotcode/source-map-support": "0.8.1",
1946
+
"acorn": "8.14.0",
1947
+
"acorn-walk": "8.3.2",
1948
+
"exit-hook": "2.2.1",
1949
+
"glob-to-regexp": "0.4.1",
1950
+
"stoppable": "1.1.0",
1951
+
"undici": "^5.28.5",
1952
+
"workerd": "1.20250428.0",
1953
+
"ws": "8.18.0",
1954
+
"youch": "3.3.4",
1955
+
"zod": "3.22.3"
1956
+
},
1957
+
"bin": {
1958
+
"miniflare": "bootstrap.js"
1959
+
},
1960
+
"engines": {
1961
+
"node": ">=18.0.0"
1962
+
}
1963
+
},
1964
+
"node_modules/miniflare/node_modules/zod": {
1965
+
"version": "3.22.3",
1966
+
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
1967
+
"integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
1968
+
"dev": true,
1969
+
"license": "MIT",
1970
+
"funding": {
1971
+
"url": "https://github.com/sponsors/colinhacks"
1972
+
}
1973
+
},
1974
+
"node_modules/ms": {
1975
+
"version": "2.1.3",
1976
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1977
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1978
+
"dev": true,
1979
+
"license": "MIT"
1980
+
},
1981
+
"node_modules/multiformats": {
1982
+
"version": "9.9.0",
1983
+
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
1984
+
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
1985
+
"license": "(Apache-2.0 AND MIT)"
1986
+
},
1987
+
"node_modules/mustache": {
1988
+
"version": "4.2.0",
1989
+
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
1990
+
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
1991
+
"dev": true,
1992
+
"license": "MIT",
1993
+
"bin": {
1994
+
"mustache": "bin/mustache"
1995
+
}
1996
+
},
1997
+
"node_modules/nanoid": {
1998
+
"version": "3.3.11",
1999
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
2000
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
2001
+
"dev": true,
2002
+
"funding": [
2003
+
{
2004
+
"type": "github",
2005
+
"url": "https://github.com/sponsors/ai"
2006
+
}
2007
+
],
2008
+
"license": "MIT",
2009
+
"bin": {
2010
+
"nanoid": "bin/nanoid.cjs"
2011
+
},
2012
+
"engines": {
2013
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2014
+
}
2015
+
},
2016
+
"node_modules/ohash": {
2017
+
"version": "2.0.11",
2018
+
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
2019
+
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
2020
+
"dev": true,
2021
+
"license": "MIT"
2022
+
},
2023
+
"node_modules/path-to-regexp": {
2024
+
"version": "6.3.0",
2025
+
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
2026
+
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
2027
+
"dev": true,
2028
+
"license": "MIT"
2029
+
},
2030
+
"node_modules/pathe": {
2031
+
"version": "2.0.3",
2032
+
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
2033
+
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
2034
+
"dev": true,
2035
+
"license": "MIT"
2036
+
},
2037
+
"node_modules/pathval": {
2038
+
"version": "2.0.0",
2039
+
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
2040
+
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
2041
+
"dev": true,
2042
+
"license": "MIT",
2043
+
"engines": {
2044
+
"node": ">= 14.16"
2045
+
}
2046
+
},
2047
+
"node_modules/picocolors": {
2048
+
"version": "1.1.1",
2049
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
2050
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
2051
+
"dev": true,
2052
+
"license": "ISC"
2053
+
},
2054
+
"node_modules/picomatch": {
2055
+
"version": "4.0.2",
2056
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
2057
+
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
2058
+
"dev": true,
2059
+
"license": "MIT",
2060
+
"engines": {
2061
+
"node": ">=12"
2062
+
},
2063
+
"funding": {
2064
+
"url": "https://github.com/sponsors/jonschlinkert"
2065
+
}
2066
+
},
2067
+
"node_modules/postcss": {
2068
+
"version": "8.5.3",
2069
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
2070
+
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
2071
+
"dev": true,
2072
+
"funding": [
2073
+
{
2074
+
"type": "opencollective",
2075
+
"url": "https://opencollective.com/postcss/"
2076
+
},
2077
+
{
2078
+
"type": "tidelift",
2079
+
"url": "https://tidelift.com/funding/github/npm/postcss"
2080
+
},
2081
+
{
2082
+
"type": "github",
2083
+
"url": "https://github.com/sponsors/ai"
2084
+
}
2085
+
],
2086
+
"license": "MIT",
2087
+
"dependencies": {
2088
+
"nanoid": "^3.3.8",
2089
+
"picocolors": "^1.1.1",
2090
+
"source-map-js": "^1.2.1"
2091
+
},
2092
+
"engines": {
2093
+
"node": "^10 || ^12 || >=14"
2094
+
}
2095
+
},
2096
+
"node_modules/printable-characters": {
2097
+
"version": "1.0.42",
2098
+
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
2099
+
"integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
2100
+
"dev": true,
2101
+
"license": "Unlicense"
2102
+
},
2103
+
"node_modules/rollup": {
2104
+
"version": "4.40.1",
2105
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.1.tgz",
2106
+
"integrity": "sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw==",
2107
+
"dev": true,
2108
+
"license": "MIT",
2109
+
"dependencies": {
2110
+
"@types/estree": "1.0.7"
2111
+
},
2112
+
"bin": {
2113
+
"rollup": "dist/bin/rollup"
2114
+
},
2115
+
"engines": {
2116
+
"node": ">=18.0.0",
2117
+
"npm": ">=8.0.0"
2118
+
},
2119
+
"optionalDependencies": {
2120
+
"@rollup/rollup-android-arm-eabi": "4.40.1",
2121
+
"@rollup/rollup-android-arm64": "4.40.1",
2122
+
"@rollup/rollup-darwin-arm64": "4.40.1",
2123
+
"@rollup/rollup-darwin-x64": "4.40.1",
2124
+
"@rollup/rollup-freebsd-arm64": "4.40.1",
2125
+
"@rollup/rollup-freebsd-x64": "4.40.1",
2126
+
"@rollup/rollup-linux-arm-gnueabihf": "4.40.1",
2127
+
"@rollup/rollup-linux-arm-musleabihf": "4.40.1",
2128
+
"@rollup/rollup-linux-arm64-gnu": "4.40.1",
2129
+
"@rollup/rollup-linux-arm64-musl": "4.40.1",
2130
+
"@rollup/rollup-linux-loongarch64-gnu": "4.40.1",
2131
+
"@rollup/rollup-linux-powerpc64le-gnu": "4.40.1",
2132
+
"@rollup/rollup-linux-riscv64-gnu": "4.40.1",
2133
+
"@rollup/rollup-linux-riscv64-musl": "4.40.1",
2134
+
"@rollup/rollup-linux-s390x-gnu": "4.40.1",
2135
+
"@rollup/rollup-linux-x64-gnu": "4.40.1",
2136
+
"@rollup/rollup-linux-x64-musl": "4.40.1",
2137
+
"@rollup/rollup-win32-arm64-msvc": "4.40.1",
2138
+
"@rollup/rollup-win32-ia32-msvc": "4.40.1",
2139
+
"@rollup/rollup-win32-x64-msvc": "4.40.1",
2140
+
"fsevents": "~2.3.2"
2141
+
}
2142
+
},
2143
+
"node_modules/semver": {
2144
+
"version": "7.7.1",
2145
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
2146
+
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
2147
+
"dev": true,
2148
+
"license": "ISC",
2149
+
"bin": {
2150
+
"semver": "bin/semver.js"
2151
+
},
2152
+
"engines": {
2153
+
"node": ">=10"
2154
+
}
2155
+
},
2156
+
"node_modules/sharp": {
2157
+
"version": "0.33.5",
2158
+
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
2159
+
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
2160
+
"dev": true,
2161
+
"hasInstallScript": true,
2162
+
"license": "Apache-2.0",
2163
+
"optional": true,
2164
+
"dependencies": {
2165
+
"color": "^4.2.3",
2166
+
"detect-libc": "^2.0.3",
2167
+
"semver": "^7.6.3"
2168
+
},
2169
+
"engines": {
2170
+
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
2171
+
},
2172
+
"funding": {
2173
+
"url": "https://opencollective.com/libvips"
2174
+
},
2175
+
"optionalDependencies": {
2176
+
"@img/sharp-darwin-arm64": "0.33.5",
2177
+
"@img/sharp-darwin-x64": "0.33.5",
2178
+
"@img/sharp-libvips-darwin-arm64": "1.0.4",
2179
+
"@img/sharp-libvips-darwin-x64": "1.0.4",
2180
+
"@img/sharp-libvips-linux-arm": "1.0.5",
2181
+
"@img/sharp-libvips-linux-arm64": "1.0.4",
2182
+
"@img/sharp-libvips-linux-s390x": "1.0.4",
2183
+
"@img/sharp-libvips-linux-x64": "1.0.4",
2184
+
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
2185
+
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
2186
+
"@img/sharp-linux-arm": "0.33.5",
2187
+
"@img/sharp-linux-arm64": "0.33.5",
2188
+
"@img/sharp-linux-s390x": "0.33.5",
2189
+
"@img/sharp-linux-x64": "0.33.5",
2190
+
"@img/sharp-linuxmusl-arm64": "0.33.5",
2191
+
"@img/sharp-linuxmusl-x64": "0.33.5",
2192
+
"@img/sharp-wasm32": "0.33.5",
2193
+
"@img/sharp-win32-ia32": "0.33.5",
2194
+
"@img/sharp-win32-x64": "0.33.5"
2195
+
}
2196
+
},
2197
+
"node_modules/siginfo": {
2198
+
"version": "2.0.0",
2199
+
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
2200
+
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
2201
+
"dev": true,
2202
+
"license": "ISC"
2203
+
},
2204
+
"node_modules/simple-swizzle": {
2205
+
"version": "0.2.2",
2206
+
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
2207
+
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
2208
+
"dev": true,
2209
+
"license": "MIT",
2210
+
"optional": true,
2211
+
"dependencies": {
2212
+
"is-arrayish": "^0.3.1"
2213
+
}
2214
+
},
2215
+
"node_modules/source-map": {
2216
+
"version": "0.6.1",
2217
+
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
2218
+
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
2219
+
"dev": true,
2220
+
"license": "BSD-3-Clause",
2221
+
"engines": {
2222
+
"node": ">=0.10.0"
2223
+
}
2224
+
},
2225
+
"node_modules/source-map-js": {
2226
+
"version": "1.2.1",
2227
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2228
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2229
+
"dev": true,
2230
+
"license": "BSD-3-Clause",
2231
+
"engines": {
2232
+
"node": ">=0.10.0"
2233
+
}
2234
+
},
2235
+
"node_modules/stackback": {
2236
+
"version": "0.0.2",
2237
+
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
2238
+
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
2239
+
"dev": true,
2240
+
"license": "MIT"
2241
+
},
2242
+
"node_modules/stacktracey": {
2243
+
"version": "2.1.8",
2244
+
"resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz",
2245
+
"integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==",
2246
+
"dev": true,
2247
+
"license": "Unlicense",
2248
+
"dependencies": {
2249
+
"as-table": "^1.0.36",
2250
+
"get-source": "^2.0.12"
2251
+
}
2252
+
},
2253
+
"node_modules/std-env": {
2254
+
"version": "3.9.0",
2255
+
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
2256
+
"integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
2257
+
"dev": true,
2258
+
"license": "MIT"
2259
+
},
2260
+
"node_modules/stoppable": {
2261
+
"version": "1.1.0",
2262
+
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
2263
+
"integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
2264
+
"dev": true,
2265
+
"license": "MIT",
2266
+
"engines": {
2267
+
"node": ">=4",
2268
+
"npm": ">=6"
2269
+
}
2270
+
},
2271
+
"node_modules/tinybench": {
2272
+
"version": "2.9.0",
2273
+
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
2274
+
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
2275
+
"dev": true,
2276
+
"license": "MIT"
2277
+
},
2278
+
"node_modules/tinyexec": {
2279
+
"version": "0.3.2",
2280
+
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
2281
+
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
2282
+
"dev": true,
2283
+
"license": "MIT"
2284
+
},
2285
+
"node_modules/tinyglobby": {
2286
+
"version": "0.2.13",
2287
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
2288
+
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
2289
+
"dev": true,
2290
+
"license": "MIT",
2291
+
"dependencies": {
2292
+
"fdir": "^6.4.4",
2293
+
"picomatch": "^4.0.2"
2294
+
},
2295
+
"engines": {
2296
+
"node": ">=12.0.0"
2297
+
},
2298
+
"funding": {
2299
+
"url": "https://github.com/sponsors/SuperchupuDev"
2300
+
}
2301
+
},
2302
+
"node_modules/tinypool": {
2303
+
"version": "1.0.2",
2304
+
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
2305
+
"integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==",
2306
+
"dev": true,
2307
+
"license": "MIT",
2308
+
"engines": {
2309
+
"node": "^18.0.0 || >=20.0.0"
2310
+
}
2311
+
},
2312
+
"node_modules/tinyrainbow": {
2313
+
"version": "2.0.0",
2314
+
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
2315
+
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
2316
+
"dev": true,
2317
+
"license": "MIT",
2318
+
"engines": {
2319
+
"node": ">=14.0.0"
2320
+
}
2321
+
},
2322
+
"node_modules/tinyspy": {
2323
+
"version": "3.0.2",
2324
+
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
2325
+
"integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
2326
+
"dev": true,
2327
+
"license": "MIT",
2328
+
"engines": {
2329
+
"node": ">=14.0.0"
2330
+
}
2331
+
},
2332
+
"node_modules/tslib": {
2333
+
"version": "2.8.1",
2334
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
2335
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
2336
+
"license": "0BSD"
2337
+
},
2338
+
"node_modules/ufo": {
2339
+
"version": "1.6.1",
2340
+
"resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
2341
+
"integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
2342
+
"dev": true,
2343
+
"license": "MIT"
2344
+
},
2345
+
"node_modules/uint8arrays": {
2346
+
"version": "3.0.0",
2347
+
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
2348
+
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
2349
+
"license": "MIT",
2350
+
"dependencies": {
2351
+
"multiformats": "^9.4.2"
2352
+
}
2353
+
},
2354
+
"node_modules/undici": {
2355
+
"version": "5.29.0",
2356
+
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
2357
+
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
2358
+
"dev": true,
2359
+
"license": "MIT",
2360
+
"dependencies": {
2361
+
"@fastify/busboy": "^2.0.0"
2362
+
},
2363
+
"engines": {
2364
+
"node": ">=14.0"
2365
+
}
2366
+
},
2367
+
"node_modules/unenv": {
2368
+
"version": "2.0.0-rc.15",
2369
+
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.15.tgz",
2370
+
"integrity": "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==",
2371
+
"dev": true,
2372
+
"license": "MIT",
2373
+
"dependencies": {
2374
+
"defu": "^6.1.4",
2375
+
"exsolve": "^1.0.4",
2376
+
"ohash": "^2.0.11",
2377
+
"pathe": "^2.0.3",
2378
+
"ufo": "^1.5.4"
2379
+
}
2380
+
},
2381
+
"node_modules/unicode-segmenter": {
2382
+
"version": "0.14.4",
2383
+
"resolved": "https://registry.npmjs.org/unicode-segmenter/-/unicode-segmenter-0.14.4.tgz",
2384
+
"integrity": "sha512-pR5VCiCrLrKOL6FRW61jnk9+wyMtKKowq+jyFY9oc6uHbWKhDL4yVRiI4YZPksGMK72Pahh8m0cn/0JvbDDyJg==",
2385
+
"license": "MIT"
2386
+
},
2387
+
"node_modules/vite": {
2388
+
"version": "6.3.4",
2389
+
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
2390
+
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
2391
+
"dev": true,
2392
+
"license": "MIT",
2393
+
"dependencies": {
2394
+
"esbuild": "^0.25.0",
2395
+
"fdir": "^6.4.4",
2396
+
"picomatch": "^4.0.2",
2397
+
"postcss": "^8.5.3",
2398
+
"rollup": "^4.34.9",
2399
+
"tinyglobby": "^0.2.13"
2400
+
},
2401
+
"bin": {
2402
+
"vite": "bin/vite.js"
2403
+
},
2404
+
"engines": {
2405
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2406
+
},
2407
+
"funding": {
2408
+
"url": "https://github.com/vitejs/vite?sponsor=1"
2409
+
},
2410
+
"optionalDependencies": {
2411
+
"fsevents": "~2.3.3"
2412
+
},
2413
+
"peerDependencies": {
2414
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2415
+
"jiti": ">=1.21.0",
2416
+
"less": "*",
2417
+
"lightningcss": "^1.21.0",
2418
+
"sass": "*",
2419
+
"sass-embedded": "*",
2420
+
"stylus": "*",
2421
+
"sugarss": "*",
2422
+
"terser": "^5.16.0",
2423
+
"tsx": "^4.8.1",
2424
+
"yaml": "^2.4.2"
2425
+
},
2426
+
"peerDependenciesMeta": {
2427
+
"@types/node": {
2428
+
"optional": true
2429
+
},
2430
+
"jiti": {
2431
+
"optional": true
2432
+
},
2433
+
"less": {
2434
+
"optional": true
2435
+
},
2436
+
"lightningcss": {
2437
+
"optional": true
2438
+
},
2439
+
"sass": {
2440
+
"optional": true
2441
+
},
2442
+
"sass-embedded": {
2443
+
"optional": true
2444
+
},
2445
+
"stylus": {
2446
+
"optional": true
2447
+
},
2448
+
"sugarss": {
2449
+
"optional": true
2450
+
},
2451
+
"terser": {
2452
+
"optional": true
2453
+
},
2454
+
"tsx": {
2455
+
"optional": true
2456
+
},
2457
+
"yaml": {
2458
+
"optional": true
2459
+
}
2460
+
}
2461
+
},
2462
+
"node_modules/vite-node": {
2463
+
"version": "3.0.9",
2464
+
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz",
2465
+
"integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==",
2466
+
"dev": true,
2467
+
"license": "MIT",
2468
+
"dependencies": {
2469
+
"cac": "^6.7.14",
2470
+
"debug": "^4.4.0",
2471
+
"es-module-lexer": "^1.6.0",
2472
+
"pathe": "^2.0.3",
2473
+
"vite": "^5.0.0 || ^6.0.0"
2474
+
},
2475
+
"bin": {
2476
+
"vite-node": "vite-node.mjs"
2477
+
},
2478
+
"engines": {
2479
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2480
+
},
2481
+
"funding": {
2482
+
"url": "https://opencollective.com/vitest"
2483
+
}
2484
+
},
2485
+
"node_modules/vitest": {
2486
+
"version": "3.0.9",
2487
+
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz",
2488
+
"integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==",
2489
+
"dev": true,
2490
+
"license": "MIT",
2491
+
"dependencies": {
2492
+
"@vitest/expect": "3.0.9",
2493
+
"@vitest/mocker": "3.0.9",
2494
+
"@vitest/pretty-format": "^3.0.9",
2495
+
"@vitest/runner": "3.0.9",
2496
+
"@vitest/snapshot": "3.0.9",
2497
+
"@vitest/spy": "3.0.9",
2498
+
"@vitest/utils": "3.0.9",
2499
+
"chai": "^5.2.0",
2500
+
"debug": "^4.4.0",
2501
+
"expect-type": "^1.1.0",
2502
+
"magic-string": "^0.30.17",
2503
+
"pathe": "^2.0.3",
2504
+
"std-env": "^3.8.0",
2505
+
"tinybench": "^2.9.0",
2506
+
"tinyexec": "^0.3.2",
2507
+
"tinypool": "^1.0.2",
2508
+
"tinyrainbow": "^2.0.0",
2509
+
"vite": "^5.0.0 || ^6.0.0",
2510
+
"vite-node": "3.0.9",
2511
+
"why-is-node-running": "^2.3.0"
2512
+
},
2513
+
"bin": {
2514
+
"vitest": "vitest.mjs"
2515
+
},
2516
+
"engines": {
2517
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2518
+
},
2519
+
"funding": {
2520
+
"url": "https://opencollective.com/vitest"
2521
+
},
2522
+
"peerDependencies": {
2523
+
"@edge-runtime/vm": "*",
2524
+
"@types/debug": "^4.1.12",
2525
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2526
+
"@vitest/browser": "3.0.9",
2527
+
"@vitest/ui": "3.0.9",
2528
+
"happy-dom": "*",
2529
+
"jsdom": "*"
2530
+
},
2531
+
"peerDependenciesMeta": {
2532
+
"@edge-runtime/vm": {
2533
+
"optional": true
2534
+
},
2535
+
"@types/debug": {
2536
+
"optional": true
2537
+
},
2538
+
"@types/node": {
2539
+
"optional": true
2540
+
},
2541
+
"@vitest/browser": {
2542
+
"optional": true
2543
+
},
2544
+
"@vitest/ui": {
2545
+
"optional": true
2546
+
},
2547
+
"happy-dom": {
2548
+
"optional": true
2549
+
},
2550
+
"jsdom": {
2551
+
"optional": true
2552
+
}
2553
+
}
2554
+
},
2555
+
"node_modules/why-is-node-running": {
2556
+
"version": "2.3.0",
2557
+
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
2558
+
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
2559
+
"dev": true,
2560
+
"license": "MIT",
2561
+
"dependencies": {
2562
+
"siginfo": "^2.0.0",
2563
+
"stackback": "0.0.2"
2564
+
},
2565
+
"bin": {
2566
+
"why-is-node-running": "cli.js"
2567
+
},
2568
+
"engines": {
2569
+
"node": ">=8"
2570
+
}
2571
+
},
2572
+
"node_modules/workerd": {
2573
+
"version": "1.20250428.0",
2574
+
"resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250428.0.tgz",
2575
+
"integrity": "sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==",
2576
+
"dev": true,
2577
+
"hasInstallScript": true,
2578
+
"license": "Apache-2.0",
2579
+
"bin": {
2580
+
"workerd": "bin/workerd"
2581
+
},
2582
+
"engines": {
2583
+
"node": ">=16"
2584
+
},
2585
+
"optionalDependencies": {
2586
+
"@cloudflare/workerd-darwin-64": "1.20250428.0",
2587
+
"@cloudflare/workerd-darwin-arm64": "1.20250428.0",
2588
+
"@cloudflare/workerd-linux-64": "1.20250428.0",
2589
+
"@cloudflare/workerd-linux-arm64": "1.20250428.0",
2590
+
"@cloudflare/workerd-windows-64": "1.20250428.0"
2591
+
}
2592
+
},
2593
+
"node_modules/wrangler": {
2594
+
"version": "4.14.1",
2595
+
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.14.1.tgz",
2596
+
"integrity": "sha512-EU7IThP7i68TBftJJSveogvWZ5k/WRijcJh3UclDWiWWhDZTPbL6LOJEFhHKqFzHOaC4Y2Aewt48rfTz0e7oCw==",
2597
+
"dev": true,
2598
+
"license": "MIT OR Apache-2.0",
2599
+
"dependencies": {
2600
+
"@cloudflare/kv-asset-handler": "0.4.0",
2601
+
"@cloudflare/unenv-preset": "2.3.1",
2602
+
"blake3-wasm": "2.1.5",
2603
+
"esbuild": "0.25.2",
2604
+
"miniflare": "4.20250428.1",
2605
+
"path-to-regexp": "6.3.0",
2606
+
"unenv": "2.0.0-rc.15",
2607
+
"workerd": "1.20250428.0"
2608
+
},
2609
+
"bin": {
2610
+
"wrangler": "bin/wrangler.js",
2611
+
"wrangler2": "bin/wrangler.js"
2612
+
},
2613
+
"engines": {
2614
+
"node": ">=18.0.0"
2615
+
},
2616
+
"optionalDependencies": {
2617
+
"fsevents": "~2.3.2",
2618
+
"sharp": "^0.33.5"
2619
+
},
2620
+
"peerDependencies": {
2621
+
"@cloudflare/workers-types": "^4.20250428.0"
2622
+
},
2623
+
"peerDependenciesMeta": {
2624
+
"@cloudflare/workers-types": {
2625
+
"optional": true
2626
+
}
2627
+
}
2628
+
},
2629
+
"node_modules/wrangler/node_modules/@esbuild/aix-ppc64": {
2630
+
"version": "0.25.2",
2631
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
2632
+
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
2633
+
"cpu": [
2634
+
"ppc64"
2635
+
],
2636
+
"dev": true,
2637
+
"license": "MIT",
2638
+
"optional": true,
2639
+
"os": [
2640
+
"aix"
2641
+
],
2642
+
"engines": {
2643
+
"node": ">=18"
2644
+
}
2645
+
},
2646
+
"node_modules/wrangler/node_modules/@esbuild/android-arm": {
2647
+
"version": "0.25.2",
2648
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
2649
+
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
2650
+
"cpu": [
2651
+
"arm"
2652
+
],
2653
+
"dev": true,
2654
+
"license": "MIT",
2655
+
"optional": true,
2656
+
"os": [
2657
+
"android"
2658
+
],
2659
+
"engines": {
2660
+
"node": ">=18"
2661
+
}
2662
+
},
2663
+
"node_modules/wrangler/node_modules/@esbuild/android-arm64": {
2664
+
"version": "0.25.2",
2665
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
2666
+
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
2667
+
"cpu": [
2668
+
"arm64"
2669
+
],
2670
+
"dev": true,
2671
+
"license": "MIT",
2672
+
"optional": true,
2673
+
"os": [
2674
+
"android"
2675
+
],
2676
+
"engines": {
2677
+
"node": ">=18"
2678
+
}
2679
+
},
2680
+
"node_modules/wrangler/node_modules/@esbuild/android-x64": {
2681
+
"version": "0.25.2",
2682
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
2683
+
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
2684
+
"cpu": [
2685
+
"x64"
2686
+
],
2687
+
"dev": true,
2688
+
"license": "MIT",
2689
+
"optional": true,
2690
+
"os": [
2691
+
"android"
2692
+
],
2693
+
"engines": {
2694
+
"node": ">=18"
2695
+
}
2696
+
},
2697
+
"node_modules/wrangler/node_modules/@esbuild/darwin-arm64": {
2698
+
"version": "0.25.2",
2699
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
2700
+
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
2701
+
"cpu": [
2702
+
"arm64"
2703
+
],
2704
+
"dev": true,
2705
+
"license": "MIT",
2706
+
"optional": true,
2707
+
"os": [
2708
+
"darwin"
2709
+
],
2710
+
"engines": {
2711
+
"node": ">=18"
2712
+
}
2713
+
},
2714
+
"node_modules/wrangler/node_modules/@esbuild/darwin-x64": {
2715
+
"version": "0.25.2",
2716
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
2717
+
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
2718
+
"cpu": [
2719
+
"x64"
2720
+
],
2721
+
"dev": true,
2722
+
"license": "MIT",
2723
+
"optional": true,
2724
+
"os": [
2725
+
"darwin"
2726
+
],
2727
+
"engines": {
2728
+
"node": ">=18"
2729
+
}
2730
+
},
2731
+
"node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": {
2732
+
"version": "0.25.2",
2733
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
2734
+
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
2735
+
"cpu": [
2736
+
"arm64"
2737
+
],
2738
+
"dev": true,
2739
+
"license": "MIT",
2740
+
"optional": true,
2741
+
"os": [
2742
+
"freebsd"
2743
+
],
2744
+
"engines": {
2745
+
"node": ">=18"
2746
+
}
2747
+
},
2748
+
"node_modules/wrangler/node_modules/@esbuild/freebsd-x64": {
2749
+
"version": "0.25.2",
2750
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
2751
+
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
2752
+
"cpu": [
2753
+
"x64"
2754
+
],
2755
+
"dev": true,
2756
+
"license": "MIT",
2757
+
"optional": true,
2758
+
"os": [
2759
+
"freebsd"
2760
+
],
2761
+
"engines": {
2762
+
"node": ">=18"
2763
+
}
2764
+
},
2765
+
"node_modules/wrangler/node_modules/@esbuild/linux-arm": {
2766
+
"version": "0.25.2",
2767
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
2768
+
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
2769
+
"cpu": [
2770
+
"arm"
2771
+
],
2772
+
"dev": true,
2773
+
"license": "MIT",
2774
+
"optional": true,
2775
+
"os": [
2776
+
"linux"
2777
+
],
2778
+
"engines": {
2779
+
"node": ">=18"
2780
+
}
2781
+
},
2782
+
"node_modules/wrangler/node_modules/@esbuild/linux-arm64": {
2783
+
"version": "0.25.2",
2784
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
2785
+
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
2786
+
"cpu": [
2787
+
"arm64"
2788
+
],
2789
+
"dev": true,
2790
+
"license": "MIT",
2791
+
"optional": true,
2792
+
"os": [
2793
+
"linux"
2794
+
],
2795
+
"engines": {
2796
+
"node": ">=18"
2797
+
}
2798
+
},
2799
+
"node_modules/wrangler/node_modules/@esbuild/linux-ia32": {
2800
+
"version": "0.25.2",
2801
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
2802
+
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
2803
+
"cpu": [
2804
+
"ia32"
2805
+
],
2806
+
"dev": true,
2807
+
"license": "MIT",
2808
+
"optional": true,
2809
+
"os": [
2810
+
"linux"
2811
+
],
2812
+
"engines": {
2813
+
"node": ">=18"
2814
+
}
2815
+
},
2816
+
"node_modules/wrangler/node_modules/@esbuild/linux-loong64": {
2817
+
"version": "0.25.2",
2818
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
2819
+
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
2820
+
"cpu": [
2821
+
"loong64"
2822
+
],
2823
+
"dev": true,
2824
+
"license": "MIT",
2825
+
"optional": true,
2826
+
"os": [
2827
+
"linux"
2828
+
],
2829
+
"engines": {
2830
+
"node": ">=18"
2831
+
}
2832
+
},
2833
+
"node_modules/wrangler/node_modules/@esbuild/linux-mips64el": {
2834
+
"version": "0.25.2",
2835
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
2836
+
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
2837
+
"cpu": [
2838
+
"mips64el"
2839
+
],
2840
+
"dev": true,
2841
+
"license": "MIT",
2842
+
"optional": true,
2843
+
"os": [
2844
+
"linux"
2845
+
],
2846
+
"engines": {
2847
+
"node": ">=18"
2848
+
}
2849
+
},
2850
+
"node_modules/wrangler/node_modules/@esbuild/linux-ppc64": {
2851
+
"version": "0.25.2",
2852
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
2853
+
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
2854
+
"cpu": [
2855
+
"ppc64"
2856
+
],
2857
+
"dev": true,
2858
+
"license": "MIT",
2859
+
"optional": true,
2860
+
"os": [
2861
+
"linux"
2862
+
],
2863
+
"engines": {
2864
+
"node": ">=18"
2865
+
}
2866
+
},
2867
+
"node_modules/wrangler/node_modules/@esbuild/linux-riscv64": {
2868
+
"version": "0.25.2",
2869
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
2870
+
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
2871
+
"cpu": [
2872
+
"riscv64"
2873
+
],
2874
+
"dev": true,
2875
+
"license": "MIT",
2876
+
"optional": true,
2877
+
"os": [
2878
+
"linux"
2879
+
],
2880
+
"engines": {
2881
+
"node": ">=18"
2882
+
}
2883
+
},
2884
+
"node_modules/wrangler/node_modules/@esbuild/linux-s390x": {
2885
+
"version": "0.25.2",
2886
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
2887
+
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
2888
+
"cpu": [
2889
+
"s390x"
2890
+
],
2891
+
"dev": true,
2892
+
"license": "MIT",
2893
+
"optional": true,
2894
+
"os": [
2895
+
"linux"
2896
+
],
2897
+
"engines": {
2898
+
"node": ">=18"
2899
+
}
2900
+
},
2901
+
"node_modules/wrangler/node_modules/@esbuild/linux-x64": {
2902
+
"version": "0.25.2",
2903
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
2904
+
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
2905
+
"cpu": [
2906
+
"x64"
2907
+
],
2908
+
"dev": true,
2909
+
"license": "MIT",
2910
+
"optional": true,
2911
+
"os": [
2912
+
"linux"
2913
+
],
2914
+
"engines": {
2915
+
"node": ">=18"
2916
+
}
2917
+
},
2918
+
"node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": {
2919
+
"version": "0.25.2",
2920
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
2921
+
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
2922
+
"cpu": [
2923
+
"arm64"
2924
+
],
2925
+
"dev": true,
2926
+
"license": "MIT",
2927
+
"optional": true,
2928
+
"os": [
2929
+
"netbsd"
2930
+
],
2931
+
"engines": {
2932
+
"node": ">=18"
2933
+
}
2934
+
},
2935
+
"node_modules/wrangler/node_modules/@esbuild/netbsd-x64": {
2936
+
"version": "0.25.2",
2937
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
2938
+
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
2939
+
"cpu": [
2940
+
"x64"
2941
+
],
2942
+
"dev": true,
2943
+
"license": "MIT",
2944
+
"optional": true,
2945
+
"os": [
2946
+
"netbsd"
2947
+
],
2948
+
"engines": {
2949
+
"node": ">=18"
2950
+
}
2951
+
},
2952
+
"node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": {
2953
+
"version": "0.25.2",
2954
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
2955
+
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
2956
+
"cpu": [
2957
+
"arm64"
2958
+
],
2959
+
"dev": true,
2960
+
"license": "MIT",
2961
+
"optional": true,
2962
+
"os": [
2963
+
"openbsd"
2964
+
],
2965
+
"engines": {
2966
+
"node": ">=18"
2967
+
}
2968
+
},
2969
+
"node_modules/wrangler/node_modules/@esbuild/openbsd-x64": {
2970
+
"version": "0.25.2",
2971
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
2972
+
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
2973
+
"cpu": [
2974
+
"x64"
2975
+
],
2976
+
"dev": true,
2977
+
"license": "MIT",
2978
+
"optional": true,
2979
+
"os": [
2980
+
"openbsd"
2981
+
],
2982
+
"engines": {
2983
+
"node": ">=18"
2984
+
}
2985
+
},
2986
+
"node_modules/wrangler/node_modules/@esbuild/sunos-x64": {
2987
+
"version": "0.25.2",
2988
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
2989
+
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
2990
+
"cpu": [
2991
+
"x64"
2992
+
],
2993
+
"dev": true,
2994
+
"license": "MIT",
2995
+
"optional": true,
2996
+
"os": [
2997
+
"sunos"
2998
+
],
2999
+
"engines": {
3000
+
"node": ">=18"
3001
+
}
3002
+
},
3003
+
"node_modules/wrangler/node_modules/@esbuild/win32-arm64": {
3004
+
"version": "0.25.2",
3005
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
3006
+
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
3007
+
"cpu": [
3008
+
"arm64"
3009
+
],
3010
+
"dev": true,
3011
+
"license": "MIT",
3012
+
"optional": true,
3013
+
"os": [
3014
+
"win32"
3015
+
],
3016
+
"engines": {
3017
+
"node": ">=18"
3018
+
}
3019
+
},
3020
+
"node_modules/wrangler/node_modules/@esbuild/win32-ia32": {
3021
+
"version": "0.25.2",
3022
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
3023
+
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
3024
+
"cpu": [
3025
+
"ia32"
3026
+
],
3027
+
"dev": true,
3028
+
"license": "MIT",
3029
+
"optional": true,
3030
+
"os": [
3031
+
"win32"
3032
+
],
3033
+
"engines": {
3034
+
"node": ">=18"
3035
+
}
3036
+
},
3037
+
"node_modules/wrangler/node_modules/@esbuild/win32-x64": {
3038
+
"version": "0.25.2",
3039
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
3040
+
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
3041
+
"cpu": [
3042
+
"x64"
3043
+
],
3044
+
"dev": true,
3045
+
"license": "MIT",
3046
+
"optional": true,
3047
+
"os": [
3048
+
"win32"
3049
+
],
3050
+
"engines": {
3051
+
"node": ">=18"
3052
+
}
3053
+
},
3054
+
"node_modules/wrangler/node_modules/esbuild": {
3055
+
"version": "0.25.2",
3056
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
3057
+
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
3058
+
"dev": true,
3059
+
"hasInstallScript": true,
3060
+
"license": "MIT",
3061
+
"bin": {
3062
+
"esbuild": "bin/esbuild"
3063
+
},
3064
+
"engines": {
3065
+
"node": ">=18"
3066
+
},
3067
+
"optionalDependencies": {
3068
+
"@esbuild/aix-ppc64": "0.25.2",
3069
+
"@esbuild/android-arm": "0.25.2",
3070
+
"@esbuild/android-arm64": "0.25.2",
3071
+
"@esbuild/android-x64": "0.25.2",
3072
+
"@esbuild/darwin-arm64": "0.25.2",
3073
+
"@esbuild/darwin-x64": "0.25.2",
3074
+
"@esbuild/freebsd-arm64": "0.25.2",
3075
+
"@esbuild/freebsd-x64": "0.25.2",
3076
+
"@esbuild/linux-arm": "0.25.2",
3077
+
"@esbuild/linux-arm64": "0.25.2",
3078
+
"@esbuild/linux-ia32": "0.25.2",
3079
+
"@esbuild/linux-loong64": "0.25.2",
3080
+
"@esbuild/linux-mips64el": "0.25.2",
3081
+
"@esbuild/linux-ppc64": "0.25.2",
3082
+
"@esbuild/linux-riscv64": "0.25.2",
3083
+
"@esbuild/linux-s390x": "0.25.2",
3084
+
"@esbuild/linux-x64": "0.25.2",
3085
+
"@esbuild/netbsd-arm64": "0.25.2",
3086
+
"@esbuild/netbsd-x64": "0.25.2",
3087
+
"@esbuild/openbsd-arm64": "0.25.2",
3088
+
"@esbuild/openbsd-x64": "0.25.2",
3089
+
"@esbuild/sunos-x64": "0.25.2",
3090
+
"@esbuild/win32-arm64": "0.25.2",
3091
+
"@esbuild/win32-ia32": "0.25.2",
3092
+
"@esbuild/win32-x64": "0.25.2"
3093
+
}
3094
+
},
3095
+
"node_modules/ws": {
3096
+
"version": "8.18.0",
3097
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
3098
+
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
3099
+
"dev": true,
3100
+
"license": "MIT",
3101
+
"engines": {
3102
+
"node": ">=10.0.0"
3103
+
},
3104
+
"peerDependencies": {
3105
+
"bufferutil": "^4.0.1",
3106
+
"utf-8-validate": ">=5.0.2"
3107
+
},
3108
+
"peerDependenciesMeta": {
3109
+
"bufferutil": {
3110
+
"optional": true
3111
+
},
3112
+
"utf-8-validate": {
3113
+
"optional": true
3114
+
}
3115
+
}
3116
+
},
3117
+
"node_modules/youch": {
3118
+
"version": "3.3.4",
3119
+
"resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz",
3120
+
"integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==",
3121
+
"dev": true,
3122
+
"license": "MIT",
3123
+
"dependencies": {
3124
+
"cookie": "^0.7.1",
3125
+
"mustache": "^4.2.0",
3126
+
"stacktracey": "^2.1.8"
3127
+
}
3128
+
},
3129
+
"node_modules/zod": {
3130
+
"version": "3.24.3",
3131
+
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz",
3132
+
"integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==",
3133
+
"license": "MIT",
3134
+
"funding": {
3135
+
"url": "https://github.com/sponsors/colinhacks"
3136
+
}
3137
+
}
3138
+
}
3024
3139
}
+17
-14
avatar/package.json
+17
-14
avatar/package.json
···
1
1
{
2
-
"name": "avatar",
3
-
"version": "0.0.0",
4
-
"private": true,
5
-
"scripts": {
6
-
"deploy": "wrangler deploy",
7
-
"dev": "wrangler dev",
8
-
"start": "wrangler dev",
9
-
"test": "vitest"
10
-
},
11
-
"devDependencies": {
12
-
"@cloudflare/vitest-pool-workers": "^0.8.19",
13
-
"vitest": "~3.0.7",
14
-
"wrangler": "^4.14.1"
15
-
}
2
+
"name": "avatar",
3
+
"version": "0.0.0",
4
+
"private": true,
5
+
"scripts": {
6
+
"deploy": "wrangler deploy",
7
+
"dev": "wrangler dev",
8
+
"start": "wrangler dev",
9
+
"test": "vitest"
10
+
},
11
+
"dependencies": {
12
+
"@atproto/identity": "^0.4.1"
13
+
},
14
+
"devDependencies": {
15
+
"@cloudflare/vitest-pool-workers": "^0.8.19",
16
+
"vitest": "~3.0.7",
17
+
"wrangler": "^4.14.1"
18
+
}
16
19
}
+121
-9
avatar/src/index.js
+121
-9
avatar/src/index.js
···
1
+
import { IdResolver } from "@atproto/identity";
2
+
1
3
export default {
2
4
async fetch(request, env) {
3
5
// Helper function to generate a color from a string
···
14
16
return color;
15
17
};
16
18
19
+
// Helper function to fetch Tangled profile from PDS
20
+
const getTangledAvatarFromPDS = async (actor, resolver) => {
21
+
try {
22
+
// Resolve the identity
23
+
const identity = await resolver.resolve(actor);
24
+
if (!identity) {
25
+
console.log({
26
+
level: "debug",
27
+
message: "failed to resolve identity",
28
+
actor: actor,
29
+
});
30
+
return null;
31
+
}
32
+
33
+
const did = identity.did;
34
+
const pdsEndpoint = identity.pdsUrl;
35
+
36
+
if (!pdsEndpoint) {
37
+
console.log({
38
+
level: "debug",
39
+
message: "no PDS endpoint found",
40
+
actor: actor,
41
+
did: did,
42
+
});
43
+
return null;
44
+
}
45
+
46
+
console.log({
47
+
level: "debug",
48
+
message: "fetching Tangled profile from PDS",
49
+
actor: actor,
50
+
did: did,
51
+
pdsEndpoint: pdsEndpoint,
52
+
});
53
+
54
+
// Fetch the Tangled profile record from PDS
55
+
const profileResponse = await fetch(
56
+
`${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${did}&collection=org.tangled.actor.profile&rkey=self`,
57
+
);
58
+
59
+
if (!profileResponse.ok) {
60
+
console.log({
61
+
level: "debug",
62
+
message: "no Tangled profile found on PDS",
63
+
actor: actor,
64
+
status: profileResponse.status,
65
+
});
66
+
return null;
67
+
}
68
+
69
+
const profileData = await profileResponse.json();
70
+
const avatarCID = profileData?.value?.avatar;
71
+
72
+
if (!avatarCID) {
73
+
console.log({
74
+
level: "debug",
75
+
message: "Tangled profile has no avatar CID",
76
+
actor: actor,
77
+
});
78
+
return null;
79
+
}
80
+
81
+
// Construct blob URL
82
+
const blobUrl = `${pdsEndpoint}/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${avatarCID}`;
83
+
84
+
console.log({
85
+
level: "debug",
86
+
message: "found Tangled avatar",
87
+
actor: actor,
88
+
avatarCID: avatarCID,
89
+
});
90
+
91
+
return blobUrl;
92
+
} catch (e) {
93
+
console.log({
94
+
level: "warn",
95
+
message: "error fetching Tangled avatar from PDS",
96
+
actor: actor,
97
+
error: e.message,
98
+
});
99
+
return null;
100
+
}
101
+
};
102
+
17
103
const url = new URL(request.url);
18
104
const { pathname, searchParams } = url;
19
105
20
106
if (!pathname || pathname === "/") {
21
-
return new Response(`This is Tangled's avatar service. It fetches your pretty avatar from Bluesky and caches it on Cloudflare.
22
-
You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`);
107
+
return new Response(
108
+
`This is Tangled's avatar service. It fetches your pretty avatar from your PDS, Bluesky, or generates a placeholder.
109
+
You can't use this directly unfortunately since all requests are signed and may only originate from the appview.`,
110
+
);
23
111
}
24
112
25
113
const size = searchParams.get("size");
···
68
156
}
69
157
70
158
try {
71
-
const profileResponse = await fetch(
72
-
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
73
-
);
74
-
const profile = await profileResponse.json();
75
-
const avatar = profile.avatar;
159
+
let avatarUrl = null;
160
+
161
+
// Create identity resolver
162
+
const resolver = new IdResolver();
163
+
164
+
// Try to get Tangled avatar from user's PDS first
165
+
avatarUrl = await getTangledAvatarFromPDS(actor, resolver);
76
166
77
-
let avatarUrl = profile.avatar;
167
+
// If no Tangled avatar, fall back to Bluesky
168
+
if (!avatarUrl) {
169
+
console.log({
170
+
level: "debug",
171
+
message: "no Tangled avatar, falling back to Bluesky",
172
+
actor: actor,
173
+
});
174
+
175
+
const profileResponse = await fetch(
176
+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}`,
177
+
);
178
+
179
+
if (profileResponse.ok) {
180
+
const profile = await profileResponse.json();
181
+
avatarUrl = profile.avatar;
182
+
}
183
+
}
78
184
79
185
if (!avatarUrl) {
80
186
// Generate a random color based on the actor string
187
+
console.log({
188
+
level: "debug",
189
+
message: "no avatar found, generating placeholder",
190
+
actor: actor,
191
+
});
192
+
81
193
const bgColor = stringToColor(actor);
82
194
const size = resizeToTiny ? 32 : 128;
83
195
const svg = `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg"><rect width="${size}" height="${size}" fill="${bgColor}"/></svg>`;
···
93
205
return response;
94
206
}
95
207
96
-
// Resize if requested
208
+
// Fetch and optionally resize the avatar
97
209
let avatarResponse;
98
210
if (resizeToTiny) {
99
211
avatarResponse = await fetch(avatarUrl, {
+1
-34
crypto/verify.go
+1
-34
crypto/verify.go
···
5
5
"crypto/sha256"
6
6
"encoding/base64"
7
7
"fmt"
8
-
"strings"
9
8
10
9
"github.com/hiddeco/sshsig"
11
10
"golang.org/x/crypto/ssh"
12
-
"tangled.org/core/types"
13
11
)
14
12
15
13
func VerifySignature(pubKey, signature, payload []byte) (error, bool) {
···
28
26
// multiple algorithms but sha-512 is most secure, and git's ssh signing defaults
29
27
// to sha-512 for all key types anyway.
30
28
err = sshsig.Verify(buf, sig, pub, sshsig.HashSHA512, "git")
31
-
return err, err == nil
32
-
}
33
29
34
-
// VerifyCommitSignature reconstructs the payload used to sign a commit. This is
35
-
// essentially the git cat-file output but without the gpgsig header.
36
-
//
37
-
// Caveats: signature verification will fail on commits with more than one parent,
38
-
// i.e. merge commits, because types.NiceDiff doesn't carry more than one Parent field
39
-
// and we are unable to reconstruct the payload correctly.
40
-
//
41
-
// Ideally this should directly operate on an *object.Commit.
42
-
func VerifyCommitSignature(pubKey string, commit types.NiceDiff) (error, bool) {
43
-
signature := commit.Commit.PGPSignature
44
-
45
-
author := bytes.NewBuffer([]byte{})
46
-
committer := bytes.NewBuffer([]byte{})
47
-
commit.Commit.Author.Encode(author)
48
-
commit.Commit.Committer.Encode(committer)
49
-
50
-
payload := strings.Builder{}
51
-
52
-
fmt.Fprintf(&payload, "tree %s\n", commit.Commit.Tree)
53
-
if commit.Commit.Parent != "" {
54
-
fmt.Fprintf(&payload, "parent %s\n", commit.Commit.Parent)
55
-
}
56
-
fmt.Fprintf(&payload, "author %s\n", author.String())
57
-
fmt.Fprintf(&payload, "committer %s\n", committer.String())
58
-
if commit.Commit.ChangedId != "" {
59
-
fmt.Fprintf(&payload, "change-id %s\n", commit.Commit.ChangedId)
60
-
}
61
-
fmt.Fprintf(&payload, "\n%s", commit.Commit.Message)
62
-
63
-
return VerifySignature([]byte(pubKey), []byte(signature), []byte(payload.String()))
30
+
return err, err == nil
64
31
}
65
32
66
33
// SSHFingerprint computes the fingerprint of the supplied ssh pubkey.
+3
-3
docs/hacking.md
+3
-3
docs/hacking.md
···
117
117
# type `poweroff` at the shell to exit the VM
118
118
```
119
119
120
-
This starts a knot on port 6000, a spindle on port 6555
120
+
This starts a knot on port 6444, a spindle on port 6555
121
121
with `ssh` exposed on port 2222.
122
122
123
123
Once the services are running, head to
124
-
http://localhost:3000/knots and hit verify. It should
124
+
http://localhost:3000/settings/knots and hit verify. It should
125
125
verify the ownership of the services instantly if everything
126
126
went smoothly.
127
127
···
146
146
### running a spindle
147
147
148
148
The above VM should already be running a spindle on
149
-
`localhost:6555`. Head to http://localhost:3000/spindles and
149
+
`localhost:6555`. Head to http://localhost:3000/settings/spindles and
150
150
hit verify. You can then configure each repository to use
151
151
this spindle and run CI jobs.
152
152
+1
-1
docs/knot-hosting.md
+1
-1
docs/knot-hosting.md
···
131
131
132
132
You should now have a running knot server! You can finalize
133
133
your registration by hitting the `verify` button on the
134
-
[/knots](https://tangled.org/knots) page. This simply creates
134
+
[/settings/knots](https://tangled.org/settings/knots) page. This simply creates
135
135
a record on your PDS to announce the existence of the knot.
136
136
137
137
### custom paths
+3
-3
docs/migrations.md
+3
-3
docs/migrations.md
···
14
14
For knots:
15
15
16
16
- Upgrade to latest tag (v1.9.0 or above)
17
-
- Head to the [knot dashboard](https://tangled.org/knots) and
17
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
18
18
hit the "retry" button to verify your knot
19
19
20
20
For spindles:
21
21
22
22
- Upgrade to latest tag (v1.9.0 or above)
23
23
- Head to the [spindle
24
-
dashboard](https://tangled.org/spindles) and hit the
24
+
dashboard](https://tangled.org/settings/spindles) and hit the
25
25
"retry" button to verify your spindle
26
26
27
27
## Upgrading from v1.7.x
···
41
41
[settings](https://tangled.org/settings) page.
42
42
- Restart your knot once you have replaced the environment
43
43
variable
44
-
- Head to the [knot dashboard](https://tangled.org/knots) and
44
+
- Head to the [knot dashboard](https://tangled.org/settings/knots) and
45
45
hit the "retry" button to verify your knot. This simply
46
46
writes a `sh.tangled.knot` record to your PDS.
47
47
-2
go.mod
-2
go.mod
···
44
44
github.com/stretchr/testify v1.10.0
45
45
github.com/urfave/cli/v3 v3.3.3
46
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
-
github.com/wyatt915/goldmark-treeblood v0.0.1
48
47
github.com/yuin/goldmark v1.7.13
49
48
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
50
49
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
···
190
189
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
191
190
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
192
191
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
193
-
github.com/wyatt915/treeblood v0.1.16 // indirect
194
192
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
195
193
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
196
194
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
-4
go.sum
-4
go.sum
···
495
495
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
496
496
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
497
497
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
498
-
github.com/wyatt915/goldmark-treeblood v0.0.1 h1:6vLJcjFrHgE4ASu2ga4hqIQmbvQLU37v53jlHZ3pqDs=
499
-
github.com/wyatt915/goldmark-treeblood v0.0.1/go.mod h1:SmcJp5EBaV17rroNlgNQFydYwy0+fv85CUr/ZaCz208=
500
-
github.com/wyatt915/treeblood v0.1.16 h1:byxNbWZhnPDxdTp7W5kQhCeaY8RBVmojTFz1tEHgg8Y=
501
-
github.com/wyatt915/treeblood v0.1.16/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY=
502
498
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
503
499
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
504
500
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+15
-4
jetstream/jetstream.go
+15
-4
jetstream/jetstream.go
···
72
72
// existing instances of the closure when j.WantedDids is mutated
73
73
return func(ctx context.Context, evt *models.Event) error {
74
74
75
+
j.mu.RLock()
75
76
// empty filter => all dids allowed
76
-
if len(j.wantedDids) == 0 {
77
-
return processFunc(ctx, evt)
77
+
matches := len(j.wantedDids) == 0
78
+
if !matches {
79
+
if _, ok := j.wantedDids[evt.Did]; ok {
80
+
matches = true
81
+
}
78
82
}
83
+
j.mu.RUnlock()
79
84
80
-
if _, ok := j.wantedDids[evt.Did]; ok {
85
+
if matches {
81
86
return processFunc(ctx, evt)
82
87
} else {
83
88
return nil
···
122
127
123
128
go func() {
124
129
if j.waitForDid {
125
-
for len(j.wantedDids) == 0 {
130
+
for {
131
+
j.mu.RLock()
132
+
hasDid := len(j.wantedDids) != 0
133
+
j.mu.RUnlock()
134
+
if hasDid {
135
+
break
136
+
}
126
137
time.Sleep(time.Second)
127
138
}
128
139
}
+1
-17
knotserver/git/diff.go
+1
-17
knotserver/git/diff.go
···
77
77
nd.Diff = append(nd.Diff, ndiff)
78
78
}
79
79
80
-
nd.Stat.FilesChanged = len(diffs)
81
-
nd.Commit.This = c.Hash.String()
82
-
nd.Commit.PGPSignature = c.PGPSignature
83
-
nd.Commit.Committer = c.Committer
84
-
nd.Commit.Tree = c.TreeHash.String()
85
-
86
-
if parent.Hash.IsZero() {
87
-
nd.Commit.Parent = ""
88
-
} else {
89
-
nd.Commit.Parent = parent.Hash.String()
90
-
}
91
-
nd.Commit.Author = c.Author
92
-
nd.Commit.Message = c.Message
93
-
94
-
if v, ok := c.ExtraHeaders["change-id"]; ok {
95
-
nd.Commit.ChangedId = string(v)
96
-
}
80
+
nd.Commit.FromGoGitCommit(c)
97
81
98
82
return &nd, nil
99
83
}
+38
-2
knotserver/git/fork.go
+38
-2
knotserver/git/fork.go
···
3
3
import (
4
4
"errors"
5
5
"fmt"
6
+
"log/slog"
7
+
"net/url"
6
8
"os/exec"
9
+
"path/filepath"
7
10
8
11
"github.com/go-git/go-git/v5"
9
12
"github.com/go-git/go-git/v5/config"
13
+
knotconfig "tangled.org/core/knotserver/config"
10
14
)
11
15
12
-
func Fork(repoPath, source string) error {
13
-
cloneCmd := exec.Command("git", "clone", "--bare", source, repoPath)
16
+
func Fork(repoPath, source string, cfg *knotconfig.Config) error {
17
+
u, err := url.Parse(source)
18
+
if err != nil {
19
+
return fmt.Errorf("failed to parse source URL: %w", err)
20
+
}
21
+
22
+
if o := optimizeClone(u, cfg); o != nil {
23
+
u = o
24
+
}
25
+
26
+
cloneCmd := exec.Command("git", "clone", "--bare", u.String(), repoPath)
14
27
if err := cloneCmd.Run(); err != nil {
15
28
return fmt.Errorf("failed to bare clone repository: %w", err)
16
29
}
···
21
34
}
22
35
23
36
return nil
37
+
}
38
+
39
+
func optimizeClone(u *url.URL, cfg *knotconfig.Config) *url.URL {
40
+
// only optimize if it's the same host
41
+
if u.Host != cfg.Server.Hostname {
42
+
return nil
43
+
}
44
+
45
+
local := filepath.Join(cfg.Repo.ScanPath, u.Path)
46
+
47
+
// sanity check: is there a git repo there?
48
+
if _, err := PlainOpen(local); err != nil {
49
+
return nil
50
+
}
51
+
52
+
// create optimized file:// URL
53
+
optimized := &url.URL{
54
+
Scheme: "file",
55
+
Path: local,
56
+
}
57
+
58
+
slog.Debug("performing local clone", "url", optimized.String())
59
+
return optimized
24
60
}
25
61
26
62
func (g *GitRepo) Sync() error {
+1
-1
knotserver/xrpc/create_repo.go
+1
-1
knotserver/xrpc/create_repo.go
···
84
84
repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath)
85
85
86
86
if data.Source != nil && *data.Source != "" {
87
-
err = git.Fork(repoPath, *data.Source)
87
+
err = git.Fork(repoPath, *data.Source, h.Config)
88
88
if err != nil {
89
89
l.Error("forking repo", "error", err.Error())
90
90
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+6
-1
knotserver/xrpc/repo_log.go
+6
-1
knotserver/xrpc/repo_log.go
···
62
62
return
63
63
}
64
64
65
+
tcommits := make([]types.Commit, len(commits))
66
+
for i, c := range commits {
67
+
tcommits[i].FromGoGitCommit(c)
68
+
}
69
+
65
70
// Create response using existing types.RepoLogResponse
66
71
response := types.RepoLogResponse{
67
-
Commits: commits,
72
+
Commits: tcommits,
68
73
Ref: ref,
69
74
Page: (offset / limit) + 1,
70
75
PerPage: limit,
+7
-3
lexicons/actor/profile.json
+7
-3
lexicons/actor/profile.json
···
8
8
"key": "literal:self",
9
9
"record": {
10
10
"type": "object",
11
-
"required": [
12
-
"bluesky"
13
-
],
11
+
"required": ["bluesky"],
14
12
"properties": {
13
+
"avatar": {
14
+
"type": "blob",
15
+
"description": "Small image to be displayed next to posts from account. AKA, 'profile picture'",
16
+
"accept": ["image/png", "image/jpeg"],
17
+
"maxSize": 1000000
18
+
},
15
19
"description": {
16
20
"type": "string",
17
21
"description": "Free-form profile description text.",
+14
lexicons/issue/comment.json
+14
lexicons/issue/comment.json
···
29
29
"replyTo": {
30
30
"type": "string",
31
31
"format": "at-uri"
32
+
},
33
+
"mentions": {
34
+
"type": "array",
35
+
"items": {
36
+
"type": "string",
37
+
"format": "did"
38
+
}
39
+
},
40
+
"references": {
41
+
"type": "array",
42
+
"items": {
43
+
"type": "string",
44
+
"format": "at-uri"
45
+
}
32
46
}
33
47
}
34
48
}
+14
lexicons/issue/issue.json
+14
lexicons/issue/issue.json
···
24
24
"createdAt": {
25
25
"type": "string",
26
26
"format": "datetime"
27
+
},
28
+
"mentions": {
29
+
"type": "array",
30
+
"items": {
31
+
"type": "string",
32
+
"format": "did"
33
+
}
34
+
},
35
+
"references": {
36
+
"type": "array",
37
+
"items": {
38
+
"type": "string",
39
+
"format": "at-uri"
40
+
}
27
41
}
28
42
}
29
43
}
+14
lexicons/pulls/comment.json
+14
lexicons/pulls/comment.json
···
25
25
"createdAt": {
26
26
"type": "string",
27
27
"format": "datetime"
28
+
},
29
+
"mentions": {
30
+
"type": "array",
31
+
"items": {
32
+
"type": "string",
33
+
"format": "did"
34
+
}
35
+
},
36
+
"references": {
37
+
"type": "array",
38
+
"items": {
39
+
"type": "string",
40
+
"format": "at-uri"
41
+
}
28
42
}
29
43
}
30
44
}
+14
lexicons/pulls/pull.json
+14
lexicons/pulls/pull.json
···
36
36
"createdAt": {
37
37
"type": "string",
38
38
"format": "datetime"
39
+
},
40
+
"mentions": {
41
+
"type": "array",
42
+
"items": {
43
+
"type": "string",
44
+
"format": "did"
45
+
}
46
+
},
47
+
"references": {
48
+
"type": "array",
49
+
"items": {
50
+
"type": "string",
51
+
"format": "at-uri"
52
+
}
39
53
}
40
54
}
41
55
}
-30
nix/gomod2nix.toml
-30
nix/gomod2nix.toml
···
165
165
[mod."github.com/davecgh/go-spew"]
166
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
-
[mod."github.com/decred/dcrd/dcrec/secp256k1/v4"]
169
-
version = "v4.4.0"
170
-
hash = "sha256-qrhEIwhDll3cxoVpMbm1NQ9/HTI42S7ms8Buzlo5HCg="
171
168
[mod."github.com/dgraph-io/ristretto"]
172
169
version = "v0.2.0"
173
170
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
373
370
[mod."github.com/klauspost/cpuid/v2"]
374
371
version = "v2.3.0"
375
372
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
376
-
[mod."github.com/lestrrat-go/blackmagic"]
377
-
version = "v1.0.4"
378
-
hash = "sha256-HmWOpwoPDNMwLdOi7onNn3Sb+ZsAa3Ai3gVBbXmQ0e8="
379
-
[mod."github.com/lestrrat-go/httpcc"]
380
-
version = "v1.0.1"
381
-
hash = "sha256-SMRSwJpqDIs/xL0l2e8vP0W65qtCHX2wigcOeqPJmos="
382
-
[mod."github.com/lestrrat-go/httprc"]
383
-
version = "v1.0.6"
384
-
hash = "sha256-mfZzePEhrmyyu/avEBd2MsDXyto8dq5+fyu5lA8GUWM="
385
-
[mod."github.com/lestrrat-go/iter"]
386
-
version = "v1.0.2"
387
-
hash = "sha256-30tErRf7Qu/NOAt1YURXY/XJSA6sCr6hYQfO8QqHrtw="
388
-
[mod."github.com/lestrrat-go/jwx/v2"]
389
-
version = "v2.1.6"
390
-
hash = "sha256-0LszXRZIba+X8AOrs3T4uanAUafBdlVB8/MpUNEFpbc="
391
-
[mod."github.com/lestrrat-go/option"]
392
-
version = "v1.0.1"
393
-
hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI="
394
373
[mod."github.com/lucasb-eyer/go-colorful"]
395
374
version = "v1.2.0"
396
375
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
511
490
[mod."github.com/ryanuber/go-glob"]
512
491
version = "v1.0.0"
513
492
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
514
-
[mod."github.com/segmentio/asm"]
515
-
version = "v1.2.0"
516
-
hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs="
517
493
[mod."github.com/sergi/go-diff"]
518
494
version = "v1.1.0"
519
495
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
548
524
[mod."github.com/whyrusleeping/cbor-gen"]
549
525
version = "v0.3.1"
550
526
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
551
-
[mod."github.com/wyatt915/goldmark-treeblood"]
552
-
version = "v0.0.1"
553
-
hash = "sha256-hAVFaktO02MiiqZFffr8ZlvFEfwxw4Y84OZ2t7e5G7g="
554
-
[mod."github.com/wyatt915/treeblood"]
555
-
version = "v0.1.16"
556
-
hash = "sha256-T68sa+iVx0qY7dDjXEAJvRWQEGXYIpUsf9tcWwO1tIw="
557
527
[mod."github.com/xo/terminfo"]
558
528
version = "v0.0.0-20220910002029-abceb7e1c41e"
559
529
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
+2
nix/modules/knot.nix
+2
nix/modules/knot.nix
+4
-4
nix/vm.nix
+4
-4
nix/vm.nix
···
48
48
# knot
49
49
{
50
50
from = "host";
51
-
host.port = 6000;
52
-
guest.port = 6000;
51
+
host.port = 6444;
52
+
guest.port = 6444;
53
53
}
54
54
# spindle
55
55
{
···
87
87
motd = "Welcome to the development knot!\n";
88
88
server = {
89
89
owner = envVar "TANGLED_VM_KNOT_OWNER";
90
-
hostname = envVarOr "TANGLED_VM_KNOT_HOST" "localhost:6000";
90
+
hostname = envVarOr "TANGLED_VM_KNOT_HOST" "localhost:6444";
91
91
plcUrl = plcUrl;
92
92
jetstreamEndpoint = jetstream;
93
-
listenAddr = "0.0.0.0:6000";
93
+
listenAddr = "0.0.0.0:6444";
94
94
};
95
95
};
96
96
services.tangled.spindle = {
+122
orm/orm.go
+122
orm/orm.go
···
1
+
package orm
2
+
3
+
import (
4
+
"context"
5
+
"database/sql"
6
+
"fmt"
7
+
"log/slog"
8
+
"reflect"
9
+
"strings"
10
+
)
11
+
12
+
type migrationFn = func(*sql.Tx) error
13
+
14
+
func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
15
+
logger = logger.With("migration", name)
16
+
17
+
tx, err := c.BeginTx(context.Background(), nil)
18
+
if err != nil {
19
+
return err
20
+
}
21
+
defer tx.Rollback()
22
+
23
+
var exists bool
24
+
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
25
+
if err != nil {
26
+
return err
27
+
}
28
+
29
+
if !exists {
30
+
// run migration
31
+
err = migrationFn(tx)
32
+
if err != nil {
33
+
logger.Error("failed to run migration", "err", err)
34
+
return err
35
+
}
36
+
37
+
// mark migration as complete
38
+
_, err = tx.Exec("insert into migrations (name) values (?)", name)
39
+
if err != nil {
40
+
logger.Error("failed to mark migration as complete", "err", err)
41
+
return err
42
+
}
43
+
44
+
// commit the transaction
45
+
if err := tx.Commit(); err != nil {
46
+
return err
47
+
}
48
+
49
+
logger.Info("migration applied successfully")
50
+
} else {
51
+
logger.Warn("skipped migration, already applied")
52
+
}
53
+
54
+
return nil
55
+
}
56
+
57
+
type Filter struct {
58
+
Key string
59
+
arg any
60
+
Cmp string
61
+
}
62
+
63
+
func newFilter(key, cmp string, arg any) Filter {
64
+
return Filter{
65
+
Key: key,
66
+
arg: arg,
67
+
Cmp: cmp,
68
+
}
69
+
}
70
+
71
+
func FilterEq(key string, arg any) Filter { return newFilter(key, "=", arg) }
72
+
func FilterNotEq(key string, arg any) Filter { return newFilter(key, "<>", arg) }
73
+
func FilterGte(key string, arg any) Filter { return newFilter(key, ">=", arg) }
74
+
func FilterLte(key string, arg any) Filter { return newFilter(key, "<=", arg) }
75
+
func FilterIs(key string, arg any) Filter { return newFilter(key, "is", arg) }
76
+
func FilterIsNot(key string, arg any) Filter { return newFilter(key, "is not", arg) }
77
+
func FilterIn(key string, arg any) Filter { return newFilter(key, "in", arg) }
78
+
func FilterLike(key string, arg any) Filter { return newFilter(key, "like", arg) }
79
+
func FilterNotLike(key string, arg any) Filter { return newFilter(key, "not like", arg) }
80
+
func FilterContains(key string, arg any) Filter {
81
+
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
82
+
}
83
+
84
+
func (f Filter) Condition() string {
85
+
rv := reflect.ValueOf(f.arg)
86
+
kind := rv.Kind()
87
+
88
+
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
89
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
90
+
if rv.Len() == 0 {
91
+
// always false
92
+
return "1 = 0"
93
+
}
94
+
95
+
placeholders := make([]string, rv.Len())
96
+
for i := range placeholders {
97
+
placeholders[i] = "?"
98
+
}
99
+
100
+
return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, strings.Join(placeholders, ", "))
101
+
}
102
+
103
+
return fmt.Sprintf("%s %s ?", f.Key, f.Cmp)
104
+
}
105
+
106
+
func (f Filter) Arg() []any {
107
+
rv := reflect.ValueOf(f.arg)
108
+
kind := rv.Kind()
109
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
110
+
if rv.Len() == 0 {
111
+
return nil
112
+
}
113
+
114
+
out := make([]any, rv.Len())
115
+
for i := range rv.Len() {
116
+
out[i] = rv.Index(i).Interface()
117
+
}
118
+
return out
119
+
}
120
+
121
+
return []any{f.arg}
122
+
}
-1
patchutil/patchutil.go
-1
patchutil/patchutil.go
+8
rbac/rbac.go
+8
rbac/rbac.go
···
285
285
return e.E.Enforce(user, domain, repo, "repo:delete")
286
286
}
287
287
288
+
func (e *Enforcer) IsRepoOwner(user, domain, repo string) (bool, error) {
289
+
return e.E.Enforce(user, domain, repo, "repo:owner")
290
+
}
291
+
292
+
func (e *Enforcer) IsRepoCollaborator(user, domain, repo string) (bool, error) {
293
+
return e.E.Enforce(user, domain, repo, "repo:collaborator")
294
+
}
295
+
288
296
func (e *Enforcer) IsPushAllowed(user, domain, repo string) (bool, error) {
289
297
return e.E.Enforce(user, domain, repo, "repo:push")
290
298
}
+5
-6
spindle/engines/nixery/engine.go
+5
-6
spindle/engines/nixery/engine.go
···
73
73
type addlFields struct {
74
74
image string
75
75
container string
76
-
env map[string]string
77
76
}
78
77
79
78
func (e *Engine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) {
···
103
102
swf.Steps = append(swf.Steps, sstep)
104
103
}
105
104
swf.Name = twf.Name
106
-
addl.env = dwf.Environment
105
+
swf.Environment = dwf.Environment
107
106
addl.image = workflowImage(dwf.Dependencies, e.cfg.NixeryPipelines.Nixery)
108
107
109
108
setup := &setupSteps{}
110
109
111
110
setup.addStep(nixConfStep())
112
-
setup.addStep(cloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev))
111
+
setup.addStep(models.BuildCloneStep(twf, *tpl.TriggerMetadata, e.cfg.Server.Dev))
113
112
// this step could be empty
114
113
if s := dependencyStep(dwf.Dependencies); s != nil {
115
114
setup.addStep(*s)
···
288
287
289
288
func (e *Engine) RunStep(ctx context.Context, wid models.WorkflowId, w *models.Workflow, idx int, secrets []secrets.UnlockedSecret, wfLogger *models.WorkflowLogger) error {
290
289
addl := w.Data.(addlFields)
291
-
workflowEnvs := ConstructEnvs(addl.env)
290
+
workflowEnvs := ConstructEnvs(w.Environment)
292
291
// TODO(winter): should SetupWorkflow also have secret access?
293
292
// IMO yes, but probably worth thinking on.
294
293
for _, s := range secrets {
···
310
309
envs.AddEnv("HOME", homeDir)
311
310
312
311
mkExecResp, err := e.docker.ContainerExecCreate(ctx, addl.container, container.ExecOptions{
313
-
Cmd: []string{"bash", "-c", step.command},
312
+
Cmd: []string{"bash", "-c", step.Command()},
314
313
AttachStdout: true,
315
314
AttachStderr: true,
316
315
Env: envs,
···
333
332
// Docker doesn't provide an API to kill an exec run
334
333
// (sure, we could grab the PID and kill it ourselves,
335
334
// but that's wasted effort)
336
-
e.l.Warn("step timed out", "step", step.Name)
335
+
e.l.Warn("step timed out", "step", step.Name())
337
336
338
337
<-tailDone
339
338
-73
spindle/engines/nixery/setup_steps.go
-73
spindle/engines/nixery/setup_steps.go
···
2
2
3
3
import (
4
4
"fmt"
5
-
"path"
6
5
"strings"
7
-
8
-
"tangled.org/core/api/tangled"
9
-
"tangled.org/core/workflow"
10
6
)
11
7
12
8
func nixConfStep() Step {
···
17
13
command: setupCmd,
18
14
name: "Configure Nix",
19
15
}
20
-
}
21
-
22
-
// cloneOptsAsSteps processes clone options and adds corresponding steps
23
-
// to the beginning of the workflow's step list if cloning is not skipped.
24
-
//
25
-
// the steps to do here are:
26
-
// - git init
27
-
// - git remote add origin <url>
28
-
// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>
29
-
// - git checkout FETCH_HEAD
30
-
func cloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) Step {
31
-
if twf.Clone.Skip {
32
-
return Step{}
33
-
}
34
-
35
-
var commands []string
36
-
37
-
// initialize git repo in workspace
38
-
commands = append(commands, "git init")
39
-
40
-
// add repo as git remote
41
-
scheme := "https://"
42
-
if dev {
43
-
scheme = "http://"
44
-
tr.Repo.Knot = strings.ReplaceAll(tr.Repo.Knot, "localhost", "host.docker.internal")
45
-
}
46
-
url := scheme + path.Join(tr.Repo.Knot, tr.Repo.Did, tr.Repo.Repo)
47
-
commands = append(commands, fmt.Sprintf("git remote add origin %s", url))
48
-
49
-
// run git fetch
50
-
{
51
-
var fetchArgs []string
52
-
53
-
// default clone depth is 1
54
-
depth := 1
55
-
if twf.Clone.Depth > 1 {
56
-
depth = int(twf.Clone.Depth)
57
-
}
58
-
fetchArgs = append(fetchArgs, fmt.Sprintf("--depth=%d", depth))
59
-
60
-
// optionally recurse submodules
61
-
if twf.Clone.Submodules {
62
-
fetchArgs = append(fetchArgs, "--recurse-submodules=yes")
63
-
}
64
-
65
-
// set remote to fetch from
66
-
fetchArgs = append(fetchArgs, "origin")
67
-
68
-
// set revision to checkout
69
-
switch workflow.TriggerKind(tr.Kind) {
70
-
case workflow.TriggerKindManual:
71
-
// TODO: unimplemented
72
-
case workflow.TriggerKindPush:
73
-
fetchArgs = append(fetchArgs, tr.Push.NewSha)
74
-
case workflow.TriggerKindPullRequest:
75
-
fetchArgs = append(fetchArgs, tr.PullRequest.SourceSha)
76
-
}
77
-
78
-
commands = append(commands, fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " ")))
79
-
}
80
-
81
-
// run git checkout
82
-
commands = append(commands, "git checkout FETCH_HEAD")
83
-
84
-
cloneStep := Step{
85
-
command: strings.Join(commands, "\n"),
86
-
name: "Clone repository into workspace",
87
-
}
88
-
return cloneStep
89
16
}
90
17
91
18
// dependencyStep processes dependencies defined in the workflow.
+150
spindle/models/clone.go
+150
spindle/models/clone.go
···
1
+
package models
2
+
3
+
import (
4
+
"fmt"
5
+
"strings"
6
+
7
+
"tangled.org/core/api/tangled"
8
+
"tangled.org/core/workflow"
9
+
)
10
+
11
+
type CloneStep struct {
12
+
name string
13
+
kind StepKind
14
+
commands []string
15
+
}
16
+
17
+
func (s CloneStep) Name() string {
18
+
return s.name
19
+
}
20
+
21
+
func (s CloneStep) Commands() []string {
22
+
return s.commands
23
+
}
24
+
25
+
func (s CloneStep) Command() string {
26
+
return strings.Join(s.commands, "\n")
27
+
}
28
+
29
+
func (s CloneStep) Kind() StepKind {
30
+
return s.kind
31
+
}
32
+
33
+
// BuildCloneStep generates git clone commands.
34
+
// The caller must ensure the current working directory is set to the desired
35
+
// workspace directory before executing these commands.
36
+
//
37
+
// The generated commands are:
38
+
// - git init
39
+
// - git remote add origin <url>
40
+
// - git fetch --depth=<d> --recurse-submodules=<yes|no> <sha>
41
+
// - git checkout FETCH_HEAD
42
+
//
43
+
// Supports all trigger types (push, PR, manual) and clone options.
44
+
func BuildCloneStep(twf tangled.Pipeline_Workflow, tr tangled.Pipeline_TriggerMetadata, dev bool) CloneStep {
45
+
if twf.Clone != nil && twf.Clone.Skip {
46
+
return CloneStep{}
47
+
}
48
+
49
+
commitSHA, err := extractCommitSHA(tr)
50
+
if err != nil {
51
+
return CloneStep{
52
+
kind: StepKindSystem,
53
+
name: "Clone repository into workspace (error)",
54
+
commands: []string{fmt.Sprintf("echo 'Failed to get clone info: %s' && exit 1", err.Error())},
55
+
}
56
+
}
57
+
58
+
repoURL := BuildRepoURL(tr.Repo, dev)
59
+
60
+
var cloneOpts tangled.Pipeline_CloneOpts
61
+
if twf.Clone != nil {
62
+
cloneOpts = *twf.Clone
63
+
}
64
+
fetchArgs := buildFetchArgs(cloneOpts, commitSHA)
65
+
66
+
return CloneStep{
67
+
kind: StepKindSystem,
68
+
name: "Clone repository into workspace",
69
+
commands: []string{
70
+
"git init",
71
+
fmt.Sprintf("git remote add origin %s", repoURL),
72
+
fmt.Sprintf("git fetch %s", strings.Join(fetchArgs, " ")),
73
+
"git checkout FETCH_HEAD",
74
+
},
75
+
}
76
+
}
77
+
78
+
// extractCommitSHA extracts the commit SHA from trigger metadata based on trigger type
79
+
func extractCommitSHA(tr tangled.Pipeline_TriggerMetadata) (string, error) {
80
+
switch workflow.TriggerKind(tr.Kind) {
81
+
case workflow.TriggerKindPush:
82
+
if tr.Push == nil {
83
+
return "", fmt.Errorf("push trigger metadata is nil")
84
+
}
85
+
return tr.Push.NewSha, nil
86
+
87
+
case workflow.TriggerKindPullRequest:
88
+
if tr.PullRequest == nil {
89
+
return "", fmt.Errorf("pull request trigger metadata is nil")
90
+
}
91
+
return tr.PullRequest.SourceSha, nil
92
+
93
+
case workflow.TriggerKindManual:
94
+
// Manual triggers don't have an explicit SHA in the metadata
95
+
// For now, return empty string - could be enhanced to fetch from default branch
96
+
// TODO: Implement manual trigger SHA resolution (fetch default branch HEAD)
97
+
return "", nil
98
+
99
+
default:
100
+
return "", fmt.Errorf("unknown trigger kind: %s", tr.Kind)
101
+
}
102
+
}
103
+
104
+
// BuildRepoURL constructs the repository URL from repo metadata.
105
+
func BuildRepoURL(repo *tangled.Pipeline_TriggerRepo, devMode bool) string {
106
+
if repo == nil {
107
+
return ""
108
+
}
109
+
110
+
scheme := "https://"
111
+
if devMode {
112
+
scheme = "http://"
113
+
}
114
+
115
+
// Get host from knot
116
+
host := repo.Knot
117
+
118
+
// In dev mode, replace localhost with host.docker.internal for Docker networking
119
+
if devMode && strings.Contains(host, "localhost") {
120
+
host = strings.ReplaceAll(host, "localhost", "host.docker.internal")
121
+
}
122
+
123
+
// Build URL: {scheme}{knot}/{did}/{repo}
124
+
return fmt.Sprintf("%s%s/%s/%s", scheme, host, repo.Did, repo.Repo)
125
+
}
126
+
127
+
// buildFetchArgs constructs the arguments for git fetch based on clone options
128
+
func buildFetchArgs(clone tangled.Pipeline_CloneOpts, sha string) []string {
129
+
args := []string{}
130
+
131
+
// Set fetch depth (default to 1 for shallow clone)
132
+
depth := clone.Depth
133
+
if depth == 0 {
134
+
depth = 1
135
+
}
136
+
args = append(args, fmt.Sprintf("--depth=%d", depth))
137
+
138
+
// Add submodules if requested
139
+
if clone.Submodules {
140
+
args = append(args, "--recurse-submodules=yes")
141
+
}
142
+
143
+
// Add remote and SHA
144
+
args = append(args, "origin")
145
+
if sha != "" {
146
+
args = append(args, sha)
147
+
}
148
+
149
+
return args
150
+
}
+371
spindle/models/clone_test.go
+371
spindle/models/clone_test.go
···
1
+
package models
2
+
3
+
import (
4
+
"strings"
5
+
"testing"
6
+
7
+
"tangled.org/core/api/tangled"
8
+
"tangled.org/core/workflow"
9
+
)
10
+
11
+
func TestBuildCloneStep_PushTrigger(t *testing.T) {
12
+
twf := tangled.Pipeline_Workflow{
13
+
Clone: &tangled.Pipeline_CloneOpts{
14
+
Depth: 1,
15
+
Submodules: false,
16
+
Skip: false,
17
+
},
18
+
}
19
+
tr := tangled.Pipeline_TriggerMetadata{
20
+
Kind: string(workflow.TriggerKindPush),
21
+
Push: &tangled.Pipeline_PushTriggerData{
22
+
NewSha: "abc123",
23
+
OldSha: "def456",
24
+
Ref: "refs/heads/main",
25
+
},
26
+
Repo: &tangled.Pipeline_TriggerRepo{
27
+
Knot: "example.com",
28
+
Did: "did:plc:user123",
29
+
Repo: "my-repo",
30
+
},
31
+
}
32
+
33
+
step := BuildCloneStep(twf, tr, false)
34
+
35
+
if step.Kind() != StepKindSystem {
36
+
t.Errorf("Expected StepKindSystem, got %v", step.Kind())
37
+
}
38
+
39
+
if step.Name() != "Clone repository into workspace" {
40
+
t.Errorf("Expected 'Clone repository into workspace', got '%s'", step.Name())
41
+
}
42
+
43
+
commands := step.Commands()
44
+
if len(commands) != 4 {
45
+
t.Errorf("Expected 4 commands, got %d", len(commands))
46
+
}
47
+
48
+
// Verify commands contain expected git operations
49
+
allCmds := strings.Join(commands, " ")
50
+
if !strings.Contains(allCmds, "git init") {
51
+
t.Error("Commands should contain 'git init'")
52
+
}
53
+
if !strings.Contains(allCmds, "git remote add origin") {
54
+
t.Error("Commands should contain 'git remote add origin'")
55
+
}
56
+
if !strings.Contains(allCmds, "git fetch") {
57
+
t.Error("Commands should contain 'git fetch'")
58
+
}
59
+
if !strings.Contains(allCmds, "abc123") {
60
+
t.Error("Commands should contain commit SHA")
61
+
}
62
+
if !strings.Contains(allCmds, "git checkout FETCH_HEAD") {
63
+
t.Error("Commands should contain 'git checkout FETCH_HEAD'")
64
+
}
65
+
if !strings.Contains(allCmds, "https://example.com/did:plc:user123/my-repo") {
66
+
t.Error("Commands should contain expected repo URL")
67
+
}
68
+
}
69
+
70
+
func TestBuildCloneStep_PullRequestTrigger(t *testing.T) {
71
+
twf := tangled.Pipeline_Workflow{
72
+
Clone: &tangled.Pipeline_CloneOpts{
73
+
Depth: 1,
74
+
Skip: false,
75
+
},
76
+
}
77
+
tr := tangled.Pipeline_TriggerMetadata{
78
+
Kind: string(workflow.TriggerKindPullRequest),
79
+
PullRequest: &tangled.Pipeline_PullRequestTriggerData{
80
+
SourceSha: "pr-sha-789",
81
+
SourceBranch: "feature-branch",
82
+
TargetBranch: "main",
83
+
Action: "opened",
84
+
},
85
+
Repo: &tangled.Pipeline_TriggerRepo{
86
+
Knot: "example.com",
87
+
Did: "did:plc:user123",
88
+
Repo: "my-repo",
89
+
},
90
+
}
91
+
92
+
step := BuildCloneStep(twf, tr, false)
93
+
94
+
allCmds := strings.Join(step.Commands(), " ")
95
+
if !strings.Contains(allCmds, "pr-sha-789") {
96
+
t.Error("Commands should contain PR commit SHA")
97
+
}
98
+
}
99
+
100
+
func TestBuildCloneStep_ManualTrigger(t *testing.T) {
101
+
twf := tangled.Pipeline_Workflow{
102
+
Clone: &tangled.Pipeline_CloneOpts{
103
+
Depth: 1,
104
+
Skip: false,
105
+
},
106
+
}
107
+
tr := tangled.Pipeline_TriggerMetadata{
108
+
Kind: string(workflow.TriggerKindManual),
109
+
Manual: &tangled.Pipeline_ManualTriggerData{
110
+
Inputs: nil,
111
+
},
112
+
Repo: &tangled.Pipeline_TriggerRepo{
113
+
Knot: "example.com",
114
+
Did: "did:plc:user123",
115
+
Repo: "my-repo",
116
+
},
117
+
}
118
+
119
+
step := BuildCloneStep(twf, tr, false)
120
+
121
+
// Manual triggers don't have a SHA yet (TODO), so git fetch won't include a SHA
122
+
allCmds := strings.Join(step.Commands(), " ")
123
+
// Should still have basic git commands
124
+
if !strings.Contains(allCmds, "git init") {
125
+
t.Error("Commands should contain 'git init'")
126
+
}
127
+
if !strings.Contains(allCmds, "git fetch") {
128
+
t.Error("Commands should contain 'git fetch'")
129
+
}
130
+
}
131
+
132
+
func TestBuildCloneStep_SkipFlag(t *testing.T) {
133
+
twf := tangled.Pipeline_Workflow{
134
+
Clone: &tangled.Pipeline_CloneOpts{
135
+
Skip: true,
136
+
},
137
+
}
138
+
tr := tangled.Pipeline_TriggerMetadata{
139
+
Kind: string(workflow.TriggerKindPush),
140
+
Push: &tangled.Pipeline_PushTriggerData{
141
+
NewSha: "abc123",
142
+
},
143
+
Repo: &tangled.Pipeline_TriggerRepo{
144
+
Knot: "example.com",
145
+
Did: "did:plc:user123",
146
+
Repo: "my-repo",
147
+
},
148
+
}
149
+
150
+
step := BuildCloneStep(twf, tr, false)
151
+
152
+
// Empty step when skip is true
153
+
if step.Name() != "" {
154
+
t.Error("Expected empty step name when Skip is true")
155
+
}
156
+
if len(step.Commands()) != 0 {
157
+
t.Errorf("Expected no commands when Skip is true, got %d commands", len(step.Commands()))
158
+
}
159
+
}
160
+
161
+
func TestBuildCloneStep_DevMode(t *testing.T) {
162
+
twf := tangled.Pipeline_Workflow{
163
+
Clone: &tangled.Pipeline_CloneOpts{
164
+
Depth: 1,
165
+
Skip: false,
166
+
},
167
+
}
168
+
tr := tangled.Pipeline_TriggerMetadata{
169
+
Kind: string(workflow.TriggerKindPush),
170
+
Push: &tangled.Pipeline_PushTriggerData{
171
+
NewSha: "abc123",
172
+
},
173
+
Repo: &tangled.Pipeline_TriggerRepo{
174
+
Knot: "localhost:3000",
175
+
Did: "did:plc:user123",
176
+
Repo: "my-repo",
177
+
},
178
+
}
179
+
180
+
step := BuildCloneStep(twf, tr, true)
181
+
182
+
// In dev mode, should use http:// and replace localhost with host.docker.internal
183
+
allCmds := strings.Join(step.Commands(), " ")
184
+
expectedURL := "http://host.docker.internal:3000/did:plc:user123/my-repo"
185
+
if !strings.Contains(allCmds, expectedURL) {
186
+
t.Errorf("Expected dev mode URL '%s' in commands", expectedURL)
187
+
}
188
+
}
189
+
190
+
func TestBuildCloneStep_DepthAndSubmodules(t *testing.T) {
191
+
twf := tangled.Pipeline_Workflow{
192
+
Clone: &tangled.Pipeline_CloneOpts{
193
+
Depth: 10,
194
+
Submodules: true,
195
+
Skip: false,
196
+
},
197
+
}
198
+
tr := tangled.Pipeline_TriggerMetadata{
199
+
Kind: string(workflow.TriggerKindPush),
200
+
Push: &tangled.Pipeline_PushTriggerData{
201
+
NewSha: "abc123",
202
+
},
203
+
Repo: &tangled.Pipeline_TriggerRepo{
204
+
Knot: "example.com",
205
+
Did: "did:plc:user123",
206
+
Repo: "my-repo",
207
+
},
208
+
}
209
+
210
+
step := BuildCloneStep(twf, tr, false)
211
+
212
+
allCmds := strings.Join(step.Commands(), " ")
213
+
if !strings.Contains(allCmds, "--depth=10") {
214
+
t.Error("Commands should contain '--depth=10'")
215
+
}
216
+
217
+
if !strings.Contains(allCmds, "--recurse-submodules=yes") {
218
+
t.Error("Commands should contain '--recurse-submodules=yes'")
219
+
}
220
+
}
221
+
222
+
func TestBuildCloneStep_DefaultDepth(t *testing.T) {
223
+
twf := tangled.Pipeline_Workflow{
224
+
Clone: &tangled.Pipeline_CloneOpts{
225
+
Depth: 0, // Default should be 1
226
+
Skip: false,
227
+
},
228
+
}
229
+
tr := tangled.Pipeline_TriggerMetadata{
230
+
Kind: string(workflow.TriggerKindPush),
231
+
Push: &tangled.Pipeline_PushTriggerData{
232
+
NewSha: "abc123",
233
+
},
234
+
Repo: &tangled.Pipeline_TriggerRepo{
235
+
Knot: "example.com",
236
+
Did: "did:plc:user123",
237
+
Repo: "my-repo",
238
+
},
239
+
}
240
+
241
+
step := BuildCloneStep(twf, tr, false)
242
+
243
+
allCmds := strings.Join(step.Commands(), " ")
244
+
if !strings.Contains(allCmds, "--depth=1") {
245
+
t.Error("Commands should default to '--depth=1'")
246
+
}
247
+
}
248
+
249
+
func TestBuildCloneStep_NilPushData(t *testing.T) {
250
+
twf := tangled.Pipeline_Workflow{
251
+
Clone: &tangled.Pipeline_CloneOpts{
252
+
Depth: 1,
253
+
Skip: false,
254
+
},
255
+
}
256
+
tr := tangled.Pipeline_TriggerMetadata{
257
+
Kind: string(workflow.TriggerKindPush),
258
+
Push: nil, // Nil push data should create error step
259
+
Repo: &tangled.Pipeline_TriggerRepo{
260
+
Knot: "example.com",
261
+
Did: "did:plc:user123",
262
+
Repo: "my-repo",
263
+
},
264
+
}
265
+
266
+
step := BuildCloneStep(twf, tr, false)
267
+
268
+
// Should return an error step
269
+
if !strings.Contains(step.Name(), "error") {
270
+
t.Error("Expected error in step name when push data is nil")
271
+
}
272
+
273
+
allCmds := strings.Join(step.Commands(), " ")
274
+
if !strings.Contains(allCmds, "Failed to get clone info") {
275
+
t.Error("Commands should contain error message")
276
+
}
277
+
if !strings.Contains(allCmds, "exit 1") {
278
+
t.Error("Commands should exit with error")
279
+
}
280
+
}
281
+
282
+
func TestBuildCloneStep_NilPRData(t *testing.T) {
283
+
twf := tangled.Pipeline_Workflow{
284
+
Clone: &tangled.Pipeline_CloneOpts{
285
+
Depth: 1,
286
+
Skip: false,
287
+
},
288
+
}
289
+
tr := tangled.Pipeline_TriggerMetadata{
290
+
Kind: string(workflow.TriggerKindPullRequest),
291
+
PullRequest: nil, // Nil PR data should create error step
292
+
Repo: &tangled.Pipeline_TriggerRepo{
293
+
Knot: "example.com",
294
+
Did: "did:plc:user123",
295
+
Repo: "my-repo",
296
+
},
297
+
}
298
+
299
+
step := BuildCloneStep(twf, tr, false)
300
+
301
+
// Should return an error step
302
+
if !strings.Contains(step.Name(), "error") {
303
+
t.Error("Expected error in step name when pull request data is nil")
304
+
}
305
+
306
+
allCmds := strings.Join(step.Commands(), " ")
307
+
if !strings.Contains(allCmds, "Failed to get clone info") {
308
+
t.Error("Commands should contain error message")
309
+
}
310
+
}
311
+
312
+
func TestBuildCloneStep_UnknownTriggerKind(t *testing.T) {
313
+
twf := tangled.Pipeline_Workflow{
314
+
Clone: &tangled.Pipeline_CloneOpts{
315
+
Depth: 1,
316
+
Skip: false,
317
+
},
318
+
}
319
+
tr := tangled.Pipeline_TriggerMetadata{
320
+
Kind: "unknown_trigger",
321
+
Repo: &tangled.Pipeline_TriggerRepo{
322
+
Knot: "example.com",
323
+
Did: "did:plc:user123",
324
+
Repo: "my-repo",
325
+
},
326
+
}
327
+
328
+
step := BuildCloneStep(twf, tr, false)
329
+
330
+
// Should return an error step
331
+
if !strings.Contains(step.Name(), "error") {
332
+
t.Error("Expected error in step name for unknown trigger kind")
333
+
}
334
+
335
+
allCmds := strings.Join(step.Commands(), " ")
336
+
if !strings.Contains(allCmds, "unknown trigger kind") {
337
+
t.Error("Commands should contain error message about unknown trigger kind")
338
+
}
339
+
}
340
+
341
+
func TestBuildCloneStep_NilCloneOpts(t *testing.T) {
342
+
twf := tangled.Pipeline_Workflow{
343
+
Clone: nil, // Nil clone options should use defaults
344
+
}
345
+
tr := tangled.Pipeline_TriggerMetadata{
346
+
Kind: string(workflow.TriggerKindPush),
347
+
Push: &tangled.Pipeline_PushTriggerData{
348
+
NewSha: "abc123",
349
+
},
350
+
Repo: &tangled.Pipeline_TriggerRepo{
351
+
Knot: "example.com",
352
+
Did: "did:plc:user123",
353
+
Repo: "my-repo",
354
+
},
355
+
}
356
+
357
+
step := BuildCloneStep(twf, tr, false)
358
+
359
+
// Should still work with default options
360
+
if step.Kind() != StepKindSystem {
361
+
t.Errorf("Expected StepKindSystem, got %v", step.Kind())
362
+
}
363
+
364
+
allCmds := strings.Join(step.Commands(), " ")
365
+
if !strings.Contains(allCmds, "--depth=1") {
366
+
t.Error("Commands should default to '--depth=1' when Clone is nil")
367
+
}
368
+
if !strings.Contains(allCmds, "git init") {
369
+
t.Error("Commands should contain 'git init'")
370
+
}
371
+
}
+4
-3
spindle/models/pipeline.go
+4
-3
spindle/models/pipeline.go
+77
spindle/models/pipeline_env.go
+77
spindle/models/pipeline_env.go
···
1
+
package models
2
+
3
+
import (
4
+
"strings"
5
+
6
+
"github.com/go-git/go-git/v5/plumbing"
7
+
"tangled.org/core/api/tangled"
8
+
"tangled.org/core/workflow"
9
+
)
10
+
11
+
// PipelineEnvVars extracts environment variables from pipeline trigger metadata.
12
+
// These are framework-provided variables that are injected into workflow steps.
13
+
func PipelineEnvVars(tr *tangled.Pipeline_TriggerMetadata, pipelineId PipelineId, devMode bool) map[string]string {
14
+
if tr == nil {
15
+
return nil
16
+
}
17
+
18
+
env := make(map[string]string)
19
+
20
+
// Standard CI environment variable
21
+
env["CI"] = "true"
22
+
23
+
env["TANGLED_PIPELINE_ID"] = pipelineId.Rkey
24
+
25
+
// Repo info
26
+
if tr.Repo != nil {
27
+
env["TANGLED_REPO_KNOT"] = tr.Repo.Knot
28
+
env["TANGLED_REPO_DID"] = tr.Repo.Did
29
+
env["TANGLED_REPO_NAME"] = tr.Repo.Repo
30
+
env["TANGLED_REPO_DEFAULT_BRANCH"] = tr.Repo.DefaultBranch
31
+
env["TANGLED_REPO_URL"] = BuildRepoURL(tr.Repo, devMode)
32
+
}
33
+
34
+
switch workflow.TriggerKind(tr.Kind) {
35
+
case workflow.TriggerKindPush:
36
+
if tr.Push != nil {
37
+
refName := plumbing.ReferenceName(tr.Push.Ref)
38
+
refType := "branch"
39
+
if refName.IsTag() {
40
+
refType = "tag"
41
+
}
42
+
43
+
env["TANGLED_REF"] = tr.Push.Ref
44
+
env["TANGLED_REF_NAME"] = refName.Short()
45
+
env["TANGLED_REF_TYPE"] = refType
46
+
env["TANGLED_SHA"] = tr.Push.NewSha
47
+
env["TANGLED_COMMIT_SHA"] = tr.Push.NewSha
48
+
}
49
+
50
+
case workflow.TriggerKindPullRequest:
51
+
if tr.PullRequest != nil {
52
+
// For PRs, the "ref" is the source branch
53
+
env["TANGLED_REF"] = "refs/heads/" + tr.PullRequest.SourceBranch
54
+
env["TANGLED_REF_NAME"] = tr.PullRequest.SourceBranch
55
+
env["TANGLED_REF_TYPE"] = "branch"
56
+
env["TANGLED_SHA"] = tr.PullRequest.SourceSha
57
+
env["TANGLED_COMMIT_SHA"] = tr.PullRequest.SourceSha
58
+
59
+
// PR-specific variables
60
+
env["TANGLED_PR_SOURCE_BRANCH"] = tr.PullRequest.SourceBranch
61
+
env["TANGLED_PR_TARGET_BRANCH"] = tr.PullRequest.TargetBranch
62
+
env["TANGLED_PR_SOURCE_SHA"] = tr.PullRequest.SourceSha
63
+
env["TANGLED_PR_ACTION"] = tr.PullRequest.Action
64
+
}
65
+
66
+
case workflow.TriggerKindManual:
67
+
// Manual triggers may not have ref/sha info
68
+
// Include any manual inputs if present
69
+
if tr.Manual != nil {
70
+
for _, pair := range tr.Manual.Inputs {
71
+
env["TANGLED_INPUT_"+strings.ToUpper(pair.Key)] = pair.Value
72
+
}
73
+
}
74
+
}
75
+
76
+
return env
77
+
}
+260
spindle/models/pipeline_env_test.go
+260
spindle/models/pipeline_env_test.go
···
1
+
package models
2
+
3
+
import (
4
+
"testing"
5
+
6
+
"tangled.org/core/api/tangled"
7
+
"tangled.org/core/workflow"
8
+
)
9
+
10
+
func TestPipelineEnvVars_PushBranch(t *testing.T) {
11
+
tr := &tangled.Pipeline_TriggerMetadata{
12
+
Kind: string(workflow.TriggerKindPush),
13
+
Push: &tangled.Pipeline_PushTriggerData{
14
+
NewSha: "abc123def456",
15
+
OldSha: "000000000000",
16
+
Ref: "refs/heads/main",
17
+
},
18
+
Repo: &tangled.Pipeline_TriggerRepo{
19
+
Knot: "example.com",
20
+
Did: "did:plc:user123",
21
+
Repo: "my-repo",
22
+
DefaultBranch: "main",
23
+
},
24
+
}
25
+
id := PipelineId{
26
+
Knot: "example.com",
27
+
Rkey: "123123",
28
+
}
29
+
env := PipelineEnvVars(tr, id, false)
30
+
31
+
// Check standard CI variable
32
+
if env["CI"] != "true" {
33
+
t.Errorf("Expected CI='true', got '%s'", env["CI"])
34
+
}
35
+
36
+
// Check ref variables
37
+
if env["TANGLED_REF"] != "refs/heads/main" {
38
+
t.Errorf("Expected TANGLED_REF='refs/heads/main', got '%s'", env["TANGLED_REF"])
39
+
}
40
+
if env["TANGLED_REF_NAME"] != "main" {
41
+
t.Errorf("Expected TANGLED_REF_NAME='main', got '%s'", env["TANGLED_REF_NAME"])
42
+
}
43
+
if env["TANGLED_REF_TYPE"] != "branch" {
44
+
t.Errorf("Expected TANGLED_REF_TYPE='branch', got '%s'", env["TANGLED_REF_TYPE"])
45
+
}
46
+
47
+
// Check SHA variables
48
+
if env["TANGLED_SHA"] != "abc123def456" {
49
+
t.Errorf("Expected TANGLED_SHA='abc123def456', got '%s'", env["TANGLED_SHA"])
50
+
}
51
+
if env["TANGLED_COMMIT_SHA"] != "abc123def456" {
52
+
t.Errorf("Expected TANGLED_COMMIT_SHA='abc123def456', got '%s'", env["TANGLED_COMMIT_SHA"])
53
+
}
54
+
55
+
// Check repo variables
56
+
if env["TANGLED_REPO_KNOT"] != "example.com" {
57
+
t.Errorf("Expected TANGLED_REPO_KNOT='example.com', got '%s'", env["TANGLED_REPO_KNOT"])
58
+
}
59
+
if env["TANGLED_REPO_DID"] != "did:plc:user123" {
60
+
t.Errorf("Expected TANGLED_REPO_DID='did:plc:user123', got '%s'", env["TANGLED_REPO_DID"])
61
+
}
62
+
if env["TANGLED_REPO_NAME"] != "my-repo" {
63
+
t.Errorf("Expected TANGLED_REPO_NAME='my-repo', got '%s'", env["TANGLED_REPO_NAME"])
64
+
}
65
+
if env["TANGLED_REPO_DEFAULT_BRANCH"] != "main" {
66
+
t.Errorf("Expected TANGLED_REPO_DEFAULT_BRANCH='main', got '%s'", env["TANGLED_REPO_DEFAULT_BRANCH"])
67
+
}
68
+
if env["TANGLED_REPO_URL"] != "https://example.com/did:plc:user123/my-repo" {
69
+
t.Errorf("Expected TANGLED_REPO_URL='https://example.com/did:plc:user123/my-repo', got '%s'", env["TANGLED_REPO_URL"])
70
+
}
71
+
}
72
+
73
+
func TestPipelineEnvVars_PushTag(t *testing.T) {
74
+
tr := &tangled.Pipeline_TriggerMetadata{
75
+
Kind: string(workflow.TriggerKindPush),
76
+
Push: &tangled.Pipeline_PushTriggerData{
77
+
NewSha: "abc123def456",
78
+
OldSha: "000000000000",
79
+
Ref: "refs/tags/v1.2.3",
80
+
},
81
+
Repo: &tangled.Pipeline_TriggerRepo{
82
+
Knot: "example.com",
83
+
Did: "did:plc:user123",
84
+
Repo: "my-repo",
85
+
},
86
+
}
87
+
id := PipelineId{
88
+
Knot: "example.com",
89
+
Rkey: "123123",
90
+
}
91
+
env := PipelineEnvVars(tr, id, false)
92
+
93
+
if env["TANGLED_REF"] != "refs/tags/v1.2.3" {
94
+
t.Errorf("Expected TANGLED_REF='refs/tags/v1.2.3', got '%s'", env["TANGLED_REF"])
95
+
}
96
+
if env["TANGLED_REF_NAME"] != "v1.2.3" {
97
+
t.Errorf("Expected TANGLED_REF_NAME='v1.2.3', got '%s'", env["TANGLED_REF_NAME"])
98
+
}
99
+
if env["TANGLED_REF_TYPE"] != "tag" {
100
+
t.Errorf("Expected TANGLED_REF_TYPE='tag', got '%s'", env["TANGLED_REF_TYPE"])
101
+
}
102
+
}
103
+
104
+
func TestPipelineEnvVars_PullRequest(t *testing.T) {
105
+
tr := &tangled.Pipeline_TriggerMetadata{
106
+
Kind: string(workflow.TriggerKindPullRequest),
107
+
PullRequest: &tangled.Pipeline_PullRequestTriggerData{
108
+
SourceBranch: "feature-branch",
109
+
TargetBranch: "main",
110
+
SourceSha: "pr-sha-789",
111
+
Action: "opened",
112
+
},
113
+
Repo: &tangled.Pipeline_TriggerRepo{
114
+
Knot: "example.com",
115
+
Did: "did:plc:user123",
116
+
Repo: "my-repo",
117
+
},
118
+
}
119
+
id := PipelineId{
120
+
Knot: "example.com",
121
+
Rkey: "123123",
122
+
}
123
+
env := PipelineEnvVars(tr, id, false)
124
+
125
+
// Check ref variables for PR
126
+
if env["TANGLED_REF"] != "refs/heads/feature-branch" {
127
+
t.Errorf("Expected TANGLED_REF='refs/heads/feature-branch', got '%s'", env["TANGLED_REF"])
128
+
}
129
+
if env["TANGLED_REF_NAME"] != "feature-branch" {
130
+
t.Errorf("Expected TANGLED_REF_NAME='feature-branch', got '%s'", env["TANGLED_REF_NAME"])
131
+
}
132
+
if env["TANGLED_REF_TYPE"] != "branch" {
133
+
t.Errorf("Expected TANGLED_REF_TYPE='branch', got '%s'", env["TANGLED_REF_TYPE"])
134
+
}
135
+
136
+
// Check SHA variables
137
+
if env["TANGLED_SHA"] != "pr-sha-789" {
138
+
t.Errorf("Expected TANGLED_SHA='pr-sha-789', got '%s'", env["TANGLED_SHA"])
139
+
}
140
+
if env["TANGLED_COMMIT_SHA"] != "pr-sha-789" {
141
+
t.Errorf("Expected TANGLED_COMMIT_SHA='pr-sha-789', got '%s'", env["TANGLED_COMMIT_SHA"])
142
+
}
143
+
144
+
// Check PR-specific variables
145
+
if env["TANGLED_PR_SOURCE_BRANCH"] != "feature-branch" {
146
+
t.Errorf("Expected TANGLED_PR_SOURCE_BRANCH='feature-branch', got '%s'", env["TANGLED_PR_SOURCE_BRANCH"])
147
+
}
148
+
if env["TANGLED_PR_TARGET_BRANCH"] != "main" {
149
+
t.Errorf("Expected TANGLED_PR_TARGET_BRANCH='main', got '%s'", env["TANGLED_PR_TARGET_BRANCH"])
150
+
}
151
+
if env["TANGLED_PR_SOURCE_SHA"] != "pr-sha-789" {
152
+
t.Errorf("Expected TANGLED_PR_SOURCE_SHA='pr-sha-789', got '%s'", env["TANGLED_PR_SOURCE_SHA"])
153
+
}
154
+
if env["TANGLED_PR_ACTION"] != "opened" {
155
+
t.Errorf("Expected TANGLED_PR_ACTION='opened', got '%s'", env["TANGLED_PR_ACTION"])
156
+
}
157
+
}
158
+
159
+
func TestPipelineEnvVars_ManualWithInputs(t *testing.T) {
160
+
tr := &tangled.Pipeline_TriggerMetadata{
161
+
Kind: string(workflow.TriggerKindManual),
162
+
Manual: &tangled.Pipeline_ManualTriggerData{
163
+
Inputs: []*tangled.Pipeline_Pair{
164
+
{Key: "version", Value: "1.0.0"},
165
+
{Key: "environment", Value: "production"},
166
+
},
167
+
},
168
+
Repo: &tangled.Pipeline_TriggerRepo{
169
+
Knot: "example.com",
170
+
Did: "did:plc:user123",
171
+
Repo: "my-repo",
172
+
},
173
+
}
174
+
id := PipelineId{
175
+
Knot: "example.com",
176
+
Rkey: "123123",
177
+
}
178
+
env := PipelineEnvVars(tr, id, false)
179
+
180
+
// Check manual input variables
181
+
if env["TANGLED_INPUT_VERSION"] != "1.0.0" {
182
+
t.Errorf("Expected TANGLED_INPUT_VERSION='1.0.0', got '%s'", env["TANGLED_INPUT_VERSION"])
183
+
}
184
+
if env["TANGLED_INPUT_ENVIRONMENT"] != "production" {
185
+
t.Errorf("Expected TANGLED_INPUT_ENVIRONMENT='production', got '%s'", env["TANGLED_INPUT_ENVIRONMENT"])
186
+
}
187
+
188
+
// Manual triggers shouldn't have ref/sha variables
189
+
if _, ok := env["TANGLED_REF"]; ok {
190
+
t.Error("Manual trigger should not have TANGLED_REF")
191
+
}
192
+
if _, ok := env["TANGLED_SHA"]; ok {
193
+
t.Error("Manual trigger should not have TANGLED_SHA")
194
+
}
195
+
}
196
+
197
+
func TestPipelineEnvVars_DevMode(t *testing.T) {
198
+
tr := &tangled.Pipeline_TriggerMetadata{
199
+
Kind: string(workflow.TriggerKindPush),
200
+
Push: &tangled.Pipeline_PushTriggerData{
201
+
NewSha: "abc123",
202
+
Ref: "refs/heads/main",
203
+
},
204
+
Repo: &tangled.Pipeline_TriggerRepo{
205
+
Knot: "localhost:3000",
206
+
Did: "did:plc:user123",
207
+
Repo: "my-repo",
208
+
},
209
+
}
210
+
id := PipelineId{
211
+
Knot: "example.com",
212
+
Rkey: "123123",
213
+
}
214
+
env := PipelineEnvVars(tr, id, true)
215
+
216
+
// Dev mode should use http:// and replace localhost with host.docker.internal
217
+
expectedURL := "http://host.docker.internal:3000/did:plc:user123/my-repo"
218
+
if env["TANGLED_REPO_URL"] != expectedURL {
219
+
t.Errorf("Expected TANGLED_REPO_URL='%s', got '%s'", expectedURL, env["TANGLED_REPO_URL"])
220
+
}
221
+
}
222
+
223
+
func TestPipelineEnvVars_NilTrigger(t *testing.T) {
224
+
id := PipelineId{
225
+
Knot: "example.com",
226
+
Rkey: "123123",
227
+
}
228
+
env := PipelineEnvVars(nil, id, false)
229
+
230
+
if env != nil {
231
+
t.Error("Expected nil env for nil trigger")
232
+
}
233
+
}
234
+
235
+
func TestPipelineEnvVars_NilPushData(t *testing.T) {
236
+
tr := &tangled.Pipeline_TriggerMetadata{
237
+
Kind: string(workflow.TriggerKindPush),
238
+
Push: nil,
239
+
Repo: &tangled.Pipeline_TriggerRepo{
240
+
Knot: "example.com",
241
+
Did: "did:plc:user123",
242
+
Repo: "my-repo",
243
+
},
244
+
}
245
+
id := PipelineId{
246
+
Knot: "example.com",
247
+
Rkey: "123123",
248
+
}
249
+
env := PipelineEnvVars(tr, id, false)
250
+
251
+
// Should still have repo variables
252
+
if env["TANGLED_REPO_KNOT"] != "example.com" {
253
+
t.Errorf("Expected TANGLED_REPO_KNOT='example.com', got '%s'", env["TANGLED_REPO_KNOT"])
254
+
}
255
+
256
+
// Should not have ref/sha variables
257
+
if _, ok := env["TANGLED_REF"]; ok {
258
+
t.Error("Should not have TANGLED_REF when push data is nil")
259
+
}
260
+
}
+15
-7
spindle/secrets/openbao.go
+15
-7
spindle/secrets/openbao.go
···
13
13
)
14
14
15
15
type OpenBaoManager struct {
16
-
client *vault.Client
17
-
mountPath string
18
-
logger *slog.Logger
16
+
client *vault.Client
17
+
mountPath string
18
+
logger *slog.Logger
19
+
connectionTimeout time.Duration
19
20
}
20
21
21
22
type OpenBaoManagerOpt func(*OpenBaoManager)
···
26
27
}
27
28
}
28
29
30
+
func WithConnectionTimeout(timeout time.Duration) OpenBaoManagerOpt {
31
+
return func(v *OpenBaoManager) {
32
+
v.connectionTimeout = timeout
33
+
}
34
+
}
35
+
29
36
// NewOpenBaoManager creates a new OpenBao manager that connects to a Bao Proxy
30
37
// The proxyAddress should point to the local Bao Proxy (e.g., "http://127.0.0.1:8200")
31
38
// The proxy handles all authentication automatically via Auto-Auth
···
43
50
}
44
51
45
52
manager := &OpenBaoManager{
46
-
client: client,
47
-
mountPath: "spindle", // default KV v2 mount path
48
-
logger: logger,
53
+
client: client,
54
+
mountPath: "spindle", // default KV v2 mount path
55
+
logger: logger,
56
+
connectionTimeout: 10 * time.Second, // default connection timeout
49
57
}
50
58
51
59
for _, opt := range opts {
···
62
70
63
71
// testConnection verifies that we can connect to the proxy
64
72
func (v *OpenBaoManager) testConnection() error {
65
-
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
73
+
ctx, cancel := context.WithTimeout(context.Background(), v.connectionTimeout)
66
74
defer cancel()
67
75
68
76
// try token self-lookup as a quick way to verify proxy works
+5
-2
spindle/secrets/openbao_test.go
+5
-2
spindle/secrets/openbao_test.go
···
152
152
for _, tt := range tests {
153
153
t.Run(tt.name, func(t *testing.T) {
154
154
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
155
-
manager, err := NewOpenBaoManager(tt.proxyAddr, logger, tt.opts...)
155
+
// Use shorter timeout for tests to avoid long waits
156
+
opts := append(tt.opts, WithConnectionTimeout(1*time.Second))
157
+
manager, err := NewOpenBaoManager(tt.proxyAddr, logger, opts...)
156
158
157
159
if tt.expectError {
158
160
assert.Error(t, err)
···
596
598
597
599
// All these will fail because no real proxy is running
598
600
// but we can test that the configuration is properly accepted
599
-
manager, err := NewOpenBaoManager(tt.proxyAddr, logger)
601
+
// Use shorter timeout for tests to avoid long waits
602
+
manager, err := NewOpenBaoManager(tt.proxyAddr, logger, WithConnectionTimeout(1*time.Second))
600
603
assert.Error(t, err) // Expected because no real proxy
601
604
assert.Nil(t, manager)
602
605
assert.Contains(t, err.Error(), "failed to connect to bao proxy")
+11
spindle/server.go
+11
spindle/server.go
···
6
6
"encoding/json"
7
7
"fmt"
8
8
"log/slog"
9
+
"maps"
9
10
"net/http"
10
11
11
12
"github.com/go-chi/chi/v5"
···
311
312
312
313
workflows := make(map[models.Engine][]models.Workflow)
313
314
315
+
// Build pipeline environment variables once for all workflows
316
+
pipelineEnv := models.PipelineEnvVars(tpl.TriggerMetadata, pipelineId, s.cfg.Server.Dev)
317
+
314
318
for _, w := range tpl.Workflows {
315
319
if w != nil {
316
320
if _, ok := s.engs[w.Engine]; !ok {
···
335
339
if err != nil {
336
340
return err
337
341
}
342
+
343
+
// inject TANGLED_* env vars after InitWorkflow
344
+
// This prevents user-defined env vars from overriding them
345
+
if ewf.Environment == nil {
346
+
ewf.Environment = make(map[string]string)
347
+
}
348
+
maps.Copy(ewf.Environment, pipelineEnv)
338
349
339
350
workflows[eng] = append(workflows[eng], *ewf)
340
351
+194
types/commit.go
+194
types/commit.go
···
1
+
package types
2
+
3
+
import (
4
+
"bytes"
5
+
"encoding/json"
6
+
"fmt"
7
+
"maps"
8
+
"regexp"
9
+
"strings"
10
+
11
+
"github.com/go-git/go-git/v5/plumbing"
12
+
"github.com/go-git/go-git/v5/plumbing/object"
13
+
)
14
+
15
+
type Commit struct {
16
+
// hash of the commit object.
17
+
Hash plumbing.Hash `json:"hash,omitempty"`
18
+
19
+
// author is the original author of the commit.
20
+
Author object.Signature `json:"author"`
21
+
22
+
// committer is the one performing the commit, might be different from author.
23
+
Committer object.Signature `json:"committer"`
24
+
25
+
// message is the commit message, contains arbitrary text.
26
+
Message string `json:"message"`
27
+
28
+
// treehash is the hash of the root tree of the commit.
29
+
Tree string `json:"tree"`
30
+
31
+
// parents are the hashes of the parent commits of the commit.
32
+
ParentHashes []plumbing.Hash `json:"parent_hashes,omitempty"`
33
+
34
+
// pgpsignature is the pgp signature of the commit.
35
+
PGPSignature string `json:"pgp_signature,omitempty"`
36
+
37
+
// mergetag is the embedded tag object when a merge commit is created by
38
+
// merging a signed tag.
39
+
MergeTag string `json:"merge_tag,omitempty"`
40
+
41
+
// changeid is a unique identifier for the change (e.g., gerrit change-id).
42
+
ChangeId string `json:"change_id,omitempty"`
43
+
44
+
// extraheaders contains additional headers not captured by other fields.
45
+
ExtraHeaders map[string][]byte `json:"extra_headers,omitempty"`
46
+
47
+
// deprecated: kept for backwards compatibility with old json format.
48
+
This string `json:"this,omitempty"`
49
+
50
+
// deprecated: kept for backwards compatibility with old json format.
51
+
Parent string `json:"parent,omitempty"`
52
+
}
53
+
54
+
// types.Commit is an unify two commit structs:
55
+
// - git.object.Commit from
56
+
// - types.NiceDiff.commit
57
+
//
58
+
// to do this in backwards compatible fashion, we define the base struct
59
+
// to use the same fields as NiceDiff.Commit, and then we also unmarshal
60
+
// the struct fields from go-git structs, this custom unmarshal makes sense
61
+
// of both representations and unifies them to have maximal data in either
62
+
// form.
63
+
func (c *Commit) UnmarshalJSON(data []byte) error {
64
+
type Alias Commit
65
+
66
+
aux := &struct {
67
+
*object.Commit
68
+
*Alias
69
+
}{
70
+
Alias: (*Alias)(c),
71
+
}
72
+
73
+
if err := json.Unmarshal(data, aux); err != nil {
74
+
return err
75
+
}
76
+
77
+
c.FromGoGitCommit(aux.Commit)
78
+
79
+
return nil
80
+
}
81
+
82
+
// fill in as much of Commit as possible from the given go-git commit
83
+
func (c *Commit) FromGoGitCommit(gc *object.Commit) {
84
+
if gc == nil {
85
+
return
86
+
}
87
+
88
+
if c.Hash.IsZero() {
89
+
c.Hash = gc.Hash
90
+
}
91
+
if c.This == "" {
92
+
c.This = gc.Hash.String()
93
+
}
94
+
if isEmptySignature(c.Author) {
95
+
c.Author = gc.Author
96
+
}
97
+
if isEmptySignature(c.Committer) {
98
+
c.Committer = gc.Committer
99
+
}
100
+
if c.Message == "" {
101
+
c.Message = gc.Message
102
+
}
103
+
if c.Tree == "" {
104
+
c.Tree = gc.TreeHash.String()
105
+
}
106
+
if c.PGPSignature == "" {
107
+
c.PGPSignature = gc.PGPSignature
108
+
}
109
+
if c.MergeTag == "" {
110
+
c.MergeTag = gc.MergeTag
111
+
}
112
+
113
+
if len(c.ParentHashes) == 0 {
114
+
c.ParentHashes = gc.ParentHashes
115
+
}
116
+
if c.Parent == "" && len(gc.ParentHashes) > 0 {
117
+
c.Parent = gc.ParentHashes[0].String()
118
+
}
119
+
120
+
if len(c.ExtraHeaders) == 0 {
121
+
c.ExtraHeaders = make(map[string][]byte)
122
+
maps.Copy(c.ExtraHeaders, gc.ExtraHeaders)
123
+
}
124
+
125
+
if c.ChangeId == "" {
126
+
if v, ok := gc.ExtraHeaders["change-id"]; ok {
127
+
c.ChangeId = string(v)
128
+
}
129
+
}
130
+
}
131
+
132
+
func isEmptySignature(s object.Signature) bool {
133
+
return s.Email == "" && s.Name == "" && s.When.IsZero()
134
+
}
135
+
136
+
// produce a verifiable payload from this commit's metadata
137
+
func (c *Commit) Payload() string {
138
+
author := bytes.NewBuffer([]byte{})
139
+
c.Author.Encode(author)
140
+
141
+
committer := bytes.NewBuffer([]byte{})
142
+
c.Committer.Encode(committer)
143
+
144
+
payload := strings.Builder{}
145
+
146
+
fmt.Fprintf(&payload, "tree %s\n", c.Tree)
147
+
148
+
if len(c.ParentHashes) > 0 {
149
+
for _, p := range c.ParentHashes {
150
+
fmt.Fprintf(&payload, "parent %s\n", p.String())
151
+
}
152
+
} else {
153
+
// present for backwards compatibility
154
+
fmt.Fprintf(&payload, "parent %s\n", c.Parent)
155
+
}
156
+
157
+
fmt.Fprintf(&payload, "author %s\n", author.String())
158
+
fmt.Fprintf(&payload, "committer %s\n", committer.String())
159
+
160
+
if c.ChangeId != "" {
161
+
fmt.Fprintf(&payload, "change-id %s\n", c.ChangeId)
162
+
} else if v, ok := c.ExtraHeaders["change-id"]; ok {
163
+
fmt.Fprintf(&payload, "change-id %s\n", string(v))
164
+
}
165
+
166
+
fmt.Fprintf(&payload, "\n%s", c.Message)
167
+
168
+
return payload.String()
169
+
}
170
+
171
+
var (
172
+
coAuthorRegex = regexp.MustCompile(`(?im)^Co-authored-by:\s*(.+?)\s*<([^>]+)>`)
173
+
)
174
+
175
+
func (commit Commit) CoAuthors() []object.Signature {
176
+
var coAuthors []object.Signature
177
+
178
+
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
179
+
180
+
for _, match := range matches {
181
+
if len(match) >= 3 {
182
+
name := strings.TrimSpace(match[1])
183
+
email := strings.TrimSpace(match[2])
184
+
185
+
coAuthors = append(coAuthors, object.Signature{
186
+
Name: name,
187
+
Email: email,
188
+
When: commit.Committer.When,
189
+
})
190
+
}
191
+
}
192
+
193
+
return coAuthors
194
+
}
+2
-12
types/diff.go
+2
-12
types/diff.go
···
2
2
3
3
import (
4
4
"github.com/bluekeyes/go-gitdiff/gitdiff"
5
-
"github.com/go-git/go-git/v5/plumbing/object"
6
5
)
7
6
8
7
type DiffOpts struct {
···
43
42
44
43
// A nicer git diff representation.
45
44
type NiceDiff struct {
46
-
Commit struct {
47
-
Message string `json:"message"`
48
-
Author object.Signature `json:"author"`
49
-
This string `json:"this"`
50
-
Parent string `json:"parent"`
51
-
PGPSignature string `json:"pgp_signature"`
52
-
Committer object.Signature `json:"committer"`
53
-
Tree string `json:"tree"`
54
-
ChangedId string `json:"change_id"`
55
-
} `json:"commit"`
56
-
Stat struct {
45
+
Commit Commit `json:"commit"`
46
+
Stat struct {
57
47
FilesChanged int `json:"files_changed"`
58
48
Insertions int `json:"insertions"`
59
49
Deletions int `json:"deletions"`
+17
-17
types/repo.go
+17
-17
types/repo.go
···
8
8
)
9
9
10
10
type RepoIndexResponse struct {
11
-
IsEmpty bool `json:"is_empty"`
12
-
Ref string `json:"ref,omitempty"`
13
-
Readme string `json:"readme,omitempty"`
14
-
ReadmeFileName string `json:"readme_file_name,omitempty"`
15
-
Commits []*object.Commit `json:"commits,omitempty"`
16
-
Description string `json:"description,omitempty"`
17
-
Files []NiceTree `json:"files,omitempty"`
18
-
Branches []Branch `json:"branches,omitempty"`
19
-
Tags []*TagReference `json:"tags,omitempty"`
20
-
TotalCommits int `json:"total_commits,omitempty"`
11
+
IsEmpty bool `json:"is_empty"`
12
+
Ref string `json:"ref,omitempty"`
13
+
Readme string `json:"readme,omitempty"`
14
+
ReadmeFileName string `json:"readme_file_name,omitempty"`
15
+
Commits []Commit `json:"commits,omitempty"`
16
+
Description string `json:"description,omitempty"`
17
+
Files []NiceTree `json:"files,omitempty"`
18
+
Branches []Branch `json:"branches,omitempty"`
19
+
Tags []*TagReference `json:"tags,omitempty"`
20
+
TotalCommits int `json:"total_commits,omitempty"`
21
21
}
22
22
23
23
type RepoLogResponse struct {
24
-
Commits []*object.Commit `json:"commits,omitempty"`
25
-
Ref string `json:"ref,omitempty"`
26
-
Description string `json:"description,omitempty"`
27
-
Log bool `json:"log,omitempty"`
28
-
Total int `json:"total,omitempty"`
29
-
Page int `json:"page,omitempty"`
30
-
PerPage int `json:"per_page,omitempty"`
24
+
Commits []Commit `json:"commits,omitempty"`
25
+
Ref string `json:"ref,omitempty"`
26
+
Description string `json:"description,omitempty"`
27
+
Log bool `json:"log,omitempty"`
28
+
Total int `json:"total,omitempty"`
29
+
Page int `json:"page,omitempty"`
30
+
PerPage int `json:"per_page,omitempty"`
31
31
}
32
32
33
33
type RepoCommitResponse struct {