+19
__generated__/index.ts
+19
__generated__/index.ts
···
31
31
DefsClickthroughReposter: 'app.bsky.feed.defs#clickthroughReposter',
32
32
DefsContentModeUnspecified: 'app.bsky.feed.defs#contentModeUnspecified',
33
33
}
34
+
export const COM_ATPROTO_MODERATION = {
35
+
DefsReasonRude: 'com.atproto.moderation.defs#reasonRude',
36
+
DefsReasonSpam: 'com.atproto.moderation.defs#reasonSpam',
37
+
DefsReasonOther: 'com.atproto.moderation.defs#reasonOther',
38
+
DefsReasonAppeal: 'com.atproto.moderation.defs#reasonAppeal',
39
+
DefsReasonSexual: 'com.atproto.moderation.defs#reasonSexual',
40
+
DefsReasonViolation: 'com.atproto.moderation.defs#reasonViolation',
41
+
DefsReasonMisleading: 'com.atproto.moderation.defs#reasonMisleading',
42
+
}
34
43
35
44
export function createServer(options?: XrpcOptions): Server {
36
45
return new Server(options)
···
172
181
_server: Server
173
182
gallery: SocialGrainGalleryNS
174
183
graph: SocialGrainGraphNS
184
+
labeler: SocialGrainLabelerNS
175
185
actor: SocialGrainActorNS
176
186
177
187
constructor(server: Server) {
178
188
this._server = server
179
189
this.gallery = new SocialGrainGalleryNS(server)
180
190
this.graph = new SocialGrainGraphNS(server)
191
+
this.labeler = new SocialGrainLabelerNS(server)
181
192
this.actor = new SocialGrainActorNS(server)
182
193
}
183
194
}
···
191
202
}
192
203
193
204
export class SocialGrainGraphNS {
205
+
_server: Server
206
+
207
+
constructor(server: Server) {
208
+
this._server = server
209
+
}
210
+
}
211
+
212
+
export class SocialGrainLabelerNS {
194
213
_server: Server
195
214
196
215
constructor(server: Server) {
+275
__generated__/lexicons.ts
+275
__generated__/lexicons.ts
···
2527
2527
refs: ['lex:social.grain.photo.defs#photoView'],
2528
2528
},
2529
2529
},
2530
+
labels: {
2531
+
type: 'array',
2532
+
items: {
2533
+
type: 'ref',
2534
+
ref: 'lex:com.atproto.label.defs#label',
2535
+
},
2536
+
},
2530
2537
indexedAt: {
2531
2538
type: 'string',
2532
2539
format: 'datetime',
···
2554
2561
type: 'string',
2555
2562
maxLength: 1000,
2556
2563
},
2564
+
labels: {
2565
+
type: 'union',
2566
+
description:
2567
+
'Self-label values for this post. Effectively content warnings.',
2568
+
refs: ['lex:com.atproto.label.defs#selfLabels'],
2569
+
},
2557
2570
createdAt: {
2558
2571
type: 'string',
2559
2572
format: 'datetime',
···
2587
2600
},
2588
2601
},
2589
2602
},
2603
+
SocialGrainLabelerDefs: {
2604
+
lexicon: 1,
2605
+
id: 'social.grain.labeler.defs',
2606
+
defs: {
2607
+
labelerView: {
2608
+
type: 'object',
2609
+
required: ['uri', 'cid', 'creator', 'indexedAt'],
2610
+
properties: {
2611
+
uri: {
2612
+
type: 'string',
2613
+
format: 'at-uri',
2614
+
},
2615
+
cid: {
2616
+
type: 'string',
2617
+
format: 'cid',
2618
+
},
2619
+
creator: {
2620
+
type: 'ref',
2621
+
ref: 'lex:social.grain.actor.defs#profileView',
2622
+
},
2623
+
favoriteCount: {
2624
+
type: 'integer',
2625
+
minimum: 0,
2626
+
},
2627
+
viewer: {
2628
+
type: 'ref',
2629
+
ref: 'lex:social.grain.labeler.defs#labelerViewerState',
2630
+
},
2631
+
indexedAt: {
2632
+
type: 'string',
2633
+
format: 'datetime',
2634
+
},
2635
+
labels: {
2636
+
type: 'array',
2637
+
items: {
2638
+
type: 'ref',
2639
+
ref: 'lex:com.atproto.label.defs#label',
2640
+
},
2641
+
},
2642
+
},
2643
+
},
2644
+
labelerViewDetailed: {
2645
+
type: 'object',
2646
+
required: ['uri', 'cid', 'creator', 'policies', 'indexedAt'],
2647
+
properties: {
2648
+
uri: {
2649
+
type: 'string',
2650
+
format: 'at-uri',
2651
+
},
2652
+
cid: {
2653
+
type: 'string',
2654
+
format: 'cid',
2655
+
},
2656
+
creator: {
2657
+
type: 'ref',
2658
+
ref: 'lex:app.bsky.actor.defs#profileView',
2659
+
},
2660
+
policies: {
2661
+
type: 'ref',
2662
+
ref: 'lex:social.grain.actor.defs#labelerPolicies',
2663
+
},
2664
+
favoriteCount: {
2665
+
type: 'integer',
2666
+
minimum: 0,
2667
+
},
2668
+
viewer: {
2669
+
type: 'ref',
2670
+
ref: 'lex:social.grain.labeler.defs#labelerViewerState',
2671
+
},
2672
+
indexedAt: {
2673
+
type: 'string',
2674
+
format: 'datetime',
2675
+
},
2676
+
labels: {
2677
+
type: 'array',
2678
+
items: {
2679
+
type: 'ref',
2680
+
ref: 'lex:com.atproto.label.defs#label',
2681
+
},
2682
+
},
2683
+
reasonTypes: {
2684
+
description:
2685
+
"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.",
2686
+
type: 'array',
2687
+
items: {
2688
+
type: 'ref',
2689
+
ref: 'lex:com.atproto.moderation.defs#reasonType',
2690
+
},
2691
+
},
2692
+
subjectTypes: {
2693
+
description:
2694
+
'The set of subject types (account, record, etc) this service accepts reports on.',
2695
+
type: 'array',
2696
+
items: {
2697
+
type: 'ref',
2698
+
ref: 'lex:com.atproto.moderation.defs#subjectType',
2699
+
},
2700
+
},
2701
+
subjectCollections: {
2702
+
type: 'array',
2703
+
description:
2704
+
'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',
2705
+
items: {
2706
+
type: 'string',
2707
+
format: 'nsid',
2708
+
},
2709
+
},
2710
+
},
2711
+
},
2712
+
labelerViewerState: {
2713
+
type: 'object',
2714
+
properties: {
2715
+
like: {
2716
+
type: 'string',
2717
+
format: 'at-uri',
2718
+
},
2719
+
},
2720
+
},
2721
+
labelerPolicies: {
2722
+
type: 'object',
2723
+
required: ['labelValues'],
2724
+
properties: {
2725
+
labelValues: {
2726
+
type: 'array',
2727
+
description:
2728
+
'The label values which this labeler publishes. May include global or custom labels.',
2729
+
items: {
2730
+
type: 'ref',
2731
+
ref: 'lex:com.atproto.label.defs#labelValue',
2732
+
},
2733
+
},
2734
+
labelValueDefinitions: {
2735
+
type: 'array',
2736
+
description:
2737
+
'Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler.',
2738
+
items: {
2739
+
type: 'ref',
2740
+
ref: 'lex:com.atproto.label.defs#labelValueDefinition',
2741
+
},
2742
+
},
2743
+
},
2744
+
},
2745
+
},
2746
+
},
2747
+
SocialGrainLabelerService: {
2748
+
lexicon: 1,
2749
+
id: 'social.grain.labeler.service',
2750
+
defs: {
2751
+
main: {
2752
+
type: 'record',
2753
+
description: 'A declaration of the existence of labeler service.',
2754
+
key: 'literal:self',
2755
+
record: {
2756
+
type: 'object',
2757
+
required: ['policies', 'createdAt'],
2758
+
properties: {
2759
+
policies: {
2760
+
type: 'ref',
2761
+
ref: 'lex:app.bsky.labeler.defs#labelerPolicies',
2762
+
},
2763
+
labels: {
2764
+
type: 'union',
2765
+
refs: ['lex:com.atproto.label.defs#selfLabels'],
2766
+
},
2767
+
createdAt: {
2768
+
type: 'string',
2769
+
format: 'datetime',
2770
+
},
2771
+
reasonTypes: {
2772
+
description:
2773
+
"The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed.",
2774
+
type: 'array',
2775
+
items: {
2776
+
type: 'ref',
2777
+
ref: 'lex:com.atproto.moderation.defs#reasonType',
2778
+
},
2779
+
},
2780
+
subjectTypes: {
2781
+
description:
2782
+
'The set of subject types (account, record, etc) this service accepts reports on.',
2783
+
type: 'array',
2784
+
items: {
2785
+
type: 'ref',
2786
+
ref: 'lex:com.atproto.moderation.defs#subjectType',
2787
+
},
2788
+
},
2789
+
subjectCollections: {
2790
+
type: 'array',
2791
+
description:
2792
+
'Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type.',
2793
+
items: {
2794
+
type: 'string',
2795
+
format: 'nsid',
2796
+
},
2797
+
},
2798
+
},
2799
+
},
2800
+
},
2801
+
},
2802
+
},
2590
2803
SocialGrainFavorite: {
2591
2804
lexicon: 1,
2592
2805
id: 'social.grain.favorite',
···
2636
2849
type: 'string',
2637
2850
maxLength: 2560,
2638
2851
maxGraphemes: 256,
2852
+
},
2853
+
labels: {
2854
+
type: 'array',
2855
+
items: {
2856
+
ref: 'lex:com.atproto.label.defs#label',
2857
+
type: 'ref',
2858
+
},
2639
2859
},
2640
2860
avatar: {
2641
2861
type: 'string',
···
2962
3182
},
2963
3183
},
2964
3184
},
3185
+
ComAtprotoModerationDefs: {
3186
+
lexicon: 1,
3187
+
id: 'com.atproto.moderation.defs',
3188
+
defs: {
3189
+
reasonRude: {
3190
+
type: 'token',
3191
+
description:
3192
+
'Rude, harassing, explicit, or otherwise unwelcoming behavior',
3193
+
},
3194
+
reasonSpam: {
3195
+
type: 'token',
3196
+
description: 'Spam: frequent unwanted promotion, replies, mentions',
3197
+
},
3198
+
reasonType: {
3199
+
type: 'string',
3200
+
knownValues: [
3201
+
'com.atproto.moderation.defs#reasonSpam',
3202
+
'com.atproto.moderation.defs#reasonViolation',
3203
+
'com.atproto.moderation.defs#reasonMisleading',
3204
+
'com.atproto.moderation.defs#reasonSexual',
3205
+
'com.atproto.moderation.defs#reasonRude',
3206
+
'com.atproto.moderation.defs#reasonOther',
3207
+
'com.atproto.moderation.defs#reasonAppeal',
3208
+
],
3209
+
},
3210
+
reasonOther: {
3211
+
type: 'token',
3212
+
description: 'Other: reports not falling under another report category',
3213
+
},
3214
+
subjectType: {
3215
+
type: 'string',
3216
+
description: 'Tag describing a type of subject that might be reported.',
3217
+
knownValues: ['account', 'record', 'chat'],
3218
+
},
3219
+
reasonAppeal: {
3220
+
type: 'token',
3221
+
description: 'Appeal: appeal a previously taken moderation action',
3222
+
},
3223
+
reasonSexual: {
3224
+
type: 'token',
3225
+
description: 'Unwanted or mislabeled sexual content',
3226
+
},
3227
+
reasonViolation: {
3228
+
type: 'token',
3229
+
description: 'Direct violation of server rules, laws, terms of service',
3230
+
},
3231
+
reasonMisleading: {
3232
+
type: 'token',
3233
+
description: 'Misleading identity, affiliation, or content',
3234
+
},
3235
+
},
3236
+
},
2965
3237
} as const satisfies Record<string, LexiconDoc>
2966
3238
export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]
2967
3239
export const lexicons: Lexicons = new Lexicons(schemas)
···
3018
3290
SocialGrainGalleryDefs: 'social.grain.gallery.defs',
3019
3291
SocialGrainGallery: 'social.grain.gallery',
3020
3292
SocialGrainGraphFollow: 'social.grain.graph.follow',
3293
+
SocialGrainLabelerDefs: 'social.grain.labeler.defs',
3294
+
SocialGrainLabelerService: 'social.grain.labeler.service',
3021
3295
SocialGrainFavorite: 'social.grain.favorite',
3022
3296
SocialGrainActorDefs: 'social.grain.actor.defs',
3023
3297
SocialGrainActorProfile: 'social.grain.actor.profile',
···
3025
3299
SocialGrainPhoto: 'social.grain.photo',
3026
3300
ComAtprotoLabelDefs: 'com.atproto.label.defs',
3027
3301
ComAtprotoRepoStrongRef: 'com.atproto.repo.strongRef',
3302
+
ComAtprotoModerationDefs: 'com.atproto.moderation.defs',
3028
3303
} as const
+44
__generated__/types/com/atproto/moderation/defs.ts
+44
__generated__/types/com/atproto/moderation/defs.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { type ValidationResult, BlobRef } from "npm:@atproto/lexicon"
5
+
import { CID } from "npm:multiformats/cid"
6
+
import { validate as _validate } from '../../../../lexicons.ts'
7
+
import {
8
+
type $Typed,
9
+
is$typed as _is$typed,
10
+
type OmitKey,
11
+
} from '../../../../util.ts'
12
+
13
+
const is$typed = _is$typed,
14
+
validate = _validate
15
+
const id = 'com.atproto.moderation.defs'
16
+
/** Rude, harassing, explicit, or otherwise unwelcoming behavior */
17
+
export const REASONRUDE = `${id}#reasonRude`
18
+
/** Spam: frequent unwanted promotion, replies, mentions */
19
+
export const REASONSPAM = `${id}#reasonSpam`
20
+
21
+
export type ReasonType =
22
+
| 'com.atproto.moderation.defs#reasonSpam'
23
+
| 'com.atproto.moderation.defs#reasonViolation'
24
+
| 'com.atproto.moderation.defs#reasonMisleading'
25
+
| 'com.atproto.moderation.defs#reasonSexual'
26
+
| 'com.atproto.moderation.defs#reasonRude'
27
+
| 'com.atproto.moderation.defs#reasonOther'
28
+
| 'com.atproto.moderation.defs#reasonAppeal'
29
+
| (string & {})
30
+
31
+
/** Other: reports not falling under another report category */
32
+
export const REASONOTHER = `${id}#reasonOther`
33
+
34
+
/** Tag describing a type of subject that might be reported. */
35
+
export type SubjectType = 'account' | 'record' | 'chat' | (string & {})
36
+
37
+
/** Appeal: appeal a previously taken moderation action */
38
+
export const REASONAPPEAL = `${id}#reasonAppeal`
39
+
/** Unwanted or mislabeled sexual content */
40
+
export const REASONSEXUAL = `${id}#reasonSexual`
41
+
/** Direct violation of server rules, laws, terms of service */
42
+
export const REASONVIOLATION = `${id}#reasonViolation`
43
+
/** Misleading identity, affiliation, or content */
44
+
export const REASONMISLEADING = `${id}#reasonMisleading`
+1
-1
deno.json
+1
-1
deno.json
···
2
2
"imports": {
3
3
"$lexicon/": "./__generated__/",
4
4
"@atproto/syntax": "npm:@atproto/syntax@^0.4.0",
5
-
"@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.32",
5
+
"@bigmoves/bff": "jsr:@bigmoves/bff@0.3.0-beta.33",
6
6
"@luca/esbuild-deno-loader": "jsr:@luca/esbuild-deno-loader@^0.11.1",
7
7
"@std/http": "jsr:@std/http@^1.0.17",
8
8
"@std/path": "jsr:@std/path@^1.0.9",
+132
-352
deno.lock
+132
-352
deno.lock
···
2
2
"version": "5",
3
3
"specifiers": {
4
4
"jsr:@bigmoves/atproto-oauth-client@0.2": "0.2.0",
5
-
"jsr:@bigmoves/bff@0.3.0-beta.32": "0.3.0-beta.32",
5
+
"jsr:@bigmoves/bff@0.3.0-beta.33": "0.3.0-beta.33",
6
6
"jsr:@luca/esbuild-deno-loader@~0.11.1": "0.11.1",
7
7
"jsr:@std/assert@^1.0.13": "1.0.13",
8
8
"jsr:@std/bytes@^1.0.2": "1.0.6",
9
+
"jsr:@std/cache@0.2": "0.2.0",
9
10
"jsr:@std/cli@^1.0.18": "1.0.19",
10
11
"jsr:@std/encoding@^1.0.10": "1.0.10",
11
12
"jsr:@std/encoding@^1.0.5": "1.0.10",
···
21
22
"jsr:@std/path@^1.0.9": "1.1.0",
22
23
"jsr:@std/path@^1.1.0": "1.1.0",
23
24
"jsr:@std/streams@^1.0.9": "1.0.9",
24
-
"npm:@atproto-labs/handle-resolver-node@~0.1.14": "0.1.15",
25
+
"npm:@atproto-labs/handle-resolver-node@~0.1.14": "0.1.16",
25
26
"npm:@atproto-labs/simple-store@~0.1.2": "0.1.2",
26
-
"npm:@atproto/api@~0.15.7": "0.15.7",
27
+
"npm:@atproto/api@~0.15.7": "0.15.12",
27
28
"npm:@atproto/common@~0.4.10": "0.4.11",
28
29
"npm:@atproto/identity@~0.4.7": "0.4.8",
29
30
"npm:@atproto/jwk@0.1.4": "0.1.4",
30
-
"npm:@atproto/lex-cli@*": "0.8.1",
31
31
"npm:@atproto/lexicon@*": "0.4.11",
32
+
"npm:@atproto/lexicon@0.4.11": "0.4.11",
32
33
"npm:@atproto/lexicon@~0.4.11": "0.4.11",
33
-
"npm:@atproto/oauth-client@~0.3.13": "0.3.16",
34
-
"npm:@atproto/oauth-types@~0.2.4": "0.2.7",
34
+
"npm:@atproto/oauth-client@~0.3.13": "0.3.20",
35
+
"npm:@atproto/oauth-types@~0.2.4": "0.2.8",
35
36
"npm:@atproto/syntax@0.4": "0.4.0",
36
-
"npm:@atproto/xrpc-server@*": "0.7.18",
37
-
"npm:@tailwindcss/cli@*": "4.1.7",
38
-
"npm:@tailwindcss/cli@^4.1.4": "4.1.7",
37
+
"npm:@atproto/xrpc-server@*": "0.7.19",
38
+
"npm:@atproto/xrpc-server@0.7.18": "0.7.18",
39
+
"npm:@tailwindcss/cli@*": "4.1.8",
40
+
"npm:@tailwindcss/cli@^4.1.4": "4.1.8",
39
41
"npm:@types/node@*": "22.15.15",
40
42
"npm:clsx@^2.1.1": "2.1.1",
41
43
"npm:date-fns@^4.1.0": "4.1.0",
···
43
45
"npm:htmx.org@^1.9.12": "1.9.12",
44
46
"npm:hyperscript.org@~0.9.14": "0.9.14",
45
47
"npm:jose@5.9.6": "5.9.6",
46
-
"npm:multiformats@*": "13.3.4",
47
-
"npm:multiformats@^13.3.2": "13.3.4",
48
+
"npm:multiformats@*": "13.3.6",
49
+
"npm:multiformats@^13.3.2": "13.3.6",
48
50
"npm:popmotion@^11.0.5": "11.0.5",
49
-
"npm:preact-render-to-string@^6.5.13": "6.5.13_preact@10.26.6",
50
-
"npm:preact@^10.26.5": "10.26.6",
51
+
"npm:preact-render-to-string@^6.5.13": "6.5.13_preact@10.26.8",
52
+
"npm:preact@^10.26.5": "10.26.8",
51
53
"npm:sortablejs@^1.15.6": "1.15.6",
52
54
"npm:tailwind-merge@^3.2.0": "3.3.0",
53
-
"npm:tailwindcss@^4.1.4": "4.1.7",
55
+
"npm:tailwindcss@^4.1.4": "4.1.8",
54
56
"npm:typed-htmx@~0.3.1": "0.3.1"
55
57
},
56
58
"jsr": {
···
73
75
"npm:tailwind-merge"
74
76
]
75
77
},
76
-
"@bigmoves/bff@0.3.0-beta.32": {
77
-
"integrity": "d33581157c6d52bd9ecccdbcb090559377de71d28137ce7fdf3882740390a389",
78
+
"@bigmoves/bff@0.3.0-beta.33": {
79
+
"integrity": "6e1f7ca871be05270e45742a7a6576dc1bf3247de7fa1ec966a30a93d2a79556",
78
80
"dependencies": [
79
81
"jsr:@bigmoves/atproto-oauth-client",
80
82
"jsr:@std/assert",
83
+
"jsr:@std/cache",
81
84
"jsr:@std/fmt",
82
85
"jsr:@std/http@^1.0.13",
83
86
"jsr:@std/path@^1.0.8",
84
87
"npm:@atproto/api",
85
88
"npm:@atproto/common",
86
89
"npm:@atproto/identity",
90
+
"npm:@atproto/lexicon@0.4.11",
87
91
"npm:@atproto/lexicon@~0.4.11",
88
92
"npm:@atproto/oauth-client",
89
93
"npm:@atproto/syntax",
94
+
"npm:@atproto/xrpc-server@0.7.18",
90
95
"npm:clsx",
91
96
"npm:multiformats@^13.3.2",
92
97
"npm:preact",
···
111
116
"@std/bytes@1.0.6": {
112
117
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
113
118
},
119
+
"@std/cache@0.2.0": {
120
+
"integrity": "63a2ccd5a9e7c03e430f7d34dfcfd0d0cfc90731a1eaf8208f4c66e418fc3035"
121
+
},
114
122
"@std/cli@1.0.19": {
115
123
"integrity": "b3601a54891f89f3f738023af11960c4e6f7a45dc76cde39a6861124cba79e88"
116
124
},
···
160
168
"@jridgewell/trace-mapping"
161
169
]
162
170
},
163
-
"@atproto-labs/did-resolver@0.1.12": {
164
-
"integrity": "sha512-criWN7o21C5TFsauB+bGTlkqqerOU6gT2TbxdQVgZUWqNcfazUmUjT4gJAY02i+O4d3QmZa27fv9CcaRKWkSug==",
171
+
"@atproto-labs/did-resolver@0.1.13": {
172
+
"integrity": "sha512-DG3YNaCKc6PAIv1Gsz3E1Kufw2t14OBxe4LdKK7KKLCNoex51hm+A5yMevShe3BSll+QosqWYIEgkPSc5xBoGQ==",
165
173
"dependencies": [
166
174
"@atproto-labs/fetch",
167
175
"@atproto-labs/pipe",
···
171
179
"zod"
172
180
]
173
181
},
174
-
"@atproto-labs/fetch-node@0.1.8": {
175
-
"integrity": "sha512-OOTIhZNPEDDm7kaYU8iYRgzM+D5n3mP2iiBSyKuLakKTaZBL5WwYlUsJVsqX26SnUXtGEroOJEVJ6f66OcG80w==",
182
+
"@atproto-labs/fetch-node@0.1.9": {
183
+
"integrity": "sha512-8sHDDXZEzQptLu8ddUU/8U+THS6dumgPynVX0/1PjUYd4S/FWyPcz6yMIiVChTfzKnZvYRRz47+qvOKhydrHQw==",
176
184
"dependencies": [
177
185
"@atproto-labs/fetch",
178
186
"@atproto-labs/pipe",
179
187
"ipaddr.js@2.2.0",
180
-
"psl",
181
188
"undici"
182
189
]
183
190
},
184
-
"@atproto-labs/fetch@0.2.2": {
185
-
"integrity": "sha512-QyafkedbFeVaN20DYUpnY2hcArYxjdThPXbYMqOSoZhcvkrUqaw4xDND4wZB5TBD9cq2yqe9V6mcw9P4XQKQuQ==",
191
+
"@atproto-labs/fetch@0.2.3": {
192
+
"integrity": "sha512-NZtbJOCbxKUFRFKMpamT38PUQMY0hX0p7TG5AEYOPhZKZEP7dHZ1K2s1aB8MdVH0qxmqX7nQleNrrvLf09Zfdw==",
186
193
"dependencies": [
187
194
"@atproto-labs/pipe"
188
195
]
189
196
},
190
-
"@atproto-labs/handle-resolver-node@0.1.15": {
191
-
"integrity": "sha512-krl9KqfCCrGID35VAAHKBIiXOxe3gYxAtOJLYpZc5cOPFwnvPlAdhTYZLIc1dJRKDayi8gh6Q4XZRDv7i8dryg==",
197
+
"@atproto-labs/handle-resolver-node@0.1.16": {
198
+
"integrity": "sha512-i2F989zjyC7b/odrV3/tOpIT1IDIxR3F0khPG4REfOWcmJ89QcP8BiejJ6KFJk3hbTJHq6X9/pTG1vesCvyIKA==",
192
199
"dependencies": [
193
200
"@atproto-labs/fetch-node",
194
201
"@atproto-labs/handle-resolver",
···
204
211
"zod"
205
212
]
206
213
},
207
-
"@atproto-labs/identity-resolver@0.1.16": {
208
-
"integrity": "sha512-pFrtKT49cYBhCDd2U1t/CcUBiMmQzaNQxh8oSkDUlGs/K3P8rJFTAGAMm8UjokfGEKwF4hX9oo7O8Kn+GkyExw==",
214
+
"@atproto-labs/identity-resolver@0.1.17": {
215
+
"integrity": "sha512-EaH9Lm8M85IKRx+oWZ4tppYRVH8u+MYpEz1kjzYeM3ttZ2xcqKVmYHiOIgd5YPCVV2EIfXKnlM4soHQ+rZ1c6A==",
209
216
"dependencies": [
210
217
"@atproto-labs/did-resolver",
211
218
"@atproto-labs/handle-resolver",
212
219
"@atproto/syntax"
213
220
]
214
221
},
215
-
"@atproto-labs/pipe@0.1.0": {
216
-
"integrity": "sha512-ghOqHFyJlQVFPESzlVHjKroP0tPzbmG5Jms0dNI9yLDEfL8xp4OFPWLX4f6T8mRq69wWs4nIDM3sSsFbFqLa1w=="
222
+
"@atproto-labs/pipe@0.1.1": {
223
+
"integrity": "sha512-hdNw2oUs2B6BN1lp+32pF7cp8EMKuIN5Qok2Vvv/aOpG/3tNSJ9YkvfI0k6Zd188LeDDYRUpYpxcoFIcGH/FNg=="
217
224
},
218
225
"@atproto-labs/simple-store-memory@0.1.3": {
219
226
"integrity": "sha512-jkitT9+AtU+0b28DoN92iURLaCt/q/q4yX8q6V+9LSwYlUTqKoj/5NFKvF7x6EBuG+gpUdlcycbH7e60gjOhRQ==",
···
228
235
"@atproto-labs/simple-store@0.2.0": {
229
236
"integrity": "sha512-0bRbAlI8Ayh03wRwncAMEAyUKtZ+AuTS1jgPrfym1WVOAOiottI/ZmgccqLl6w5MbxVcClNQF7WYGKvGwGoIhA=="
230
237
},
231
-
"@atproto/api@0.15.7": {
232
-
"integrity": "sha512-YRETLcOwDCYfGs7Sl9ObqPwhOlVWrPkw4f1AYGIrXLQS58WHe/vz1lZbqOqMsC6gvCnyZnOuKlhsRHZ14rBLzg==",
238
+
"@atproto/api@0.15.12": {
239
+
"integrity": "sha512-51IHenZMA+Ekfe2OlZL/mTFqvZQU93jI4xsLvTFhGc4tSQYCHV9r/AJTANPZLFrhm9GfWZ0n90r/9IQl9eicjg==",
233
240
"dependencies": [
234
241
"@atproto/common-web",
235
242
"@atproto/lexicon",
···
289
296
"zod"
290
297
]
291
298
},
292
-
"@atproto/jwk@0.1.5": {
293
-
"integrity": "sha512-OzZFLhX41TOcMeanP3aZlL5bLeaUIZT15MI4aU5cwflNq/rwpGOpz3uwDjZc8ytgUjuTQ8LabSz5jMmwoTSWFg==",
299
+
"@atproto/jwk@0.2.0": {
300
+
"integrity": "sha512-foOxExbw04XCaoLaGdv9BQj0Ac7snZsk6IpQjOsjYatf+i62Pi9bUkZ0MAoA75HPk8ZmKoDnbA60uBMmiOPPHQ==",
294
301
"dependencies": [
295
302
"multiformats@9.9.0",
296
303
"zod"
297
304
]
298
305
},
299
-
"@atproto/lex-cli@0.8.1": {
300
-
"integrity": "sha512-0Ns6kX46gum2jU8bpvWCSVqoYhjmJrOGR/NLfLHgPbJtBlyxMGQAxqpy1x6zOi6SkkRGWYhHvRfr5J8lTHbxjA==",
301
-
"dependencies": [
302
-
"@atproto/lexicon",
303
-
"@atproto/syntax",
304
-
"chalk",
305
-
"commander@9.5.0",
306
-
"prettier",
307
-
"ts-morph",
308
-
"yesno",
309
-
"zod"
310
-
],
311
-
"bin": true
312
-
},
313
306
"@atproto/lexicon@0.4.11": {
314
307
"integrity": "sha512-btefdnvNz2Ao2I+qbmj0F06HC8IlrM/IBz6qOBS50r0S6uDf5tOO+Mv2tSVdimFkdzyDdLtBI1sV36ONxz2cOw==",
315
308
"dependencies": [
···
320
313
"zod"
321
314
]
322
315
},
323
-
"@atproto/oauth-client@0.3.16": {
324
-
"integrity": "sha512-AEtGLOXRJzBcBa8LyUXwFf/M7cZc+CcOBjLsiqmVQriSwccfyTkALgiyM0UcRHJqlwtLPuf9RYtgKPc8rW5F/w==",
316
+
"@atproto/oauth-client@0.3.20": {
317
+
"integrity": "sha512-aclxN2vD5ldc9YiQtX6z4S5g5lU12sz297gzuTxBFUYiS3bh7dxU8j/cbD/BDvXIiVRzzzc5kOgE1CgT9XZ2mg==",
325
318
"dependencies": [
326
319
"@atproto-labs/did-resolver",
327
320
"@atproto-labs/fetch",
···
330
323
"@atproto-labs/simple-store@0.2.0",
331
324
"@atproto-labs/simple-store-memory",
332
325
"@atproto/did",
333
-
"@atproto/jwk@0.1.5",
326
+
"@atproto/jwk@0.2.0",
334
327
"@atproto/oauth-types",
335
328
"@atproto/xrpc",
336
329
"multiformats@9.9.0",
337
330
"zod"
338
331
]
339
332
},
340
-
"@atproto/oauth-types@0.2.7": {
341
-
"integrity": "sha512-2SlDveiSI0oowC+sfuNd/npV8jw/FhokSS26qyUyldTg1g9ZlhxXUfMP4IZOPeZcVn9EszzQRHs1H9ZJqVQIew==",
333
+
"@atproto/oauth-types@0.2.8": {
334
+
"integrity": "sha512-xcYI2JmhrWwscePDoaKeDawVCCZkcvBqrBFMpMk4gf/OujH0pNSKBD/aWsayc6WvujVbTqwrG2hwPLfRqzJbwg==",
342
335
"dependencies": [
343
-
"@atproto/jwk@0.1.5",
336
+
"@atproto/jwk@0.2.0",
344
337
"zod"
345
338
]
346
339
},
···
349
342
},
350
343
"@atproto/xrpc-server@0.7.18": {
351
344
"integrity": "sha512-kjlAsI+UNbbm6AK3Y5Hb4BJ7VQHNKiYYu2kX5vhZJZHO8qfO40GPYYb/2TknZV8IG6fDPBQhUpcDRolI86sgag==",
345
+
"dependencies": [
346
+
"@atproto/common",
347
+
"@atproto/crypto",
348
+
"@atproto/lexicon",
349
+
"@atproto/xrpc",
350
+
"cbor-x",
351
+
"express",
352
+
"http-errors",
353
+
"mime-types",
354
+
"rate-limiter-flexible",
355
+
"uint8arrays",
356
+
"ws",
357
+
"zod"
358
+
]
359
+
},
360
+
"@atproto/xrpc-server@0.7.19": {
361
+
"integrity": "sha512-YSCl/tU2NDykgDYslFSOYCr96esUgDwncFiADKL59/fyIFPLoT0qY8Uq/budpxUh0qPzjow4HHgVWESOaOpUmA==",
352
362
"dependencies": [
353
363
"@atproto/common",
354
364
"@atproto/crypto",
···
419
429
"dependencies": [
420
430
"tslib@2.8.1"
421
431
]
422
-
},
423
-
"@esbuild/aix-ppc64@0.25.4": {
424
-
"integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
425
-
"os": ["aix"],
426
-
"cpu": ["ppc64"]
427
432
},
428
433
"@esbuild/aix-ppc64@0.25.5": {
429
434
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
430
435
"os": ["aix"],
431
436
"cpu": ["ppc64"]
432
437
},
433
-
"@esbuild/android-arm64@0.25.4": {
434
-
"integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
435
-
"os": ["android"],
436
-
"cpu": ["arm64"]
437
-
},
438
438
"@esbuild/android-arm64@0.25.5": {
439
439
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
440
440
"os": ["android"],
441
441
"cpu": ["arm64"]
442
442
},
443
-
"@esbuild/android-arm@0.25.4": {
444
-
"integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
445
-
"os": ["android"],
446
-
"cpu": ["arm"]
447
-
},
448
443
"@esbuild/android-arm@0.25.5": {
449
444
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
450
445
"os": ["android"],
451
446
"cpu": ["arm"]
452
447
},
453
-
"@esbuild/android-x64@0.25.4": {
454
-
"integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
455
-
"os": ["android"],
456
-
"cpu": ["x64"]
457
-
},
458
448
"@esbuild/android-x64@0.25.5": {
459
449
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
460
450
"os": ["android"],
461
451
"cpu": ["x64"]
462
452
},
463
-
"@esbuild/darwin-arm64@0.25.4": {
464
-
"integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
465
-
"os": ["darwin"],
466
-
"cpu": ["arm64"]
467
-
},
468
453
"@esbuild/darwin-arm64@0.25.5": {
469
454
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
470
455
"os": ["darwin"],
471
456
"cpu": ["arm64"]
472
457
},
473
-
"@esbuild/darwin-x64@0.25.4": {
474
-
"integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
475
-
"os": ["darwin"],
476
-
"cpu": ["x64"]
477
-
},
478
458
"@esbuild/darwin-x64@0.25.5": {
479
459
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
480
460
"os": ["darwin"],
481
461
"cpu": ["x64"]
482
462
},
483
-
"@esbuild/freebsd-arm64@0.25.4": {
484
-
"integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
485
-
"os": ["freebsd"],
486
-
"cpu": ["arm64"]
487
-
},
488
463
"@esbuild/freebsd-arm64@0.25.5": {
489
464
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
490
465
"os": ["freebsd"],
491
466
"cpu": ["arm64"]
492
467
},
493
-
"@esbuild/freebsd-x64@0.25.4": {
494
-
"integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
495
-
"os": ["freebsd"],
496
-
"cpu": ["x64"]
497
-
},
498
468
"@esbuild/freebsd-x64@0.25.5": {
499
469
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
500
470
"os": ["freebsd"],
501
471
"cpu": ["x64"]
502
472
},
503
-
"@esbuild/linux-arm64@0.25.4": {
504
-
"integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
505
-
"os": ["linux"],
506
-
"cpu": ["arm64"]
507
-
},
508
473
"@esbuild/linux-arm64@0.25.5": {
509
474
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
510
475
"os": ["linux"],
511
476
"cpu": ["arm64"]
512
477
},
513
-
"@esbuild/linux-arm@0.25.4": {
514
-
"integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
515
-
"os": ["linux"],
516
-
"cpu": ["arm"]
517
-
},
518
478
"@esbuild/linux-arm@0.25.5": {
519
479
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
520
480
"os": ["linux"],
521
481
"cpu": ["arm"]
522
482
},
523
-
"@esbuild/linux-ia32@0.25.4": {
524
-
"integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
525
-
"os": ["linux"],
526
-
"cpu": ["ia32"]
527
-
},
528
483
"@esbuild/linux-ia32@0.25.5": {
529
484
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
530
485
"os": ["linux"],
531
486
"cpu": ["ia32"]
532
487
},
533
-
"@esbuild/linux-loong64@0.25.4": {
534
-
"integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
535
-
"os": ["linux"],
536
-
"cpu": ["loong64"]
537
-
},
538
488
"@esbuild/linux-loong64@0.25.5": {
539
489
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
540
490
"os": ["linux"],
541
491
"cpu": ["loong64"]
542
492
},
543
-
"@esbuild/linux-mips64el@0.25.4": {
544
-
"integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
545
-
"os": ["linux"],
546
-
"cpu": ["mips64el"]
547
-
},
548
493
"@esbuild/linux-mips64el@0.25.5": {
549
494
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
550
495
"os": ["linux"],
551
496
"cpu": ["mips64el"]
552
497
},
553
-
"@esbuild/linux-ppc64@0.25.4": {
554
-
"integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
555
-
"os": ["linux"],
556
-
"cpu": ["ppc64"]
557
-
},
558
498
"@esbuild/linux-ppc64@0.25.5": {
559
499
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
560
500
"os": ["linux"],
561
501
"cpu": ["ppc64"]
562
502
},
563
-
"@esbuild/linux-riscv64@0.25.4": {
564
-
"integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
565
-
"os": ["linux"],
566
-
"cpu": ["riscv64"]
567
-
},
568
503
"@esbuild/linux-riscv64@0.25.5": {
569
504
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
570
505
"os": ["linux"],
571
506
"cpu": ["riscv64"]
572
507
},
573
-
"@esbuild/linux-s390x@0.25.4": {
574
-
"integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
575
-
"os": ["linux"],
576
-
"cpu": ["s390x"]
577
-
},
578
508
"@esbuild/linux-s390x@0.25.5": {
579
509
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
580
510
"os": ["linux"],
581
511
"cpu": ["s390x"]
582
512
},
583
-
"@esbuild/linux-x64@0.25.4": {
584
-
"integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
585
-
"os": ["linux"],
586
-
"cpu": ["x64"]
587
-
},
588
513
"@esbuild/linux-x64@0.25.5": {
589
514
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
590
515
"os": ["linux"],
591
516
"cpu": ["x64"]
592
517
},
593
-
"@esbuild/netbsd-arm64@0.25.4": {
594
-
"integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
595
-
"os": ["netbsd"],
596
-
"cpu": ["arm64"]
597
-
},
598
518
"@esbuild/netbsd-arm64@0.25.5": {
599
519
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
600
520
"os": ["netbsd"],
601
521
"cpu": ["arm64"]
602
522
},
603
-
"@esbuild/netbsd-x64@0.25.4": {
604
-
"integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
605
-
"os": ["netbsd"],
606
-
"cpu": ["x64"]
607
-
},
608
523
"@esbuild/netbsd-x64@0.25.5": {
609
524
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
610
525
"os": ["netbsd"],
611
526
"cpu": ["x64"]
612
527
},
613
-
"@esbuild/openbsd-arm64@0.25.4": {
614
-
"integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
615
-
"os": ["openbsd"],
616
-
"cpu": ["arm64"]
617
-
},
618
528
"@esbuild/openbsd-arm64@0.25.5": {
619
529
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
620
530
"os": ["openbsd"],
621
531
"cpu": ["arm64"]
622
532
},
623
-
"@esbuild/openbsd-x64@0.25.4": {
624
-
"integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
625
-
"os": ["openbsd"],
626
-
"cpu": ["x64"]
627
-
},
628
533
"@esbuild/openbsd-x64@0.25.5": {
629
534
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
630
535
"os": ["openbsd"],
631
536
"cpu": ["x64"]
632
537
},
633
-
"@esbuild/sunos-x64@0.25.4": {
634
-
"integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
635
-
"os": ["sunos"],
636
-
"cpu": ["x64"]
637
-
},
638
538
"@esbuild/sunos-x64@0.25.5": {
639
539
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
640
540
"os": ["sunos"],
641
541
"cpu": ["x64"]
642
542
},
643
-
"@esbuild/win32-arm64@0.25.4": {
644
-
"integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
645
-
"os": ["win32"],
646
-
"cpu": ["arm64"]
647
-
},
648
543
"@esbuild/win32-arm64@0.25.5": {
649
544
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
650
545
"os": ["win32"],
651
546
"cpu": ["arm64"]
652
547
},
653
-
"@esbuild/win32-ia32@0.25.4": {
654
-
"integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
655
-
"os": ["win32"],
656
-
"cpu": ["ia32"]
657
-
},
658
548
"@esbuild/win32-ia32@0.25.5": {
659
549
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
660
550
"os": ["win32"],
661
551
"cpu": ["ia32"]
662
-
},
663
-
"@esbuild/win32-x64@0.25.4": {
664
-
"integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
665
-
"os": ["win32"],
666
-
"cpu": ["x64"]
667
552
},
668
553
"@esbuild/win32-x64@0.25.5": {
669
554
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
···
722
607
"@tybys/wasm-util"
723
608
]
724
609
},
725
-
"@noble/curves@1.9.1": {
726
-
"integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
610
+
"@noble/curves@1.9.2": {
611
+
"integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==",
727
612
"dependencies": [
728
613
"@noble/hashes"
729
614
]
···
821
706
],
822
707
"scripts": true
823
708
},
824
-
"@tailwindcss/cli@4.1.7": {
825
-
"integrity": "sha512-hJNjpov/UiJc9ZWH4j/eEQxqklADrD/71s+t8Y0wbyQVAwtLkSp+MeC/sHTb03X+28rfbe0fRXkiBsf73/IwPg==",
709
+
"@tailwindcss/cli@4.1.8": {
710
+
"integrity": "sha512-+6lkjXSr/68zWiabK3mVYVHmOq/SAHjJ13mR8spyB4LgUWZbWzU9kCSErlAUo+gK5aVfgqe8kY6Ltz9+nz5XYA==",
826
711
"dependencies": [
827
712
"@parcel/watcher",
828
713
"@tailwindcss/node",
···
834
719
],
835
720
"bin": true
836
721
},
837
-
"@tailwindcss/node@4.1.7": {
838
-
"integrity": "sha512-9rsOpdY9idRI2NH6CL4wORFY0+Q6fnx9XP9Ju+iq/0wJwGD5IByIgFmwVbyy4ymuyprj8Qh4ErxMKTUL4uNh3g==",
722
+
"@tailwindcss/node@4.1.8": {
723
+
"integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==",
839
724
"dependencies": [
840
725
"@ampproject/remapping",
841
726
"enhanced-resolve",
···
846
731
"tailwindcss"
847
732
]
848
733
},
849
-
"@tailwindcss/oxide-android-arm64@4.1.7": {
850
-
"integrity": "sha512-IWA410JZ8fF7kACus6BrUwY2Z1t1hm0+ZWNEzykKmMNM09wQooOcN/VXr0p/WJdtHZ90PvJf2AIBS/Ceqx1emg==",
734
+
"@tailwindcss/oxide-android-arm64@4.1.8": {
735
+
"integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==",
851
736
"os": ["android"],
852
737
"cpu": ["arm64"]
853
738
},
854
-
"@tailwindcss/oxide-darwin-arm64@4.1.7": {
855
-
"integrity": "sha512-81jUw9To7fimGGkuJ2W5h3/oGonTOZKZ8C2ghm/TTxbwvfSiFSDPd6/A/KE2N7Jp4mv3Ps9OFqg2fEKgZFfsvg==",
739
+
"@tailwindcss/oxide-darwin-arm64@4.1.8": {
740
+
"integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==",
856
741
"os": ["darwin"],
857
742
"cpu": ["arm64"]
858
743
},
859
-
"@tailwindcss/oxide-darwin-x64@4.1.7": {
860
-
"integrity": "sha512-q77rWjEyGHV4PdDBtrzO0tgBBPlQWKY7wZK0cUok/HaGgbNKecegNxCGikuPJn5wFAlIywC3v+WMBt0PEBtwGw==",
744
+
"@tailwindcss/oxide-darwin-x64@4.1.8": {
745
+
"integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==",
861
746
"os": ["darwin"],
862
747
"cpu": ["x64"]
863
748
},
864
-
"@tailwindcss/oxide-freebsd-x64@4.1.7": {
865
-
"integrity": "sha512-RfmdbbK6G6ptgF4qqbzoxmH+PKfP4KSVs7SRlTwcbRgBwezJkAO3Qta/7gDy10Q2DcUVkKxFLXUQO6J3CRvBGw==",
749
+
"@tailwindcss/oxide-freebsd-x64@4.1.8": {
750
+
"integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==",
866
751
"os": ["freebsd"],
867
752
"cpu": ["x64"]
868
753
},
869
-
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.7": {
870
-
"integrity": "sha512-OZqsGvpwOa13lVd1z6JVwQXadEobmesxQ4AxhrwRiPuE04quvZHWn/LnihMg7/XkN+dTioXp/VMu/p6A5eZP3g==",
754
+
"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8": {
755
+
"integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==",
871
756
"os": ["linux"],
872
757
"cpu": ["arm"]
873
758
},
874
-
"@tailwindcss/oxide-linux-arm64-gnu@4.1.7": {
875
-
"integrity": "sha512-voMvBTnJSfKecJxGkoeAyW/2XRToLZ227LxswLAwKY7YslG/Xkw9/tJNH+3IVh5bdYzYE7DfiaPbRkSHFxY1xA==",
759
+
"@tailwindcss/oxide-linux-arm64-gnu@4.1.8": {
760
+
"integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==",
876
761
"os": ["linux"],
877
762
"cpu": ["arm64"]
878
763
},
879
-
"@tailwindcss/oxide-linux-arm64-musl@4.1.7": {
880
-
"integrity": "sha512-PjGuNNmJeKHnP58M7XyjJyla8LPo+RmwHQpBI+W/OxqrwojyuCQ+GUtygu7jUqTEexejZHr/z3nBc/gTiXBj4A==",
764
+
"@tailwindcss/oxide-linux-arm64-musl@4.1.8": {
765
+
"integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==",
881
766
"os": ["linux"],
882
767
"cpu": ["arm64"]
883
768
},
884
-
"@tailwindcss/oxide-linux-x64-gnu@4.1.7": {
885
-
"integrity": "sha512-HMs+Va+ZR3gC3mLZE00gXxtBo3JoSQxtu9lobbZd+DmfkIxR54NO7Z+UQNPsa0P/ITn1TevtFxXTpsRU7qEvWg==",
769
+
"@tailwindcss/oxide-linux-x64-gnu@4.1.8": {
770
+
"integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==",
886
771
"os": ["linux"],
887
772
"cpu": ["x64"]
888
773
},
889
-
"@tailwindcss/oxide-linux-x64-musl@4.1.7": {
890
-
"integrity": "sha512-MHZ6jyNlutdHH8rd+YTdr3QbXrHXqwIhHw9e7yXEBcQdluGwhpQY2Eku8UZK6ReLaWtQ4gijIv5QoM5eE+qlsA==",
774
+
"@tailwindcss/oxide-linux-x64-musl@4.1.8": {
775
+
"integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==",
891
776
"os": ["linux"],
892
777
"cpu": ["x64"]
893
778
},
894
-
"@tailwindcss/oxide-wasm32-wasi@4.1.7": {
895
-
"integrity": "sha512-ANaSKt74ZRzE2TvJmUcbFQ8zS201cIPxUDm5qez5rLEwWkie2SkGtA4P+GPTj+u8N6JbPrC8MtY8RmJA35Oo+A==",
779
+
"@tailwindcss/oxide-wasm32-wasi@4.1.8": {
780
+
"integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==",
896
781
"dependencies": [
897
782
"@emnapi/core",
898
783
"@emnapi/runtime",
···
903
788
],
904
789
"cpu": ["wasm32"]
905
790
},
906
-
"@tailwindcss/oxide-win32-arm64-msvc@4.1.7": {
907
-
"integrity": "sha512-HUiSiXQ9gLJBAPCMVRk2RT1ZrBjto7WvqsPBwUrNK2BcdSxMnk19h4pjZjI7zgPhDxlAbJSumTC4ljeA9y0tEw==",
791
+
"@tailwindcss/oxide-win32-arm64-msvc@4.1.8": {
792
+
"integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==",
908
793
"os": ["win32"],
909
794
"cpu": ["arm64"]
910
795
},
911
-
"@tailwindcss/oxide-win32-x64-msvc@4.1.7": {
912
-
"integrity": "sha512-rYHGmvoHiLJ8hWucSfSOEmdCBIGZIq7SpkPRSqLsH2Ab2YUNgKeAPT1Fi2cx3+hnYOrAb0jp9cRyode3bBW4mQ==",
796
+
"@tailwindcss/oxide-win32-x64-msvc@4.1.8": {
797
+
"integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==",
913
798
"os": ["win32"],
914
799
"cpu": ["x64"]
915
800
},
916
-
"@tailwindcss/oxide@4.1.7": {
917
-
"integrity": "sha512-5SF95Ctm9DFiUyjUPnDGkoKItPX/k+xifcQhcqX5RA85m50jw1pT/KzjdvlqxRja45Y52nR4MR9fD1JYd7f8NQ==",
801
+
"@tailwindcss/oxide@4.1.8": {
802
+
"integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==",
918
803
"dependencies": [
919
804
"detect-libc@2.0.4",
920
805
"tar"
···
935
820
],
936
821
"scripts": true
937
822
},
938
-
"@ts-morph/common@0.25.0": {
939
-
"integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==",
940
-
"dependencies": [
941
-
"minimatch",
942
-
"path-browserify",
943
-
"tinyglobby"
944
-
]
945
-
},
946
823
"@tybys/wasm-util@0.9.0": {
947
824
"integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==",
948
825
"dependencies": [
···
972
849
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
973
850
"bin": true
974
851
},
975
-
"ansi-styles@4.3.0": {
976
-
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
977
-
"dependencies": [
978
-
"color-convert"
979
-
]
980
-
},
981
852
"array-flatten@1.1.1": {
982
853
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
983
854
},
···
986
857
},
987
858
"await-lock@2.2.2": {
988
859
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw=="
989
-
},
990
-
"balanced-match@1.0.2": {
991
-
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
992
860
},
993
861
"base64-js@1.5.1": {
994
862
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
···
1008
876
"raw-body",
1009
877
"type-is",
1010
878
"unpipe"
1011
-
]
1012
-
},
1013
-
"brace-expansion@2.0.1": {
1014
-
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
1015
-
"dependencies": [
1016
-
"balanced-match"
1017
879
]
1018
880
},
1019
881
"braces@3.0.3": {
···
1075
937
"integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==",
1076
938
"bin": true
1077
939
},
1078
-
"chalk@4.1.2": {
1079
-
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
1080
-
"dependencies": [
1081
-
"ansi-styles",
1082
-
"supports-color"
1083
-
]
1084
-
},
1085
940
"chownr@3.0.0": {
1086
941
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="
1087
942
},
1088
943
"clsx@2.1.1": {
1089
944
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
1090
945
},
1091
-
"code-block-writer@13.0.3": {
1092
-
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="
1093
-
},
1094
-
"color-convert@2.0.1": {
1095
-
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1096
-
"dependencies": [
1097
-
"color-name"
1098
-
]
1099
-
},
1100
-
"color-name@1.1.4": {
1101
-
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
1102
-
},
1103
946
"commander@2.20.3": {
1104
947
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
1105
-
},
1106
-
"commander@9.5.0": {
1107
-
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="
1108
948
},
1109
949
"content-disposition@0.5.4": {
1110
950
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
···
1182
1022
"esbuild@0.25.5": {
1183
1023
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
1184
1024
"optionalDependencies": [
1185
-
"@esbuild/aix-ppc64@0.25.5",
1186
-
"@esbuild/android-arm@0.25.5",
1187
-
"@esbuild/android-arm64@0.25.5",
1188
-
"@esbuild/android-x64@0.25.5",
1189
-
"@esbuild/darwin-arm64@0.25.5",
1190
-
"@esbuild/darwin-x64@0.25.5",
1191
-
"@esbuild/freebsd-arm64@0.25.5",
1192
-
"@esbuild/freebsd-x64@0.25.5",
1193
-
"@esbuild/linux-arm@0.25.5",
1194
-
"@esbuild/linux-arm64@0.25.5",
1195
-
"@esbuild/linux-ia32@0.25.5",
1196
-
"@esbuild/linux-loong64@0.25.5",
1197
-
"@esbuild/linux-mips64el@0.25.5",
1198
-
"@esbuild/linux-ppc64@0.25.5",
1199
-
"@esbuild/linux-riscv64@0.25.5",
1200
-
"@esbuild/linux-s390x@0.25.5",
1201
-
"@esbuild/linux-x64@0.25.5",
1202
-
"@esbuild/netbsd-arm64@0.25.5",
1203
-
"@esbuild/netbsd-x64@0.25.5",
1204
-
"@esbuild/openbsd-arm64@0.25.5",
1205
-
"@esbuild/openbsd-x64@0.25.5",
1206
-
"@esbuild/sunos-x64@0.25.5",
1207
-
"@esbuild/win32-arm64@0.25.5",
1208
-
"@esbuild/win32-ia32@0.25.5",
1209
-
"@esbuild/win32-x64@0.25.5"
1025
+
"@esbuild/aix-ppc64",
1026
+
"@esbuild/android-arm",
1027
+
"@esbuild/android-arm64",
1028
+
"@esbuild/android-x64",
1029
+
"@esbuild/darwin-arm64",
1030
+
"@esbuild/darwin-x64",
1031
+
"@esbuild/freebsd-arm64",
1032
+
"@esbuild/freebsd-x64",
1033
+
"@esbuild/linux-arm",
1034
+
"@esbuild/linux-arm64",
1035
+
"@esbuild/linux-ia32",
1036
+
"@esbuild/linux-loong64",
1037
+
"@esbuild/linux-mips64el",
1038
+
"@esbuild/linux-ppc64",
1039
+
"@esbuild/linux-riscv64",
1040
+
"@esbuild/linux-s390x",
1041
+
"@esbuild/linux-x64",
1042
+
"@esbuild/netbsd-arm64",
1043
+
"@esbuild/netbsd-x64",
1044
+
"@esbuild/openbsd-arm64",
1045
+
"@esbuild/openbsd-x64",
1046
+
"@esbuild/sunos-x64",
1047
+
"@esbuild/win32-arm64",
1048
+
"@esbuild/win32-ia32",
1049
+
"@esbuild/win32-x64"
1210
1050
],
1211
1051
"scripts": true,
1212
1052
"bin": true
···
1261
1101
},
1262
1102
"fast-redact@3.5.0": {
1263
1103
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="
1264
-
},
1265
-
"fdir@6.4.4_picomatch@4.0.2": {
1266
-
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
1267
-
"dependencies": [
1268
-
"picomatch@4.0.2"
1269
-
],
1270
-
"optionalPeers": [
1271
-
"picomatch@4.0.2"
1272
-
]
1273
1104
},
1274
1105
"fill-range@7.1.1": {
1275
1106
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
···
1335
1166
"graphemer@1.4.0": {
1336
1167
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
1337
1168
},
1338
-
"has-flag@4.0.0": {
1339
-
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
1340
-
},
1341
1169
"has-symbols@1.1.0": {
1342
1170
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
1343
1171
},
···
1507
1335
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
1508
1336
"dependencies": [
1509
1337
"braces",
1510
-
"picomatch@2.3.1"
1338
+
"picomatch"
1511
1339
]
1512
1340
},
1513
1341
"mime-db@1.52.0": {
···
1522
1350
"mime@1.6.0": {
1523
1351
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1524
1352
"bin": true
1525
-
},
1526
-
"minimatch@9.0.5": {
1527
-
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
1528
-
"dependencies": [
1529
-
"brace-expansion"
1530
-
]
1531
1353
},
1532
1354
"minipass@7.1.2": {
1533
1355
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="
···
1551
1373
"ms@2.1.3": {
1552
1374
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1553
1375
},
1554
-
"multiformats@13.3.4": {
1555
-
"integrity": "sha512-JXpM5p9TpJ/BHsUtmLaWuRN0ft0gJPGa6BhkX2KXjFHvkFQOQkDManoar3gx0JsTLNrOojBE2Mj4hFxohGnXZA=="
1376
+
"multiformats@13.3.6": {
1377
+
"integrity": "sha512-yakbt9cPYj8d3vi/8o/XWm61MrOILo7fsTL0qxNx6zS0Nso6K5JqqS2WV7vK/KSuDBvrW3KfCwAdAgarAgOmww=="
1556
1378
},
1557
1379
"multiformats@9.9.0": {
1558
1380
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
···
1585
1407
"parseurl@1.3.3": {
1586
1408
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
1587
1409
},
1588
-
"path-browserify@1.0.1": {
1589
-
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
1590
-
},
1591
1410
"path-to-regexp@0.1.12": {
1592
1411
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
1593
1412
},
···
1596
1415
},
1597
1416
"picomatch@2.3.1": {
1598
1417
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
1599
-
},
1600
-
"picomatch@4.0.2": {
1601
-
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
1602
1418
},
1603
1419
"pino-abstract-transport@1.2.0": {
1604
1420
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==",
···
1636
1452
"tslib@2.4.0"
1637
1453
]
1638
1454
},
1639
-
"preact-render-to-string@6.5.13_preact@10.26.6": {
1455
+
"preact-render-to-string@6.5.13_preact@10.26.8": {
1640
1456
"integrity": "sha512-iGPd+hKPMFKsfpR2vL4kJ6ZPcFIoWZEcBf0Dpm3zOpdVvj77aY8RlLiQji5OMrngEyaxGogeakTb54uS2FvA6w==",
1641
1457
"dependencies": [
1642
1458
"preact"
1643
1459
]
1644
1460
},
1645
-
"preact@10.26.6": {
1646
-
"integrity": "sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g=="
1647
-
},
1648
-
"prettier@3.5.3": {
1649
-
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
1650
-
"bin": true
1461
+
"preact@10.26.8": {
1462
+
"integrity": "sha512-1nMfdFjucm5hKvq0IClqZwK4FJkGXhRrQstOQ3P4vp8HxKrJEMFcY6RdBRVTdfQS/UlnX6gfbPuTvaqx/bDoeQ=="
1651
1463
},
1652
1464
"process-warning@3.0.0": {
1653
1465
"integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="
···
1661
1473
"forwarded",
1662
1474
"ipaddr.js@1.9.1"
1663
1475
]
1664
-
},
1665
-
"psl@1.15.0": {
1666
-
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
1667
-
"dependencies": [
1668
-
"punycode"
1669
-
]
1670
-
},
1671
-
"punycode@2.3.1": {
1672
-
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
1673
1476
},
1674
1477
"qs@6.13.0": {
1675
1478
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
···
1824
1627
"tslib@2.4.0"
1825
1628
]
1826
1629
},
1827
-
"supports-color@7.2.0": {
1828
-
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
1829
-
"dependencies": [
1830
-
"has-flag"
1831
-
]
1832
-
},
1833
1630
"tailwind-merge@3.3.0": {
1834
1631
"integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="
1835
1632
},
1836
-
"tailwindcss@4.1.7": {
1837
-
"integrity": "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg=="
1633
+
"tailwindcss@4.1.8": {
1634
+
"integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="
1838
1635
},
1839
1636
"tapable@2.2.2": {
1840
1637
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="
···
1855
1652
"dependencies": [
1856
1653
"@jridgewell/source-map",
1857
1654
"acorn",
1858
-
"commander@2.20.3",
1655
+
"commander",
1859
1656
"source-map-support"
1860
1657
],
1861
1658
"bin": true
···
1866
1663
"real-require"
1867
1664
]
1868
1665
},
1869
-
"tinyglobby@0.2.13_picomatch@4.0.2": {
1870
-
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
1871
-
"dependencies": [
1872
-
"fdir",
1873
-
"picomatch@4.0.2"
1874
-
]
1875
-
},
1876
1666
"tlds@1.259.0": {
1877
1667
"integrity": "sha512-AldGGlDP0PNgwppe2quAvuBl18UcjuNtOnDuUkqhd6ipPqrYYBt3aTxK1QTsBVknk97lS2JcafWMghjGWFtunw==",
1878
1668
"bin": true
···
1886
1676
"toidentifier@1.0.1": {
1887
1677
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
1888
1678
},
1889
-
"ts-morph@24.0.0": {
1890
-
"integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==",
1891
-
"dependencies": [
1892
-
"@ts-morph/common",
1893
-
"code-block-writer"
1894
-
]
1895
-
},
1896
1679
"tslib@2.4.0": {
1897
1680
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
1898
1681
},
···
1942
1725
"yallist@5.0.0": {
1943
1726
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="
1944
1727
},
1945
-
"yesno@0.4.0": {
1946
-
"integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA=="
1947
-
},
1948
-
"zod@3.25.7": {
1949
-
"integrity": "sha512-YGdT1cVRmKkOg6Sq7vY7IkxdphySKnXhaUmFI4r4FcuFVNgpCb9tZfNwXbT6BPjD5oz0nubFsoo9pIqKrDcCvg=="
1728
+
"zod@3.25.51": {
1729
+
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg=="
1950
1730
}
1951
1731
},
1952
1732
"workspace": {
1953
1733
"dependencies": [
1954
-
"jsr:@bigmoves/bff@0.3.0-beta.32",
1734
+
"jsr:@bigmoves/bff@0.3.0-beta.33",
1955
1735
"jsr:@luca/esbuild-deno-loader@~0.11.1",
1956
1736
"jsr:@std/http@^1.0.17",
1957
1737
"jsr:@std/path@^1.0.9",
+3
-1
lexicons.json
+3
-1
lexicons.json
+55
lexicons/com/atproto/moderation/defs.json
+55
lexicons/com/atproto/moderation/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.moderation.defs",
4
+
"defs": {
5
+
"reasonRude": {
6
+
"type": "token",
7
+
"description": "Rude, harassing, explicit, or otherwise unwelcoming behavior"
8
+
},
9
+
"reasonSpam": {
10
+
"type": "token",
11
+
"description": "Spam: frequent unwanted promotion, replies, mentions"
12
+
},
13
+
"reasonType": {
14
+
"type": "string",
15
+
"knownValues": [
16
+
"com.atproto.moderation.defs#reasonSpam",
17
+
"com.atproto.moderation.defs#reasonViolation",
18
+
"com.atproto.moderation.defs#reasonMisleading",
19
+
"com.atproto.moderation.defs#reasonSexual",
20
+
"com.atproto.moderation.defs#reasonRude",
21
+
"com.atproto.moderation.defs#reasonOther",
22
+
"com.atproto.moderation.defs#reasonAppeal"
23
+
]
24
+
},
25
+
"reasonOther": {
26
+
"type": "token",
27
+
"description": "Other: reports not falling under another report category"
28
+
},
29
+
"subjectType": {
30
+
"type": "string",
31
+
"description": "Tag describing a type of subject that might be reported.",
32
+
"knownValues": [
33
+
"account",
34
+
"record",
35
+
"chat"
36
+
]
37
+
},
38
+
"reasonAppeal": {
39
+
"type": "token",
40
+
"description": "Appeal: appeal a previously taken moderation action"
41
+
},
42
+
"reasonSexual": {
43
+
"type": "token",
44
+
"description": "Unwanted or mislabeled sexual content"
45
+
},
46
+
"reasonViolation": {
47
+
"type": "token",
48
+
"description": "Direct violation of server rules, laws, terms of service"
49
+
},
50
+
"reasonMisleading": {
51
+
"type": "token",
52
+
"description": "Misleading identity, affiliation, or content"
53
+
}
54
+
}
55
+
}
+3
services/mod/.env.example
+3
services/mod/.env.example
+11
services/mod/Dockerfile
+11
services/mod/Dockerfile
+103
services/mod/__generated__/index.ts
+103
services/mod/__generated__/index.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import {
5
+
createServer as createXrpcServer,
6
+
Server as XrpcServer,
7
+
type Options as XrpcOptions,
8
+
type AuthVerifier,
9
+
type StreamAuthVerifier,
10
+
} from "npm:@atproto/xrpc-server"
11
+
import { schemas } from './lexicons.ts'
12
+
import * as ComAtprotoLabelSubscribeLabels from './types/com/atproto/label/subscribeLabels.ts'
13
+
import * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels.ts'
14
+
15
+
export function createServer(options?: XrpcOptions): Server {
16
+
return new Server(options)
17
+
}
18
+
19
+
export class Server {
20
+
xrpc: XrpcServer
21
+
com: ComNS
22
+
23
+
constructor(options?: XrpcOptions) {
24
+
this.xrpc = createXrpcServer(schemas, options)
25
+
this.com = new ComNS(this)
26
+
}
27
+
}
28
+
29
+
export class ComNS {
30
+
_server: Server
31
+
atproto: ComAtprotoNS
32
+
33
+
constructor(server: Server) {
34
+
this._server = server
35
+
this.atproto = new ComAtprotoNS(server)
36
+
}
37
+
}
38
+
39
+
export class ComAtprotoNS {
40
+
_server: Server
41
+
label: ComAtprotoLabelNS
42
+
43
+
constructor(server: Server) {
44
+
this._server = server
45
+
this.label = new ComAtprotoLabelNS(server)
46
+
}
47
+
}
48
+
49
+
export class ComAtprotoLabelNS {
50
+
_server: Server
51
+
52
+
constructor(server: Server) {
53
+
this._server = server
54
+
}
55
+
56
+
subscribeLabels<AV extends StreamAuthVerifier>(
57
+
cfg: ConfigOf<
58
+
AV,
59
+
ComAtprotoLabelSubscribeLabels.Handler<ExtractAuth<AV>>,
60
+
ComAtprotoLabelSubscribeLabels.HandlerReqCtx<ExtractAuth<AV>>
61
+
>,
62
+
) {
63
+
const nsid = 'com.atproto.label.subscribeLabels' // @ts-ignore
64
+
return this._server.xrpc.streamMethod(nsid, cfg)
65
+
}
66
+
67
+
queryLabels<AV extends AuthVerifier>(
68
+
cfg: ConfigOf<
69
+
AV,
70
+
ComAtprotoLabelQueryLabels.Handler<ExtractAuth<AV>>,
71
+
ComAtprotoLabelQueryLabels.HandlerReqCtx<ExtractAuth<AV>>
72
+
>,
73
+
) {
74
+
const nsid = 'com.atproto.label.queryLabels' // @ts-ignore
75
+
return this._server.xrpc.method(nsid, cfg)
76
+
}
77
+
}
78
+
79
+
type SharedRateLimitOpts<T> = {
80
+
name: string
81
+
calcKey?: (ctx: T) => string | null
82
+
calcPoints?: (ctx: T) => number
83
+
}
84
+
type RouteRateLimitOpts<T> = {
85
+
durationMs: number
86
+
points: number
87
+
calcKey?: (ctx: T) => string | null
88
+
calcPoints?: (ctx: T) => number
89
+
}
90
+
type HandlerOpts = { blobLimit?: number }
91
+
type HandlerRateLimitOpts<T> = SharedRateLimitOpts<T> | RouteRateLimitOpts<T>
92
+
type ConfigOf<Auth, Handler, ReqCtx> =
93
+
| Handler
94
+
| {
95
+
auth?: Auth
96
+
opts?: HandlerOpts
97
+
rateLimit?: HandlerRateLimitOpts<ReqCtx> | HandlerRateLimitOpts<ReqCtx>[]
98
+
handler: Handler
99
+
}
100
+
type ExtractAuth<AV extends AuthVerifier | StreamAuthVerifier> = Extract<
101
+
Awaited<ReturnType<AV>>,
102
+
{ credentials: unknown }
103
+
>
+353
services/mod/__generated__/lexicons.ts
+353
services/mod/__generated__/lexicons.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import {
5
+
type LexiconDoc,
6
+
Lexicons,
7
+
ValidationError,
8
+
type ValidationResult,
9
+
} from "npm:@atproto/lexicon"
10
+
import { type $Typed, is$typed, maybe$typed } from './util.ts'
11
+
12
+
export const schemaDict = {
13
+
ComAtprotoLabelSubscribeLabels: {
14
+
lexicon: 1,
15
+
id: 'com.atproto.label.subscribeLabels',
16
+
defs: {
17
+
info: {
18
+
type: 'object',
19
+
required: ['name'],
20
+
properties: {
21
+
name: {
22
+
type: 'string',
23
+
knownValues: ['OutdatedCursor'],
24
+
},
25
+
message: {
26
+
type: 'string',
27
+
},
28
+
},
29
+
},
30
+
main: {
31
+
type: 'subscription',
32
+
errors: [
33
+
{
34
+
name: 'FutureCursor',
35
+
},
36
+
],
37
+
message: {
38
+
schema: {
39
+
refs: [
40
+
'lex:com.atproto.label.subscribeLabels#labels',
41
+
'lex:com.atproto.label.subscribeLabels#info',
42
+
],
43
+
type: 'union',
44
+
},
45
+
},
46
+
parameters: {
47
+
type: 'params',
48
+
properties: {
49
+
cursor: {
50
+
type: 'integer',
51
+
description: 'The last known event seq number to backfill from.',
52
+
},
53
+
},
54
+
},
55
+
description:
56
+
'Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.',
57
+
},
58
+
labels: {
59
+
type: 'object',
60
+
required: ['seq', 'labels'],
61
+
properties: {
62
+
seq: {
63
+
type: 'integer',
64
+
},
65
+
labels: {
66
+
type: 'array',
67
+
items: {
68
+
ref: 'lex:com.atproto.label.defs#label',
69
+
type: 'ref',
70
+
},
71
+
},
72
+
},
73
+
},
74
+
},
75
+
},
76
+
ComAtprotoLabelDefs: {
77
+
lexicon: 1,
78
+
id: 'com.atproto.label.defs',
79
+
defs: {
80
+
label: {
81
+
type: 'object',
82
+
required: ['src', 'uri', 'val', 'cts'],
83
+
properties: {
84
+
cid: {
85
+
type: 'string',
86
+
format: 'cid',
87
+
description:
88
+
"Optionally, CID specifying the specific version of 'uri' resource this label applies to.",
89
+
},
90
+
cts: {
91
+
type: 'string',
92
+
format: 'datetime',
93
+
description: 'Timestamp when this label was created.',
94
+
},
95
+
exp: {
96
+
type: 'string',
97
+
format: 'datetime',
98
+
description:
99
+
'Timestamp at which this label expires (no longer applies).',
100
+
},
101
+
neg: {
102
+
type: 'boolean',
103
+
description:
104
+
'If true, this is a negation label, overwriting a previous label.',
105
+
},
106
+
sig: {
107
+
type: 'bytes',
108
+
description: 'Signature of dag-cbor encoded label.',
109
+
},
110
+
src: {
111
+
type: 'string',
112
+
format: 'did',
113
+
description: 'DID of the actor who created this label.',
114
+
},
115
+
uri: {
116
+
type: 'string',
117
+
format: 'uri',
118
+
description:
119
+
'AT URI of the record, repository (account), or other resource that this label applies to.',
120
+
},
121
+
val: {
122
+
type: 'string',
123
+
maxLength: 128,
124
+
description:
125
+
'The short string name of the value or type of this label.',
126
+
},
127
+
ver: {
128
+
type: 'integer',
129
+
description: 'The AT Protocol version of the label object.',
130
+
},
131
+
},
132
+
description:
133
+
'Metadata tag on an atproto resource (eg, repo or record).',
134
+
},
135
+
selfLabel: {
136
+
type: 'object',
137
+
required: ['val'],
138
+
properties: {
139
+
val: {
140
+
type: 'string',
141
+
maxLength: 128,
142
+
description:
143
+
'The short string name of the value or type of this label.',
144
+
},
145
+
},
146
+
description:
147
+
'Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.',
148
+
},
149
+
labelValue: {
150
+
type: 'string',
151
+
knownValues: [
152
+
'!hide',
153
+
'!no-promote',
154
+
'!warn',
155
+
'!no-unauthenticated',
156
+
'dmca-violation',
157
+
'doxxing',
158
+
'porn',
159
+
'sexual',
160
+
'nudity',
161
+
'nsfl',
162
+
'gore',
163
+
],
164
+
},
165
+
selfLabels: {
166
+
type: 'object',
167
+
required: ['values'],
168
+
properties: {
169
+
values: {
170
+
type: 'array',
171
+
items: {
172
+
ref: 'lex:com.atproto.label.defs#selfLabel',
173
+
type: 'ref',
174
+
},
175
+
maxLength: 10,
176
+
},
177
+
},
178
+
description:
179
+
'Metadata tags on an atproto record, published by the author within the record.',
180
+
},
181
+
labelValueDefinition: {
182
+
type: 'object',
183
+
required: ['identifier', 'severity', 'blurs', 'locales'],
184
+
properties: {
185
+
blurs: {
186
+
type: 'string',
187
+
description:
188
+
"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
189
+
knownValues: ['content', 'media', 'none'],
190
+
},
191
+
locales: {
192
+
type: 'array',
193
+
items: {
194
+
ref: 'lex:com.atproto.label.defs#labelValueDefinitionStrings',
195
+
type: 'ref',
196
+
},
197
+
},
198
+
severity: {
199
+
type: 'string',
200
+
description:
201
+
"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
202
+
knownValues: ['inform', 'alert', 'none'],
203
+
},
204
+
adultOnly: {
205
+
type: 'boolean',
206
+
description:
207
+
'Does the user need to have adult content enabled in order to configure this label?',
208
+
},
209
+
identifier: {
210
+
type: 'string',
211
+
maxLength: 100,
212
+
description:
213
+
"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
214
+
maxGraphemes: 100,
215
+
},
216
+
defaultSetting: {
217
+
type: 'string',
218
+
default: 'warn',
219
+
description: 'The default setting for this label.',
220
+
knownValues: ['ignore', 'warn', 'hide'],
221
+
},
222
+
},
223
+
description:
224
+
'Declares a label value and its expected interpretations and behaviors.',
225
+
},
226
+
labelValueDefinitionStrings: {
227
+
type: 'object',
228
+
required: ['lang', 'name', 'description'],
229
+
properties: {
230
+
lang: {
231
+
type: 'string',
232
+
format: 'language',
233
+
description:
234
+
'The code of the language these strings are written in.',
235
+
},
236
+
name: {
237
+
type: 'string',
238
+
maxLength: 640,
239
+
description: 'A short human-readable name for the label.',
240
+
maxGraphemes: 64,
241
+
},
242
+
description: {
243
+
type: 'string',
244
+
maxLength: 100000,
245
+
description:
246
+
'A longer description of what the label means and why it might be applied.',
247
+
maxGraphemes: 10000,
248
+
},
249
+
},
250
+
description:
251
+
'Strings which describe the label in the UI, localized into a specific language.',
252
+
},
253
+
},
254
+
},
255
+
ComAtprotoLabelQueryLabels: {
256
+
lexicon: 1,
257
+
id: 'com.atproto.label.queryLabels',
258
+
defs: {
259
+
main: {
260
+
type: 'query',
261
+
output: {
262
+
schema: {
263
+
type: 'object',
264
+
required: ['labels'],
265
+
properties: {
266
+
cursor: {
267
+
type: 'string',
268
+
},
269
+
labels: {
270
+
type: 'array',
271
+
items: {
272
+
ref: 'lex:com.atproto.label.defs#label',
273
+
type: 'ref',
274
+
},
275
+
},
276
+
},
277
+
},
278
+
encoding: 'application/json',
279
+
},
280
+
parameters: {
281
+
type: 'params',
282
+
required: ['uriPatterns'],
283
+
properties: {
284
+
limit: {
285
+
type: 'integer',
286
+
default: 50,
287
+
maximum: 250,
288
+
minimum: 1,
289
+
},
290
+
cursor: {
291
+
type: 'string',
292
+
},
293
+
sources: {
294
+
type: 'array',
295
+
items: {
296
+
type: 'string',
297
+
format: 'did',
298
+
},
299
+
description:
300
+
'Optional list of label sources (DIDs) to filter on.',
301
+
},
302
+
uriPatterns: {
303
+
type: 'array',
304
+
items: {
305
+
type: 'string',
306
+
},
307
+
description:
308
+
"List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.",
309
+
},
310
+
},
311
+
},
312
+
description:
313
+
'Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.',
314
+
},
315
+
},
316
+
},
317
+
} as const satisfies Record<string, LexiconDoc>
318
+
export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]
319
+
export const lexicons: Lexicons = new Lexicons(schemas)
320
+
321
+
export function validate<T extends { $type: string }>(
322
+
v: unknown,
323
+
id: string,
324
+
hash: string,
325
+
requiredType: true,
326
+
): ValidationResult<T>
327
+
export function validate<T extends { $type?: string }>(
328
+
v: unknown,
329
+
id: string,
330
+
hash: string,
331
+
requiredType?: false,
332
+
): ValidationResult<T>
333
+
export function validate(
334
+
v: unknown,
335
+
id: string,
336
+
hash: string,
337
+
requiredType?: boolean,
338
+
): ValidationResult {
339
+
return (requiredType ? is$typed : maybe$typed)(v, id, hash)
340
+
? lexicons.validate(`${id}#${hash}`, v)
341
+
: {
342
+
success: false,
343
+
error: new ValidationError(
344
+
`Must be an object with "${hash === 'main' ? id : `${id}#${hash}`}" $type property`,
345
+
),
346
+
}
347
+
}
348
+
349
+
export const ids = {
350
+
ComAtprotoLabelSubscribeLabels: 'com.atproto.label.subscribeLabels',
351
+
ComAtprotoLabelDefs: 'com.atproto.label.defs',
352
+
ComAtprotoLabelQueryLabels: 'com.atproto.label.queryLabels',
353
+
} as const
+146
services/mod/__generated__/types/com/atproto/label/defs.ts
+146
services/mod/__generated__/types/com/atproto/label/defs.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { type ValidationResult, BlobRef } from "npm:@atproto/lexicon"
5
+
import { CID } from "npm:multiformats/cid"
6
+
import { validate as _validate } from '../../../../lexicons.ts'
7
+
import {
8
+
type $Typed,
9
+
is$typed as _is$typed,
10
+
type OmitKey,
11
+
} from '../../../../util.ts'
12
+
13
+
const is$typed = _is$typed,
14
+
validate = _validate
15
+
const id = 'com.atproto.label.defs'
16
+
17
+
/** Metadata tag on an atproto resource (eg, repo or record). */
18
+
export interface Label {
19
+
$type?: 'com.atproto.label.defs#label'
20
+
/** Optionally, CID specifying the specific version of 'uri' resource this label applies to. */
21
+
cid?: string
22
+
/** Timestamp when this label was created. */
23
+
cts: string
24
+
/** Timestamp at which this label expires (no longer applies). */
25
+
exp?: string
26
+
/** If true, this is a negation label, overwriting a previous label. */
27
+
neg?: boolean
28
+
/** Signature of dag-cbor encoded label. */
29
+
sig?: Uint8Array
30
+
/** DID of the actor who created this label. */
31
+
src: string
32
+
/** AT URI of the record, repository (account), or other resource that this label applies to. */
33
+
uri: string
34
+
/** The short string name of the value or type of this label. */
35
+
val: string
36
+
/** The AT Protocol version of the label object. */
37
+
ver?: number
38
+
}
39
+
40
+
const hashLabel = 'label'
41
+
42
+
export function isLabel<V>(v: V) {
43
+
return is$typed(v, id, hashLabel)
44
+
}
45
+
46
+
export function validateLabel<V>(v: V) {
47
+
return validate<Label & V>(v, id, hashLabel)
48
+
}
49
+
50
+
/** Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel. */
51
+
export interface SelfLabel {
52
+
$type?: 'com.atproto.label.defs#selfLabel'
53
+
/** The short string name of the value or type of this label. */
54
+
val: string
55
+
}
56
+
57
+
const hashSelfLabel = 'selfLabel'
58
+
59
+
export function isSelfLabel<V>(v: V) {
60
+
return is$typed(v, id, hashSelfLabel)
61
+
}
62
+
63
+
export function validateSelfLabel<V>(v: V) {
64
+
return validate<SelfLabel & V>(v, id, hashSelfLabel)
65
+
}
66
+
67
+
export type LabelValue =
68
+
| '!hide'
69
+
| '!no-promote'
70
+
| '!warn'
71
+
| '!no-unauthenticated'
72
+
| 'dmca-violation'
73
+
| 'doxxing'
74
+
| 'porn'
75
+
| 'sexual'
76
+
| 'nudity'
77
+
| 'nsfl'
78
+
| 'gore'
79
+
| (string & {})
80
+
81
+
/** Metadata tags on an atproto record, published by the author within the record. */
82
+
export interface SelfLabels {
83
+
$type?: 'com.atproto.label.defs#selfLabels'
84
+
values: SelfLabel[]
85
+
}
86
+
87
+
const hashSelfLabels = 'selfLabels'
88
+
89
+
export function isSelfLabels<V>(v: V) {
90
+
return is$typed(v, id, hashSelfLabels)
91
+
}
92
+
93
+
export function validateSelfLabels<V>(v: V) {
94
+
return validate<SelfLabels & V>(v, id, hashSelfLabels)
95
+
}
96
+
97
+
/** Declares a label value and its expected interpretations and behaviors. */
98
+
export interface LabelValueDefinition {
99
+
$type?: 'com.atproto.label.defs#labelValueDefinition'
100
+
/** What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing. */
101
+
blurs: 'content' | 'media' | 'none' | (string & {})
102
+
locales: LabelValueDefinitionStrings[]
103
+
/** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */
104
+
severity: 'inform' | 'alert' | 'none' | (string & {})
105
+
/** Does the user need to have adult content enabled in order to configure this label? */
106
+
adultOnly?: boolean
107
+
/** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */
108
+
identifier: string
109
+
/** The default setting for this label. */
110
+
defaultSetting: 'ignore' | 'warn' | 'hide' | (string & {})
111
+
}
112
+
113
+
const hashLabelValueDefinition = 'labelValueDefinition'
114
+
115
+
export function isLabelValueDefinition<V>(v: V) {
116
+
return is$typed(v, id, hashLabelValueDefinition)
117
+
}
118
+
119
+
export function validateLabelValueDefinition<V>(v: V) {
120
+
return validate<LabelValueDefinition & V>(v, id, hashLabelValueDefinition)
121
+
}
122
+
123
+
/** Strings which describe the label in the UI, localized into a specific language. */
124
+
export interface LabelValueDefinitionStrings {
125
+
$type?: 'com.atproto.label.defs#labelValueDefinitionStrings'
126
+
/** The code of the language these strings are written in. */
127
+
lang: string
128
+
/** A short human-readable name for the label. */
129
+
name: string
130
+
/** A longer description of what the label means and why it might be applied. */
131
+
description: string
132
+
}
133
+
134
+
const hashLabelValueDefinitionStrings = 'labelValueDefinitionStrings'
135
+
136
+
export function isLabelValueDefinitionStrings<V>(v: V) {
137
+
return is$typed(v, id, hashLabelValueDefinitionStrings)
138
+
}
139
+
140
+
export function validateLabelValueDefinitionStrings<V>(v: V) {
141
+
return validate<LabelValueDefinitionStrings & V>(
142
+
v,
143
+
id,
144
+
hashLabelValueDefinitionStrings,
145
+
)
146
+
}
+54
services/mod/__generated__/types/com/atproto/label/queryLabels.ts
+54
services/mod/__generated__/types/com/atproto/label/queryLabels.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { HandlerAuth, HandlerPipeThrough } from "npm:@atproto/xrpc-server";
5
+
import express from "npm:express";
6
+
import { validate as _validate } from "../../../../lexicons.ts";
7
+
import { is$typed as _is$typed } from "../../../../util.ts";
8
+
import type * as ComAtprotoLabelDefs from "./defs.ts";
9
+
10
+
const is$typed = _is$typed,
11
+
validate = _validate;
12
+
const id = "com.atproto.label.queryLabels";
13
+
14
+
export interface QueryParams {
15
+
limit: number;
16
+
cursor?: string;
17
+
/** Optional list of label sources (DIDs) to filter on. */
18
+
sources?: string[];
19
+
/** List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI. */
20
+
uriPatterns: string[];
21
+
}
22
+
23
+
export type InputSchema = undefined;
24
+
25
+
export interface OutputSchema {
26
+
cursor?: string;
27
+
labels: ComAtprotoLabelDefs.Label[];
28
+
}
29
+
30
+
export type HandlerInput = undefined;
31
+
32
+
export interface HandlerSuccess {
33
+
encoding: "application/json";
34
+
body: OutputSchema;
35
+
headers?: { [key: string]: string };
36
+
}
37
+
38
+
export interface HandlerError {
39
+
status: number;
40
+
message?: string;
41
+
}
42
+
43
+
export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough;
44
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
45
+
auth: HA;
46
+
params: QueryParams;
47
+
input: HandlerInput;
48
+
req: express.Request;
49
+
res: express.Response;
50
+
resetRouteRateLimits: () => Promise<void>;
51
+
};
52
+
export type Handler<HA extends HandlerAuth = never> = (
53
+
ctx: HandlerReqCtx<HA>,
54
+
) => Promise<HandlerOutput> | HandlerOutput;
+68
services/mod/__generated__/types/com/atproto/label/subscribeLabels.ts
+68
services/mod/__generated__/types/com/atproto/label/subscribeLabels.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
import { type ValidationResult, BlobRef } from "npm:@atproto/lexicon"
5
+
import { CID } from "npm:multiformats/cid"
6
+
import { validate as _validate } from '../../../../lexicons.ts'
7
+
import {
8
+
type $Typed,
9
+
is$typed as _is$typed,
10
+
type OmitKey,
11
+
} from '../../../../util.ts'
12
+
import { HandlerAuth, ErrorFrame } from "npm:@atproto/xrpc-server"
13
+
import { IncomingMessage } from 'node:http'
14
+
import type * as ComAtprotoLabelDefs from './defs.ts'
15
+
16
+
const is$typed = _is$typed,
17
+
validate = _validate
18
+
const id = 'com.atproto.label.subscribeLabels'
19
+
20
+
export interface Info {
21
+
$type?: 'com.atproto.label.subscribeLabels#info'
22
+
name: 'OutdatedCursor' | (string & {})
23
+
message?: string
24
+
}
25
+
26
+
const hashInfo = 'info'
27
+
28
+
export function isInfo<V>(v: V) {
29
+
return is$typed(v, id, hashInfo)
30
+
}
31
+
32
+
export function validateInfo<V>(v: V) {
33
+
return validate<Info & V>(v, id, hashInfo)
34
+
}
35
+
36
+
export interface QueryParams {
37
+
/** The last known event seq number to backfill from. */
38
+
cursor?: number
39
+
}
40
+
41
+
export type OutputSchema = $Typed<Labels> | $Typed<Info> | { $type: string }
42
+
export type HandlerError = ErrorFrame<'FutureCursor'>
43
+
export type HandlerOutput = HandlerError | OutputSchema
44
+
export type HandlerReqCtx<HA extends HandlerAuth = never> = {
45
+
auth: HA
46
+
params: QueryParams
47
+
req: IncomingMessage
48
+
signal: AbortSignal
49
+
}
50
+
export type Handler<HA extends HandlerAuth = never> = (
51
+
ctx: HandlerReqCtx<HA>,
52
+
) => AsyncIterable<HandlerOutput>
53
+
54
+
export interface Labels {
55
+
$type?: 'com.atproto.label.subscribeLabels#labels'
56
+
seq: number
57
+
labels: ComAtprotoLabelDefs.Label[]
58
+
}
59
+
60
+
const hashLabels = 'labels'
61
+
62
+
export function isLabels<V>(v: V) {
63
+
return is$typed(v, id, hashLabels)
64
+
}
65
+
66
+
export function validateLabels<V>(v: V) {
67
+
return validate<Labels & V>(v, id, hashLabels)
68
+
}
+82
services/mod/__generated__/util.ts
+82
services/mod/__generated__/util.ts
···
1
+
/**
2
+
* GENERATED CODE - DO NOT MODIFY
3
+
*/
4
+
5
+
import { type ValidationResult } from "npm:@atproto/lexicon"
6
+
7
+
export type OmitKey<T, K extends keyof T> = {
8
+
[K2 in keyof T as K2 extends K ? never : K2]: T[K2]
9
+
}
10
+
11
+
export type $Typed<V, T extends string = string> = V & { $type: T }
12
+
export type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>
13
+
14
+
export type $Type<Id extends string, Hash extends string> = Hash extends 'main'
15
+
? Id
16
+
: `${Id}#${Hash}`
17
+
18
+
function isObject<V>(v: V): v is V & object {
19
+
return v != null && typeof v === 'object'
20
+
}
21
+
22
+
function is$type<Id extends string, Hash extends string>(
23
+
$type: unknown,
24
+
id: Id,
25
+
hash: Hash,
26
+
): $type is $Type<Id, Hash> {
27
+
return hash === 'main'
28
+
? $type === id
29
+
: // $type === `${id}#${hash}`
30
+
typeof $type === 'string' &&
31
+
$type.length === id.length + 1 + hash.length &&
32
+
$type.charCodeAt(id.length) === 35 /* '#' */ &&
33
+
$type.startsWith(id) &&
34
+
$type.endsWith(hash)
35
+
}
36
+
37
+
export type $TypedObject<
38
+
V,
39
+
Id extends string,
40
+
Hash extends string,
41
+
> = V extends {
42
+
$type: $Type<Id, Hash>
43
+
}
44
+
? V
45
+
: V extends { $type?: string }
46
+
? V extends { $type?: infer T extends $Type<Id, Hash> }
47
+
? V & { $type: T }
48
+
: never
49
+
: V & { $type: $Type<Id, Hash> }
50
+
51
+
export function is$typed<V, Id extends string, Hash extends string>(
52
+
v: V,
53
+
id: Id,
54
+
hash: Hash,
55
+
): v is $TypedObject<V, Id, Hash> {
56
+
return isObject(v) && '$type' in v && is$type(v.$type, id, hash)
57
+
}
58
+
59
+
export function maybe$typed<V, Id extends string, Hash extends string>(
60
+
v: V,
61
+
id: Id,
62
+
hash: Hash,
63
+
): v is V & object & { $type?: $Type<Id, Hash> } {
64
+
return (
65
+
isObject(v) &&
66
+
('$type' in v ? v.$type === undefined || is$type(v.$type, id, hash) : true)
67
+
)
68
+
}
69
+
70
+
export type Validator<R = unknown> = (v: unknown) => ValidationResult<R>
71
+
export type ValidatorParam<V extends Validator> =
72
+
V extends Validator<infer R> ? R : never
73
+
74
+
/**
75
+
* Utility function that allows to convert a "validate*" utility function into a
76
+
* type predicate.
77
+
*/
78
+
export function asPredicate<V extends Validator>(validate: V) {
79
+
return function <T>(v: T): v is T & ValidatorParam<V> {
80
+
return validate(v).success
81
+
}
82
+
}
+99
services/mod/bin.ts
+99
services/mod/bin.ts
···
1
+
import { stringifyLex } from "npm:@atproto/lexicon";
2
+
import type { LabelRow } from "./main.ts";
3
+
import {
4
+
createConfig,
5
+
createDb,
6
+
createLabel,
7
+
createModService,
8
+
ModService,
9
+
} from "./main.ts";
10
+
11
+
async function handleCreate(
12
+
args: string[],
13
+
cfg: Awaited<ReturnType<typeof createConfig>>,
14
+
modService: ModService,
15
+
) {
16
+
const [src, uri, val, negArg] = args;
17
+
if (!src || !uri || !val) {
18
+
console.error("Usage: deno run -A bin.ts create <src> <uri> <val> [neg]");
19
+
Deno.exit(1);
20
+
}
21
+
const neg = negArg === "true" || negArg === "1";
22
+
const doCreateLabel = createLabel(cfg, modService);
23
+
await doCreateLabel({
24
+
src,
25
+
uri,
26
+
val,
27
+
neg,
28
+
cts: new Date().toISOString(),
29
+
});
30
+
console.log(
31
+
`Label created for src: ${src}, uri: ${uri}, val: ${val}, neg: ${neg}`,
32
+
);
33
+
}
34
+
35
+
function parseQueryArgs(args: string[]) {
36
+
let src = "";
37
+
let val = "";
38
+
let limit = 50;
39
+
let cursor = 0;
40
+
const patterns: string[] = [];
41
+
let i = 0;
42
+
while (i < args.length) {
43
+
if (args[i] === "--src" && args[i + 1]) {
44
+
src = args[i + 1];
45
+
i += 2;
46
+
continue;
47
+
}
48
+
if (args[i] === "--val" && args[i + 1]) {
49
+
val = args[i + 1];
50
+
i += 2;
51
+
continue;
52
+
}
53
+
if (args[i] === "--limit" && args[i + 1]) {
54
+
limit = parseInt(args[i + 1], 10);
55
+
i += 2;
56
+
continue;
57
+
}
58
+
if (args[i] === "--cursor" && args[i + 1]) {
59
+
cursor = parseInt(args[i + 1], 10);
60
+
i += 2;
61
+
continue;
62
+
}
63
+
i++;
64
+
}
65
+
return { src, val, limit, cursor, patterns };
66
+
}
67
+
68
+
function handleQuery(args: string[], modService: ModService) {
69
+
// Usage: deno run -A bin.ts query [--src <src>] [--val <val>] [--limit <n>] [--cursor <id>]
70
+
const { src, val, limit, cursor, patterns } = parseQueryArgs(args);
71
+
const sources = src ? [src] : [];
72
+
// If val is set, filter in JS after query
73
+
const { rows } = modService.getLabels({ patterns, sources, limit, cursor });
74
+
const filtered = val ? rows.filter((r: LabelRow) => r.val === val) : rows;
75
+
for (const row of filtered) {
76
+
console.log(stringifyLex(row));
77
+
}
78
+
}
79
+
80
+
async function main() {
81
+
const [cmd, ...args] = Deno.args;
82
+
if (!cmd || ["create", "query"].indexOf(cmd) === -1) {
83
+
console.error("Usage: deno run -A bin.ts <create|query> [...args]");
84
+
Deno.exit(1);
85
+
}
86
+
const cfg = await createConfig();
87
+
const db = createDb(cfg);
88
+
const modService = createModService(db);
89
+
90
+
if (cmd === "create") {
91
+
await handleCreate(args, cfg, modService);
92
+
} else if (cmd === "query") {
93
+
handleQuery(args, modService);
94
+
}
95
+
}
96
+
97
+
if (import.meta.main) {
98
+
main();
99
+
}
+17
services/mod/deno.json
+17
services/mod/deno.json
···
1
+
{
2
+
"imports": {
3
+
"$lexicon/": "./__generated__/",
4
+
"@atproto/common": "npm:@atproto/common@^0.4.11",
5
+
"@atproto/crypto": "npm:@atproto/crypto@^0.4.4",
6
+
"@atproto/lexicon": "npm:@atproto/lexicon@^0.4.11",
7
+
"@std/assert": "jsr:@std/assert@^1.0.13",
8
+
"uint8arrays": "npm:uint8arrays@^5.1.0"
9
+
},
10
+
"tasks": {
11
+
"start": "deno run -A ./main.ts",
12
+
"dev": "deno run -A --watch --env=.env ./main.ts",
13
+
"test": "deno test -A main.ts",
14
+
"codegen": "deno run -A jsr:@bigmoves/bff-cli@0.3.0-beta.30 lexgen"
15
+
},
16
+
"nodeModulesDir": "auto"
17
+
}
+831
services/mod/deno.lock
+831
services/mod/deno.lock
···
1
+
{
2
+
"version": "5",
3
+
"specifiers": {
4
+
"jsr:@std/assert@^1.0.13": "1.0.13",
5
+
"jsr:@std/internal@^1.0.6": "1.0.8",
6
+
"npm:@atproto/common@~0.4.11": "0.4.11",
7
+
"npm:@atproto/crypto@~0.4.4": "0.4.4",
8
+
"npm:@atproto/lex-cli@*": "0.8.2",
9
+
"npm:@atproto/lexicon@*": "0.4.11",
10
+
"npm:@atproto/lexicon@~0.4.11": "0.4.11",
11
+
"npm:@atproto/xrpc-server@*": "0.7.19",
12
+
"npm:@types/node@*": "22.15.15",
13
+
"npm:multiformats@*": "9.9.0",
14
+
"npm:uint8arrays@^5.1.0": "5.1.0"
15
+
},
16
+
"jsr": {
17
+
"@std/assert@1.0.13": {
18
+
"integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29",
19
+
"dependencies": [
20
+
"jsr:@std/internal"
21
+
]
22
+
},
23
+
"@std/internal@1.0.8": {
24
+
"integrity": "fc66e846d8d38a47cffd274d80d2ca3f0de71040f855783724bb6b87f60891f5"
25
+
}
26
+
},
27
+
"npm": {
28
+
"@atproto/common-web@0.4.2": {
29
+
"integrity": "sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==",
30
+
"dependencies": [
31
+
"graphemer",
32
+
"multiformats@9.9.0",
33
+
"uint8arrays@3.0.0",
34
+
"zod"
35
+
]
36
+
},
37
+
"@atproto/common@0.4.11": {
38
+
"integrity": "sha512-Knv0viYXNMfCdIE7jLUiWJKnnMfEwg+vz2epJQi8WOjqtqCFb3W/3Jn72ZiuovIfpdm13MaOiny6w2NErUQC6g==",
39
+
"dependencies": [
40
+
"@atproto/common-web",
41
+
"@ipld/dag-cbor",
42
+
"cbor-x",
43
+
"iso-datestring-validator",
44
+
"multiformats@9.9.0",
45
+
"pino"
46
+
]
47
+
},
48
+
"@atproto/crypto@0.4.4": {
49
+
"integrity": "sha512-Yq9+crJ7WQl7sxStVpHgie5Z51R05etaK9DLWYG/7bR5T4bhdcIgF6IfklLShtZwLYdVVj+K15s0BqW9a8PSDA==",
50
+
"dependencies": [
51
+
"@noble/curves",
52
+
"@noble/hashes",
53
+
"uint8arrays@3.0.0"
54
+
]
55
+
},
56
+
"@atproto/lex-cli@0.8.2": {
57
+
"integrity": "sha512-yNQFYBV3tBBLnVrRUtUBlx/WIF4ypMFsvOsCLjA7pHL1SyW9JbczSEAoiNtoDmPc4UXCjMtXggz0ovBG8lynNA==",
58
+
"dependencies": [
59
+
"@atproto/lexicon",
60
+
"@atproto/syntax",
61
+
"chalk",
62
+
"commander",
63
+
"prettier",
64
+
"ts-morph",
65
+
"yesno",
66
+
"zod"
67
+
],
68
+
"bin": true
69
+
},
70
+
"@atproto/lexicon@0.4.11": {
71
+
"integrity": "sha512-btefdnvNz2Ao2I+qbmj0F06HC8IlrM/IBz6qOBS50r0S6uDf5tOO+Mv2tSVdimFkdzyDdLtBI1sV36ONxz2cOw==",
72
+
"dependencies": [
73
+
"@atproto/common-web",
74
+
"@atproto/syntax",
75
+
"iso-datestring-validator",
76
+
"multiformats@9.9.0",
77
+
"zod"
78
+
]
79
+
},
80
+
"@atproto/syntax@0.4.0": {
81
+
"integrity": "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="
82
+
},
83
+
"@atproto/xrpc-server@0.7.19": {
84
+
"integrity": "sha512-YSCl/tU2NDykgDYslFSOYCr96esUgDwncFiADKL59/fyIFPLoT0qY8Uq/budpxUh0qPzjow4HHgVWESOaOpUmA==",
85
+
"dependencies": [
86
+
"@atproto/common",
87
+
"@atproto/crypto",
88
+
"@atproto/lexicon",
89
+
"@atproto/xrpc",
90
+
"cbor-x",
91
+
"express",
92
+
"http-errors",
93
+
"mime-types",
94
+
"rate-limiter-flexible",
95
+
"uint8arrays@3.0.0",
96
+
"ws",
97
+
"zod"
98
+
]
99
+
},
100
+
"@atproto/xrpc@0.7.0": {
101
+
"integrity": "sha512-SfhP9dGx2qclaScFDb58Jnrmim5nk4geZXCqg6sB0I/KZhZEkr9iIx1hLCp+sxkIfEsmEJjeWO4B0rjUIJW5cw==",
102
+
"dependencies": [
103
+
"@atproto/lexicon",
104
+
"zod"
105
+
]
106
+
},
107
+
"@cbor-extract/cbor-extract-darwin-arm64@2.2.0": {
108
+
"integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==",
109
+
"os": ["darwin"],
110
+
"cpu": ["arm64"]
111
+
},
112
+
"@cbor-extract/cbor-extract-darwin-x64@2.2.0": {
113
+
"integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==",
114
+
"os": ["darwin"],
115
+
"cpu": ["x64"]
116
+
},
117
+
"@cbor-extract/cbor-extract-linux-arm64@2.2.0": {
118
+
"integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==",
119
+
"os": ["linux"],
120
+
"cpu": ["arm64"]
121
+
},
122
+
"@cbor-extract/cbor-extract-linux-arm@2.2.0": {
123
+
"integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==",
124
+
"os": ["linux"],
125
+
"cpu": ["arm"]
126
+
},
127
+
"@cbor-extract/cbor-extract-linux-x64@2.2.0": {
128
+
"integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==",
129
+
"os": ["linux"],
130
+
"cpu": ["x64"]
131
+
},
132
+
"@cbor-extract/cbor-extract-win32-x64@2.2.0": {
133
+
"integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==",
134
+
"os": ["win32"],
135
+
"cpu": ["x64"]
136
+
},
137
+
"@ipld/dag-cbor@7.0.3": {
138
+
"integrity": "sha512-1VVh2huHsuohdXC1bGJNE8WR72slZ9XE2T3wbBBq31dm7ZBatmKLLxrB+XAqafxfRFjv08RZmj/W/ZqaM13AuA==",
139
+
"dependencies": [
140
+
"cborg",
141
+
"multiformats@9.9.0"
142
+
]
143
+
},
144
+
"@noble/curves@1.9.2": {
145
+
"integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==",
146
+
"dependencies": [
147
+
"@noble/hashes"
148
+
]
149
+
},
150
+
"@noble/hashes@1.8.0": {
151
+
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="
152
+
},
153
+
"@ts-morph/common@0.25.0": {
154
+
"integrity": "sha512-kMnZz+vGGHi4GoHnLmMhGNjm44kGtKUXGnOvrKmMwAuvNjM/PgKVGfUnL7IDvK7Jb2QQ82jq3Zmp04Gy+r3Dkg==",
155
+
"dependencies": [
156
+
"minimatch",
157
+
"path-browserify",
158
+
"tinyglobby"
159
+
]
160
+
},
161
+
"@types/node@22.15.15": {
162
+
"integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==",
163
+
"dependencies": [
164
+
"undici-types"
165
+
]
166
+
},
167
+
"abort-controller@3.0.0": {
168
+
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
169
+
"dependencies": [
170
+
"event-target-shim"
171
+
]
172
+
},
173
+
"accepts@1.3.8": {
174
+
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
175
+
"dependencies": [
176
+
"mime-types",
177
+
"negotiator"
178
+
]
179
+
},
180
+
"ansi-styles@4.3.0": {
181
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
182
+
"dependencies": [
183
+
"color-convert"
184
+
]
185
+
},
186
+
"array-flatten@1.1.1": {
187
+
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
188
+
},
189
+
"atomic-sleep@1.0.0": {
190
+
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="
191
+
},
192
+
"balanced-match@1.0.2": {
193
+
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
194
+
},
195
+
"base64-js@1.5.1": {
196
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
197
+
},
198
+
"body-parser@1.20.3": {
199
+
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
200
+
"dependencies": [
201
+
"bytes",
202
+
"content-type",
203
+
"debug",
204
+
"depd",
205
+
"destroy",
206
+
"http-errors",
207
+
"iconv-lite",
208
+
"on-finished",
209
+
"qs",
210
+
"raw-body",
211
+
"type-is",
212
+
"unpipe"
213
+
]
214
+
},
215
+
"brace-expansion@2.0.1": {
216
+
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
217
+
"dependencies": [
218
+
"balanced-match"
219
+
]
220
+
},
221
+
"buffer@6.0.3": {
222
+
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
223
+
"dependencies": [
224
+
"base64-js",
225
+
"ieee754"
226
+
]
227
+
},
228
+
"bytes@3.1.2": {
229
+
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
230
+
},
231
+
"call-bind-apply-helpers@1.0.2": {
232
+
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
233
+
"dependencies": [
234
+
"es-errors",
235
+
"function-bind"
236
+
]
237
+
},
238
+
"call-bound@1.0.4": {
239
+
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
240
+
"dependencies": [
241
+
"call-bind-apply-helpers",
242
+
"get-intrinsic"
243
+
]
244
+
},
245
+
"cbor-extract@2.2.0": {
246
+
"integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==",
247
+
"dependencies": [
248
+
"node-gyp-build-optional-packages"
249
+
],
250
+
"optionalDependencies": [
251
+
"@cbor-extract/cbor-extract-darwin-arm64",
252
+
"@cbor-extract/cbor-extract-darwin-x64",
253
+
"@cbor-extract/cbor-extract-linux-arm",
254
+
"@cbor-extract/cbor-extract-linux-arm64",
255
+
"@cbor-extract/cbor-extract-linux-x64",
256
+
"@cbor-extract/cbor-extract-win32-x64"
257
+
],
258
+
"scripts": true,
259
+
"bin": true
260
+
},
261
+
"cbor-x@1.6.0": {
262
+
"integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==",
263
+
"optionalDependencies": [
264
+
"cbor-extract"
265
+
]
266
+
},
267
+
"cborg@1.10.2": {
268
+
"integrity": "sha512-b3tFPA9pUr2zCUiCfRd2+wok2/LBSNUMKOuRRok+WlvvAgEt/PlbgPTsZUcwCOs53IJvLgTp0eotwtosE6njug==",
269
+
"bin": true
270
+
},
271
+
"chalk@4.1.2": {
272
+
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
273
+
"dependencies": [
274
+
"ansi-styles",
275
+
"supports-color"
276
+
]
277
+
},
278
+
"code-block-writer@13.0.3": {
279
+
"integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="
280
+
},
281
+
"color-convert@2.0.1": {
282
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
283
+
"dependencies": [
284
+
"color-name"
285
+
]
286
+
},
287
+
"color-name@1.1.4": {
288
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
289
+
},
290
+
"commander@9.5.0": {
291
+
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="
292
+
},
293
+
"content-disposition@0.5.4": {
294
+
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
295
+
"dependencies": [
296
+
"safe-buffer"
297
+
]
298
+
},
299
+
"content-type@1.0.5": {
300
+
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
301
+
},
302
+
"cookie-signature@1.0.6": {
303
+
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
304
+
},
305
+
"cookie@0.7.1": {
306
+
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
307
+
},
308
+
"debug@2.6.9": {
309
+
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
310
+
"dependencies": [
311
+
"ms@2.0.0"
312
+
]
313
+
},
314
+
"depd@2.0.0": {
315
+
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
316
+
},
317
+
"destroy@1.2.0": {
318
+
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
319
+
},
320
+
"detect-libc@2.0.4": {
321
+
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="
322
+
},
323
+
"dunder-proto@1.0.1": {
324
+
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
325
+
"dependencies": [
326
+
"call-bind-apply-helpers",
327
+
"es-errors",
328
+
"gopd"
329
+
]
330
+
},
331
+
"ee-first@1.1.1": {
332
+
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
333
+
},
334
+
"encodeurl@1.0.2": {
335
+
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
336
+
},
337
+
"encodeurl@2.0.0": {
338
+
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
339
+
},
340
+
"es-define-property@1.0.1": {
341
+
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
342
+
},
343
+
"es-errors@1.3.0": {
344
+
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
345
+
},
346
+
"es-object-atoms@1.1.1": {
347
+
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
348
+
"dependencies": [
349
+
"es-errors"
350
+
]
351
+
},
352
+
"escape-html@1.0.3": {
353
+
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
354
+
},
355
+
"etag@1.8.1": {
356
+
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
357
+
},
358
+
"event-target-shim@5.0.1": {
359
+
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
360
+
},
361
+
"events@3.3.0": {
362
+
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
363
+
},
364
+
"express@4.21.2": {
365
+
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
366
+
"dependencies": [
367
+
"accepts",
368
+
"array-flatten",
369
+
"body-parser",
370
+
"content-disposition",
371
+
"content-type",
372
+
"cookie",
373
+
"cookie-signature",
374
+
"debug",
375
+
"depd",
376
+
"encodeurl@2.0.0",
377
+
"escape-html",
378
+
"etag",
379
+
"finalhandler",
380
+
"fresh",
381
+
"http-errors",
382
+
"merge-descriptors",
383
+
"methods",
384
+
"on-finished",
385
+
"parseurl",
386
+
"path-to-regexp",
387
+
"proxy-addr",
388
+
"qs",
389
+
"range-parser",
390
+
"safe-buffer",
391
+
"send",
392
+
"serve-static",
393
+
"setprototypeof",
394
+
"statuses",
395
+
"type-is",
396
+
"utils-merge",
397
+
"vary"
398
+
]
399
+
},
400
+
"fast-redact@3.5.0": {
401
+
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="
402
+
},
403
+
"fdir@6.4.5_picomatch@4.0.2": {
404
+
"integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==",
405
+
"dependencies": [
406
+
"picomatch"
407
+
],
408
+
"optionalPeers": [
409
+
"picomatch"
410
+
]
411
+
},
412
+
"finalhandler@1.3.1": {
413
+
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
414
+
"dependencies": [
415
+
"debug",
416
+
"encodeurl@2.0.0",
417
+
"escape-html",
418
+
"on-finished",
419
+
"parseurl",
420
+
"statuses",
421
+
"unpipe"
422
+
]
423
+
},
424
+
"forwarded@0.2.0": {
425
+
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
426
+
},
427
+
"fresh@0.5.2": {
428
+
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
429
+
},
430
+
"function-bind@1.1.2": {
431
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
432
+
},
433
+
"get-intrinsic@1.3.0": {
434
+
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
435
+
"dependencies": [
436
+
"call-bind-apply-helpers",
437
+
"es-define-property",
438
+
"es-errors",
439
+
"es-object-atoms",
440
+
"function-bind",
441
+
"get-proto",
442
+
"gopd",
443
+
"has-symbols",
444
+
"hasown",
445
+
"math-intrinsics"
446
+
]
447
+
},
448
+
"get-proto@1.0.1": {
449
+
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
450
+
"dependencies": [
451
+
"dunder-proto",
452
+
"es-object-atoms"
453
+
]
454
+
},
455
+
"gopd@1.2.0": {
456
+
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
457
+
},
458
+
"graphemer@1.4.0": {
459
+
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
460
+
},
461
+
"has-flag@4.0.0": {
462
+
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
463
+
},
464
+
"has-symbols@1.1.0": {
465
+
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
466
+
},
467
+
"hasown@2.0.2": {
468
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
469
+
"dependencies": [
470
+
"function-bind"
471
+
]
472
+
},
473
+
"http-errors@2.0.0": {
474
+
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
475
+
"dependencies": [
476
+
"depd",
477
+
"inherits",
478
+
"setprototypeof",
479
+
"statuses",
480
+
"toidentifier"
481
+
]
482
+
},
483
+
"iconv-lite@0.4.24": {
484
+
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
485
+
"dependencies": [
486
+
"safer-buffer"
487
+
]
488
+
},
489
+
"ieee754@1.2.1": {
490
+
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
491
+
},
492
+
"inherits@2.0.4": {
493
+
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
494
+
},
495
+
"ipaddr.js@1.9.1": {
496
+
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
497
+
},
498
+
"iso-datestring-validator@2.2.2": {
499
+
"integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA=="
500
+
},
501
+
"math-intrinsics@1.1.0": {
502
+
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
503
+
},
504
+
"media-typer@0.3.0": {
505
+
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
506
+
},
507
+
"merge-descriptors@1.0.3": {
508
+
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
509
+
},
510
+
"methods@1.1.2": {
511
+
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
512
+
},
513
+
"mime-db@1.52.0": {
514
+
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
515
+
},
516
+
"mime-types@2.1.35": {
517
+
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
518
+
"dependencies": [
519
+
"mime-db"
520
+
]
521
+
},
522
+
"mime@1.6.0": {
523
+
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
524
+
"bin": true
525
+
},
526
+
"minimatch@9.0.5": {
527
+
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
528
+
"dependencies": [
529
+
"brace-expansion"
530
+
]
531
+
},
532
+
"ms@2.0.0": {
533
+
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
534
+
},
535
+
"ms@2.1.3": {
536
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
537
+
},
538
+
"multiformats@13.3.6": {
539
+
"integrity": "sha512-yakbt9cPYj8d3vi/8o/XWm61MrOILo7fsTL0qxNx6zS0Nso6K5JqqS2WV7vK/KSuDBvrW3KfCwAdAgarAgOmww=="
540
+
},
541
+
"multiformats@9.9.0": {
542
+
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
543
+
},
544
+
"negotiator@0.6.3": {
545
+
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
546
+
},
547
+
"node-gyp-build-optional-packages@5.1.1": {
548
+
"integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==",
549
+
"dependencies": [
550
+
"detect-libc"
551
+
],
552
+
"bin": true
553
+
},
554
+
"object-inspect@1.13.4": {
555
+
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
556
+
},
557
+
"on-exit-leak-free@2.1.2": {
558
+
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="
559
+
},
560
+
"on-finished@2.4.1": {
561
+
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
562
+
"dependencies": [
563
+
"ee-first"
564
+
]
565
+
},
566
+
"parseurl@1.3.3": {
567
+
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
568
+
},
569
+
"path-browserify@1.0.1": {
570
+
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
571
+
},
572
+
"path-to-regexp@0.1.12": {
573
+
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
574
+
},
575
+
"picomatch@4.0.2": {
576
+
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="
577
+
},
578
+
"pino-abstract-transport@1.2.0": {
579
+
"integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==",
580
+
"dependencies": [
581
+
"readable-stream",
582
+
"split2"
583
+
]
584
+
},
585
+
"pino-std-serializers@6.2.2": {
586
+
"integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA=="
587
+
},
588
+
"pino@8.21.0": {
589
+
"integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==",
590
+
"dependencies": [
591
+
"atomic-sleep",
592
+
"fast-redact",
593
+
"on-exit-leak-free",
594
+
"pino-abstract-transport",
595
+
"pino-std-serializers",
596
+
"process-warning",
597
+
"quick-format-unescaped",
598
+
"real-require",
599
+
"safe-stable-stringify",
600
+
"sonic-boom",
601
+
"thread-stream"
602
+
],
603
+
"bin": true
604
+
},
605
+
"prettier@3.5.3": {
606
+
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
607
+
"bin": true
608
+
},
609
+
"process-warning@3.0.0": {
610
+
"integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ=="
611
+
},
612
+
"process@0.11.10": {
613
+
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
614
+
},
615
+
"proxy-addr@2.0.7": {
616
+
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
617
+
"dependencies": [
618
+
"forwarded",
619
+
"ipaddr.js"
620
+
]
621
+
},
622
+
"qs@6.13.0": {
623
+
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
624
+
"dependencies": [
625
+
"side-channel"
626
+
]
627
+
},
628
+
"quick-format-unescaped@4.0.4": {
629
+
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
630
+
},
631
+
"range-parser@1.2.1": {
632
+
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
633
+
},
634
+
"rate-limiter-flexible@2.4.2": {
635
+
"integrity": "sha512-rMATGGOdO1suFyf/mI5LYhts71g1sbdhmd6YvdiXO2gJnd42Tt6QS4JUKJKSWVVkMtBacm6l40FR7Trjo6Iruw=="
636
+
},
637
+
"raw-body@2.5.2": {
638
+
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
639
+
"dependencies": [
640
+
"bytes",
641
+
"http-errors",
642
+
"iconv-lite",
643
+
"unpipe"
644
+
]
645
+
},
646
+
"readable-stream@4.7.0": {
647
+
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
648
+
"dependencies": [
649
+
"abort-controller",
650
+
"buffer",
651
+
"events",
652
+
"process",
653
+
"string_decoder"
654
+
]
655
+
},
656
+
"real-require@0.2.0": {
657
+
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="
658
+
},
659
+
"safe-buffer@5.2.1": {
660
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
661
+
},
662
+
"safe-stable-stringify@2.5.0": {
663
+
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
664
+
},
665
+
"safer-buffer@2.1.2": {
666
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
667
+
},
668
+
"send@0.19.0": {
669
+
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
670
+
"dependencies": [
671
+
"debug",
672
+
"depd",
673
+
"destroy",
674
+
"encodeurl@1.0.2",
675
+
"escape-html",
676
+
"etag",
677
+
"fresh",
678
+
"http-errors",
679
+
"mime",
680
+
"ms@2.1.3",
681
+
"on-finished",
682
+
"range-parser",
683
+
"statuses"
684
+
]
685
+
},
686
+
"serve-static@1.16.2": {
687
+
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
688
+
"dependencies": [
689
+
"encodeurl@2.0.0",
690
+
"escape-html",
691
+
"parseurl",
692
+
"send"
693
+
]
694
+
},
695
+
"setprototypeof@1.2.0": {
696
+
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
697
+
},
698
+
"side-channel-list@1.0.0": {
699
+
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
700
+
"dependencies": [
701
+
"es-errors",
702
+
"object-inspect"
703
+
]
704
+
},
705
+
"side-channel-map@1.0.1": {
706
+
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
707
+
"dependencies": [
708
+
"call-bound",
709
+
"es-errors",
710
+
"get-intrinsic",
711
+
"object-inspect"
712
+
]
713
+
},
714
+
"side-channel-weakmap@1.0.2": {
715
+
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
716
+
"dependencies": [
717
+
"call-bound",
718
+
"es-errors",
719
+
"get-intrinsic",
720
+
"object-inspect",
721
+
"side-channel-map"
722
+
]
723
+
},
724
+
"side-channel@1.1.0": {
725
+
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
726
+
"dependencies": [
727
+
"es-errors",
728
+
"object-inspect",
729
+
"side-channel-list",
730
+
"side-channel-map",
731
+
"side-channel-weakmap"
732
+
]
733
+
},
734
+
"sonic-boom@3.8.1": {
735
+
"integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==",
736
+
"dependencies": [
737
+
"atomic-sleep"
738
+
]
739
+
},
740
+
"split2@4.2.0": {
741
+
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="
742
+
},
743
+
"statuses@2.0.1": {
744
+
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
745
+
},
746
+
"string_decoder@1.3.0": {
747
+
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
748
+
"dependencies": [
749
+
"safe-buffer"
750
+
]
751
+
},
752
+
"supports-color@7.2.0": {
753
+
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
754
+
"dependencies": [
755
+
"has-flag"
756
+
]
757
+
},
758
+
"thread-stream@2.7.0": {
759
+
"integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==",
760
+
"dependencies": [
761
+
"real-require"
762
+
]
763
+
},
764
+
"tinyglobby@0.2.14_picomatch@4.0.2": {
765
+
"integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
766
+
"dependencies": [
767
+
"fdir",
768
+
"picomatch"
769
+
]
770
+
},
771
+
"toidentifier@1.0.1": {
772
+
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
773
+
},
774
+
"ts-morph@24.0.0": {
775
+
"integrity": "sha512-2OAOg/Ob5yx9Et7ZX4CvTCc0UFoZHwLEJ+dpDPSUi5TgwwlTlX47w+iFRrEwzUZwYACjq83cgjS/Da50Ga37uw==",
776
+
"dependencies": [
777
+
"@ts-morph/common",
778
+
"code-block-writer"
779
+
]
780
+
},
781
+
"type-is@1.6.18": {
782
+
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
783
+
"dependencies": [
784
+
"media-typer",
785
+
"mime-types"
786
+
]
787
+
},
788
+
"uint8arrays@3.0.0": {
789
+
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
790
+
"dependencies": [
791
+
"multiformats@9.9.0"
792
+
]
793
+
},
794
+
"uint8arrays@5.1.0": {
795
+
"integrity": "sha512-vA6nFepEmlSKkMBnLBaUMVvAC4G3CTmO58C12y4sq6WPDOR7mOFYOi7GlrQ4djeSbP6JG9Pv9tJDM97PedRSww==",
796
+
"dependencies": [
797
+
"multiformats@13.3.6"
798
+
]
799
+
},
800
+
"undici-types@6.21.0": {
801
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
802
+
},
803
+
"unpipe@1.0.0": {
804
+
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
805
+
},
806
+
"utils-merge@1.0.1": {
807
+
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
808
+
},
809
+
"vary@1.1.2": {
810
+
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
811
+
},
812
+
"ws@8.18.2": {
813
+
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="
814
+
},
815
+
"yesno@0.4.0": {
816
+
"integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA=="
817
+
},
818
+
"zod@3.25.51": {
819
+
"integrity": "sha512-TQSnBldh+XSGL+opiSIq0575wvDPqu09AqWe1F7JhUMKY+M91/aGlK4MhpVNO7MgYfHcVCB1ffwAUTJzllKJqg=="
820
+
}
821
+
},
822
+
"workspace": {
823
+
"dependencies": [
824
+
"jsr:@std/assert@^1.0.13",
825
+
"npm:@atproto/common@~0.4.11",
826
+
"npm:@atproto/crypto@~0.4.4",
827
+
"npm:@atproto/lexicon@~0.4.11",
828
+
"npm:uint8arrays@^5.1.0"
829
+
]
830
+
}
831
+
}
+29
services/mod/fly.toml
+29
services/mod/fly.toml
···
1
+
# fly.toml app configuration file generated for grain-mod on 2025-06-08T23:51:19-07:00
2
+
#
3
+
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4
+
#
5
+
6
+
app = 'grain-mod'
7
+
primary_region = 'sea'
8
+
9
+
[build]
10
+
dockerfile = './Dockerfile'
11
+
12
+
[env]
13
+
MOD_SERVICE_DATABASE_URL = '/data/sqlite.db'
14
+
MOD_SERVICE_PORT = '8080'
15
+
16
+
[[mounts]]
17
+
source = 'data'
18
+
destination = '/data'
19
+
20
+
[http_service]
21
+
internal_port = 8080
22
+
force_https = true
23
+
auto_stop_machines = 'stop'
24
+
auto_start_machines = true
25
+
min_machines_running = 0
26
+
processes = ['app']
27
+
28
+
[[vm]]
29
+
size = 'shared-cpu-1x'
+7
services/mod/lexicons.json
+7
services/mod/lexicons.json
+192
services/mod/lexicons/com/atproto/label/defs.json
+192
services/mod/lexicons/com/atproto/label/defs.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.label.defs",
4
+
"defs": {
5
+
"label": {
6
+
"type": "object",
7
+
"required": [
8
+
"src",
9
+
"uri",
10
+
"val",
11
+
"cts"
12
+
],
13
+
"properties": {
14
+
"cid": {
15
+
"type": "string",
16
+
"format": "cid",
17
+
"description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
18
+
},
19
+
"cts": {
20
+
"type": "string",
21
+
"format": "datetime",
22
+
"description": "Timestamp when this label was created."
23
+
},
24
+
"exp": {
25
+
"type": "string",
26
+
"format": "datetime",
27
+
"description": "Timestamp at which this label expires (no longer applies)."
28
+
},
29
+
"neg": {
30
+
"type": "boolean",
31
+
"description": "If true, this is a negation label, overwriting a previous label."
32
+
},
33
+
"sig": {
34
+
"type": "bytes",
35
+
"description": "Signature of dag-cbor encoded label."
36
+
},
37
+
"src": {
38
+
"type": "string",
39
+
"format": "did",
40
+
"description": "DID of the actor who created this label."
41
+
},
42
+
"uri": {
43
+
"type": "string",
44
+
"format": "uri",
45
+
"description": "AT URI of the record, repository (account), or other resource that this label applies to."
46
+
},
47
+
"val": {
48
+
"type": "string",
49
+
"maxLength": 128,
50
+
"description": "The short string name of the value or type of this label."
51
+
},
52
+
"ver": {
53
+
"type": "integer",
54
+
"description": "The AT Protocol version of the label object."
55
+
}
56
+
},
57
+
"description": "Metadata tag on an atproto resource (eg, repo or record)."
58
+
},
59
+
"selfLabel": {
60
+
"type": "object",
61
+
"required": [
62
+
"val"
63
+
],
64
+
"properties": {
65
+
"val": {
66
+
"type": "string",
67
+
"maxLength": 128,
68
+
"description": "The short string name of the value or type of this label."
69
+
}
70
+
},
71
+
"description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel."
72
+
},
73
+
"labelValue": {
74
+
"type": "string",
75
+
"knownValues": [
76
+
"!hide",
77
+
"!no-promote",
78
+
"!warn",
79
+
"!no-unauthenticated",
80
+
"dmca-violation",
81
+
"doxxing",
82
+
"porn",
83
+
"sexual",
84
+
"nudity",
85
+
"nsfl",
86
+
"gore"
87
+
]
88
+
},
89
+
"selfLabels": {
90
+
"type": "object",
91
+
"required": [
92
+
"values"
93
+
],
94
+
"properties": {
95
+
"values": {
96
+
"type": "array",
97
+
"items": {
98
+
"ref": "#selfLabel",
99
+
"type": "ref"
100
+
},
101
+
"maxLength": 10
102
+
}
103
+
},
104
+
"description": "Metadata tags on an atproto record, published by the author within the record."
105
+
},
106
+
"labelValueDefinition": {
107
+
"type": "object",
108
+
"required": [
109
+
"identifier",
110
+
"severity",
111
+
"blurs",
112
+
"locales"
113
+
],
114
+
"properties": {
115
+
"blurs": {
116
+
"type": "string",
117
+
"description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
118
+
"knownValues": [
119
+
"content",
120
+
"media",
121
+
"none"
122
+
]
123
+
},
124
+
"locales": {
125
+
"type": "array",
126
+
"items": {
127
+
"ref": "#labelValueDefinitionStrings",
128
+
"type": "ref"
129
+
}
130
+
},
131
+
"severity": {
132
+
"type": "string",
133
+
"description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
134
+
"knownValues": [
135
+
"inform",
136
+
"alert",
137
+
"none"
138
+
]
139
+
},
140
+
"adultOnly": {
141
+
"type": "boolean",
142
+
"description": "Does the user need to have adult content enabled in order to configure this label?"
143
+
},
144
+
"identifier": {
145
+
"type": "string",
146
+
"maxLength": 100,
147
+
"description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
148
+
"maxGraphemes": 100
149
+
},
150
+
"defaultSetting": {
151
+
"type": "string",
152
+
"default": "warn",
153
+
"description": "The default setting for this label.",
154
+
"knownValues": [
155
+
"ignore",
156
+
"warn",
157
+
"hide"
158
+
]
159
+
}
160
+
},
161
+
"description": "Declares a label value and its expected interpretations and behaviors."
162
+
},
163
+
"labelValueDefinitionStrings": {
164
+
"type": "object",
165
+
"required": [
166
+
"lang",
167
+
"name",
168
+
"description"
169
+
],
170
+
"properties": {
171
+
"lang": {
172
+
"type": "string",
173
+
"format": "language",
174
+
"description": "The code of the language these strings are written in."
175
+
},
176
+
"name": {
177
+
"type": "string",
178
+
"maxLength": 640,
179
+
"description": "A short human-readable name for the label.",
180
+
"maxGraphemes": 64
181
+
},
182
+
"description": {
183
+
"type": "string",
184
+
"maxLength": 100000,
185
+
"description": "A longer description of what the label means and why it might be applied.",
186
+
"maxGraphemes": 10000
187
+
}
188
+
},
189
+
"description": "Strings which describe the label in the UI, localized into a specific language."
190
+
}
191
+
}
192
+
}
+63
services/mod/lexicons/com/atproto/label/queryLabels.json
+63
services/mod/lexicons/com/atproto/label/queryLabels.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.label.queryLabels",
4
+
"defs": {
5
+
"main": {
6
+
"type": "query",
7
+
"output": {
8
+
"schema": {
9
+
"type": "object",
10
+
"required": [
11
+
"labels"
12
+
],
13
+
"properties": {
14
+
"cursor": {
15
+
"type": "string"
16
+
},
17
+
"labels": {
18
+
"type": "array",
19
+
"items": {
20
+
"ref": "com.atproto.label.defs#label",
21
+
"type": "ref"
22
+
}
23
+
}
24
+
}
25
+
},
26
+
"encoding": "application/json"
27
+
},
28
+
"parameters": {
29
+
"type": "params",
30
+
"required": [
31
+
"uriPatterns"
32
+
],
33
+
"properties": {
34
+
"limit": {
35
+
"type": "integer",
36
+
"default": 50,
37
+
"maximum": 250,
38
+
"minimum": 1
39
+
},
40
+
"cursor": {
41
+
"type": "string"
42
+
},
43
+
"sources": {
44
+
"type": "array",
45
+
"items": {
46
+
"type": "string",
47
+
"format": "did"
48
+
},
49
+
"description": "Optional list of label sources (DIDs) to filter on."
50
+
},
51
+
"uriPatterns": {
52
+
"type": "array",
53
+
"items": {
54
+
"type": "string"
55
+
},
56
+
"description": "List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI."
57
+
}
58
+
}
59
+
},
60
+
"description": "Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth."
61
+
}
62
+
}
63
+
}
+69
services/mod/lexicons/com/atproto/label/subscribeLabels.json
+69
services/mod/lexicons/com/atproto/label/subscribeLabels.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "com.atproto.label.subscribeLabels",
4
+
"defs": {
5
+
"info": {
6
+
"type": "object",
7
+
"required": [
8
+
"name"
9
+
],
10
+
"properties": {
11
+
"name": {
12
+
"type": "string",
13
+
"knownValues": [
14
+
"OutdatedCursor"
15
+
]
16
+
},
17
+
"message": {
18
+
"type": "string"
19
+
}
20
+
}
21
+
},
22
+
"main": {
23
+
"type": "subscription",
24
+
"errors": [
25
+
{
26
+
"name": "FutureCursor"
27
+
}
28
+
],
29
+
"message": {
30
+
"schema": {
31
+
"refs": [
32
+
"#labels",
33
+
"#info"
34
+
],
35
+
"type": "union"
36
+
}
37
+
},
38
+
"parameters": {
39
+
"type": "params",
40
+
"properties": {
41
+
"cursor": {
42
+
"type": "integer",
43
+
"description": "The last known event seq number to backfill from."
44
+
}
45
+
}
46
+
},
47
+
"description": "Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream."
48
+
},
49
+
"labels": {
50
+
"type": "object",
51
+
"required": [
52
+
"seq",
53
+
"labels"
54
+
],
55
+
"properties": {
56
+
"seq": {
57
+
"type": "integer"
58
+
},
59
+
"labels": {
60
+
"type": "array",
61
+
"items": {
62
+
"ref": "com.atproto.label.defs#label",
63
+
"type": "ref"
64
+
}
65
+
}
66
+
}
67
+
}
68
+
}
69
+
}
+513
services/mod/main.ts
+513
services/mod/main.ts
···
1
+
import type { Label } from "$lexicon/types/com/atproto/label/defs.ts";
2
+
import type { Labels } from "$lexicon/types/com/atproto/label/subscribeLabels.ts";
3
+
import { cborEncode, noUndefinedVals } from "@atproto/common";
4
+
import { Keypair, Secp256k1Keypair } from "@atproto/crypto";
5
+
import { stringifyLex } from "@atproto/lexicon";
6
+
import { assertEquals, assertExists } from "@std/assert";
7
+
import { DatabaseSync } from "node:sqlite";
8
+
import * as ui8 from "uint8arrays";
9
+
10
+
if (import.meta.main) {
11
+
const cfg = await createConfig();
12
+
const db = createDb(cfg);
13
+
const modService = createModService(db);
14
+
const handler = createHandler(modService);
15
+
16
+
Deno.serve({
17
+
port: cfg.port,
18
+
onListen() {
19
+
console.log(`Listening on http://localhost:${cfg.port}`);
20
+
},
21
+
onError(err) {
22
+
console.error("Error occurred:", err);
23
+
return new Response("Internal Server Error", {
24
+
status: 500,
25
+
});
26
+
},
27
+
}, handler);
28
+
29
+
Deno.addSignalListener("SIGINT", () => {
30
+
console.log("Shutting down server...");
31
+
Deno.exit(0);
32
+
});
33
+
}
34
+
35
+
type Config = {
36
+
port: number;
37
+
databaseUrl: string;
38
+
signingKey: string;
39
+
};
40
+
41
+
export async function createConfig(): Promise<Config> {
42
+
return {
43
+
port: Number(Deno.env.get("MOD_SERVICE_PORT")) || 8080,
44
+
databaseUrl: Deno.env.get("MOD_SERVICE_DATABASE_URL") ?? ":memory:",
45
+
signingKey: Deno.env.get("MOD_SERVICE_SIGNING_KEY") ??
46
+
await createSigningKey(),
47
+
};
48
+
}
49
+
50
+
async function createSigningKey() {
51
+
const serviceKeypair = await Secp256k1Keypair.create({ exportable: true });
52
+
return ui8.toString(await serviceKeypair.export(), "hex");
53
+
}
54
+
55
+
// Track all connected WebSocket clients for label subscriptions
56
+
const labelSubscribers = new Set<WebSocket>();
57
+
58
+
export function broadcastLabel(label: LabelRow) {
59
+
// Only broadcast if this label is active (not negated, not expired, and latest)
60
+
// Gather all labels for this (src, uri, val) and check if this is the latest and not negated/expired
61
+
// For simplicity, assume label is already the latest for this key
62
+
if (label.exp && new Date(label.exp).getTime() < Date.now()) return;
63
+
if (label.neg) return;
64
+
const msg = stringifyLex({
65
+
seq: label.id,
66
+
labels: [formatLabel(label)],
67
+
} as Labels);
68
+
for (const ws of labelSubscribers) {
69
+
try {
70
+
ws.send(msg);
71
+
} catch (e) {
72
+
console.error("Error sending label to subscriber:", e);
73
+
labelSubscribers.delete(ws);
74
+
}
75
+
}
76
+
}
77
+
78
+
export async function handleSubscribeLabels(
79
+
req: Request,
80
+
modService: ReturnType<typeof createModService>,
81
+
): Promise<Response> {
82
+
const { searchParams } = new URL(req.url);
83
+
const cursorParam = searchParams.get("cursor");
84
+
const cursor = cursorParam ? parseInt(cursorParam, 10) : 0;
85
+
if (cursorParam && Number.isNaN(cursor)) {
86
+
return new Response(
87
+
JSON.stringify({ error: "Cursor must be an integer" }),
88
+
{ status: 400 },
89
+
);
90
+
}
91
+
const { response, socket } = Deno.upgradeWebSocket(req);
92
+
// On open, send all labels after the cursor (including negations and expired)
93
+
socket.onopen = () => {
94
+
try {
95
+
const { rows } = modService.getLabels({
96
+
patterns: [],
97
+
sources: [],
98
+
limit: 1000,
99
+
cursor,
100
+
});
101
+
// Send ALL rows, not just active (per ATProto spec)
102
+
for (const row of rows) {
103
+
const msg = stringifyLex({
104
+
seq: row.id,
105
+
labels: [formatLabel(row)],
106
+
} as Labels);
107
+
socket.send(msg);
108
+
}
109
+
labelSubscribers.add(socket);
110
+
const userAgent = req.headers.get("user-agent");
111
+
const origin = req.headers.get("origin");
112
+
console.log(
113
+
`New subscriber connected, total: ${labelSubscribers.size}, user-agent: ${userAgent}, origin: ${origin}, time: ${
114
+
new Date().toISOString()
115
+
}`,
116
+
);
117
+
} catch (e) {
118
+
console.error("Error sending initial labels:", e);
119
+
socket.close();
120
+
}
121
+
};
122
+
socket.onclose = () => {
123
+
labelSubscribers.delete(socket);
124
+
};
125
+
return response;
126
+
}
127
+
128
+
// Filter for active (non-negated, non-expired) labels
129
+
// Used for API hydration only. For the WebSocket stream, send all labels (including negations and expired).
130
+
function filterActiveLabels(labels: LabelRow[]): LabelRow[] {
131
+
const now = Date.now();
132
+
const latest: Record<string, LabelRow> = {};
133
+
for (const label of labels) {
134
+
if (label.exp && new Date(label.exp).getTime() < now) continue;
135
+
const key = `${label.src}|${label.uri}|${label.val}`;
136
+
if (!latest[key] || new Date(label.cts) > new Date(latest[key].cts)) {
137
+
latest[key] = label;
138
+
}
139
+
}
140
+
return Object.values(latest).filter((label) => !label.neg);
141
+
}
142
+
143
+
function createHandler(
144
+
modService: ReturnType<typeof createModService>,
145
+
) {
146
+
return async (req: Request): Promise<Response> => {
147
+
const url = new URL(req.url);
148
+
const pathname = url.pathname;
149
+
150
+
if (
151
+
pathname === "/xrpc/com.atproto.label.subscribeLabels" &&
152
+
req.headers.get("upgrade")?.toLowerCase() === "websocket"
153
+
) {
154
+
return await handleSubscribeLabels(req, modService);
155
+
} else if (
156
+
req.method === "GET" && pathname === "/xrpc/com.atproto.label.queryLabels"
157
+
) {
158
+
// Parse query params
159
+
const uriPatternsParam = url.searchParams.getAll("uriPatterns");
160
+
const sourcesParam = url.searchParams.getAll("sources");
161
+
const cursorParam = url.searchParams.get("cursor");
162
+
const limitParam = url.searchParams.get("limit");
163
+
164
+
const uriPatterns: string[] = uriPatternsParam.length
165
+
? uriPatternsParam
166
+
: [];
167
+
const sources: string[] = sourcesParam.length ? sourcesParam : [];
168
+
const cursor = cursorParam ? parseInt(cursorParam, 10) : 0;
169
+
if (cursorParam && Number.isNaN(cursor)) {
170
+
return new Response(
171
+
JSON.stringify({
172
+
error: "Cursor must be an integer",
173
+
}),
174
+
{ status: 400 },
175
+
);
176
+
}
177
+
const limit = limitParam ? parseInt(limitParam, 10) : 50;
178
+
if (Number.isNaN(limit) || limit < 1 || limit > 250) {
179
+
return new Response(
180
+
JSON.stringify({
181
+
error: "Limit must be an integer between 1 and 250",
182
+
}),
183
+
{ status: 400 },
184
+
);
185
+
}
186
+
187
+
// Handle wildcards and SQL LIKE
188
+
const patterns = uriPatterns.includes("*")
189
+
? []
190
+
: uriPatterns.map((pattern) => {
191
+
pattern = pattern.replace(/%/g, "").replace(/_/g, "\\_");
192
+
const starIndex = pattern.indexOf("*");
193
+
if (starIndex === -1) return pattern;
194
+
if (starIndex !== pattern.length - 1) {
195
+
return undefined; // Only trailing wildcards supported
196
+
}
197
+
return pattern.slice(0, -1) + "%";
198
+
}).filter(Boolean) as string[];
199
+
200
+
const { rows: labelRows, nextCursor } = modService.getLabels({
201
+
patterns,
202
+
sources,
203
+
limit,
204
+
cursor,
205
+
});
206
+
const activeLabels = filterActiveLabels(labelRows);
207
+
const formattedRows = activeLabels.map(formatLabel);
208
+
return new Response(
209
+
stringifyLex({ cursor: nextCursor, labels: formattedRows }),
210
+
{
211
+
headers: { "content-type": "application/json" },
212
+
},
213
+
);
214
+
} else if (req.method === "GET" && pathname === "/health") {
215
+
// Use the modService's DB to check health
216
+
modService.getLabels({
217
+
patterns: [],
218
+
sources: [],
219
+
limit: 1,
220
+
cursor: 0,
221
+
});
222
+
return new Response(JSON.stringify({ version: "0.1.0" }), {
223
+
headers: { "content-type": "application/json" },
224
+
});
225
+
} else if (pathname.startsWith("/xrpc/")) {
226
+
return new Response("Method Not Implemented", { status: 501 });
227
+
}
228
+
229
+
return new Response("Not Found", { status: 404 });
230
+
};
231
+
}
232
+
233
+
export function createDb(cfg: Config) {
234
+
const db = new DatabaseSync(cfg.databaseUrl);
235
+
236
+
db.exec(`
237
+
PRAGMA journal_mode = WAL;
238
+
239
+
CREATE TABLE IF NOT EXISTS labels (
240
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
241
+
src TEXT NOT NULL,
242
+
uri TEXT NOT NULL,
243
+
cid TEXT,
244
+
val TEXT NOT NULL,
245
+
neg BOOLEAN DEFAULT FALSE,
246
+
cts DATETIME NOT NULL,
247
+
exp DATETIME,
248
+
sig BLOB
249
+
);
250
+
`);
251
+
252
+
return db;
253
+
}
254
+
255
+
export type LabelRow = {
256
+
id: number;
257
+
src: string;
258
+
uri: string;
259
+
cid: string | null;
260
+
val: string;
261
+
neg: boolean;
262
+
cts: string;
263
+
exp: string | null;
264
+
sig: Uint8Array;
265
+
};
266
+
267
+
type UnsignedLabel = Omit<Label, "sig">;
268
+
type SignedLabel = Label & { sig: Uint8Array };
269
+
270
+
export function createModService(db: DatabaseSync) {
271
+
return {
272
+
insertLabel: (label: SignedLabel) => {
273
+
const result = db.prepare(
274
+
`INSERT INTO labels (src, uri, cid, val, neg, cts, exp, sig)
275
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
276
+
).run(
277
+
label.src,
278
+
label.uri,
279
+
label.cid ?? null,
280
+
label.val,
281
+
label.neg ? 1 : 0,
282
+
label.cts,
283
+
label.exp ?? null,
284
+
label.sig,
285
+
);
286
+
return result.lastInsertRowid as number;
287
+
},
288
+
getLabels: (opts: {
289
+
patterns: string[];
290
+
sources: string[];
291
+
limit: number;
292
+
cursor: number;
293
+
}): { rows: LabelRow[]; nextCursor: string } => {
294
+
const { patterns, sources, limit, cursor } = opts;
295
+
const conditions: string[] = [];
296
+
const params: (string | number | Uint8Array | null)[] = [];
297
+
if (patterns.length) {
298
+
conditions.push(
299
+
"(" + patterns.map(() => "uri LIKE ?").join(" OR ") + ")",
300
+
);
301
+
params.push(...patterns);
302
+
}
303
+
if (sources.length) {
304
+
conditions.push(`src IN (${sources.map(() => "?").join(", ")})`);
305
+
params.push(...sources);
306
+
}
307
+
if (cursor) {
308
+
conditions.push("id > ?");
309
+
params.push(cursor);
310
+
}
311
+
params.push(limit);
312
+
const whereClause = conditions.length
313
+
? `WHERE ${conditions.join(" AND ")}`
314
+
: "";
315
+
const sql = `SELECT * FROM labels ${whereClause} ORDER BY id ASC LIMIT ?`;
316
+
const stmt = db.prepare(sql);
317
+
const rows = stmt.all(...params) as unknown[];
318
+
function rowToLabelRow(row: unknown): LabelRow {
319
+
if (typeof row !== "object" || row === null) {
320
+
throw new Error("Invalid row");
321
+
}
322
+
const r = row as Record<string, unknown>;
323
+
const src = typeof r.src === "string" ? r.src : "";
324
+
const uri = typeof r.uri === "string" ? r.uri : "";
325
+
let cid: string | null = null;
326
+
if (typeof r.cid === "string") cid = r.cid;
327
+
const val = typeof r.val === "string" ? r.val : "";
328
+
const cts = typeof r.cts === "string" ? r.cts : "";
329
+
let exp: string | null = null;
330
+
if (typeof r.exp === "string") exp = r.exp;
331
+
const neg = typeof r.neg === "boolean" ? r.neg : Number(r.neg) === 1;
332
+
let sig: Uint8Array;
333
+
if (r.sig instanceof Uint8Array) sig = r.sig;
334
+
else if (Array.isArray(r.sig)) sig = new Uint8Array(r.sig);
335
+
else sig = new Uint8Array();
336
+
const id = typeof r.id === "number" ? r.id : Number(r.id);
337
+
return { id, src, uri, cid, val, neg, cts, exp, sig };
338
+
}
339
+
const labelRows = rows.map(rowToLabelRow);
340
+
let nextCursor = "0";
341
+
if (rows.length > 0) {
342
+
const lastId = (rows[rows.length - 1] as Record<string, unknown>).id;
343
+
nextCursor = typeof lastId === "string" || typeof lastId === "number"
344
+
? String(lastId)
345
+
: "0";
346
+
}
347
+
return { rows: labelRows, nextCursor };
348
+
},
349
+
};
350
+
}
351
+
352
+
export function createLabel(
353
+
cfg: Config,
354
+
modService: ReturnType<typeof createModService>,
355
+
) {
356
+
return async (
357
+
label: UnsignedLabel,
358
+
) => {
359
+
const serviceSigningKey = cfg.signingKey;
360
+
if (!serviceSigningKey) {
361
+
throw new Error("MOD_SERVICE_SIGNING_KEY is not set");
362
+
}
363
+
const signingKey = await Secp256k1Keypair.import(serviceSigningKey);
364
+
365
+
const signed = await signLabel(label, signingKey);
366
+
367
+
const id = modService.insertLabel(signed);
368
+
// Broadcast the label to all subscribers after insert
369
+
const labelRow: LabelRow = {
370
+
id: Number(id),
371
+
src: signed.src,
372
+
uri: signed.uri,
373
+
cid: signed.cid ?? null,
374
+
val: signed.val,
375
+
neg: signed.neg === true, // ensure boolean
376
+
cts: signed.cts,
377
+
exp: signed.exp ?? null,
378
+
sig: signed.sig,
379
+
};
380
+
broadcastLabel(labelRow);
381
+
};
382
+
}
383
+
384
+
const formatLabel = (row: LabelRow): Label => {
385
+
return noUndefinedVals(
386
+
{
387
+
ver: 1,
388
+
src: row.src,
389
+
uri: row.uri,
390
+
cid: row.cid === "" || row.cid === null ? undefined : row.cid,
391
+
val: row.val,
392
+
neg: row.neg === true ? true : undefined,
393
+
cts: row.cts,
394
+
exp: row.exp ?? undefined,
395
+
sig: row.sig ? new Uint8Array(row.sig) : undefined,
396
+
} satisfies Label,
397
+
) as unknown as Label;
398
+
};
399
+
400
+
const signLabel = async (
401
+
label: Label,
402
+
signingKey: Keypair,
403
+
): Promise<SignedLabel> => {
404
+
const { ver, src, uri, cid, val, neg, cts, exp } = label;
405
+
const reformatted = noUndefinedVals(
406
+
{
407
+
ver: ver ?? 1,
408
+
src,
409
+
uri,
410
+
cid,
411
+
val,
412
+
neg: neg === true ? true : undefined,
413
+
cts,
414
+
exp,
415
+
} satisfies Label,
416
+
) as unknown as Label;
417
+
418
+
const bytes = cborEncode(reformatted);
419
+
const sig = await signingKey.sign(bytes);
420
+
return {
421
+
...reformatted,
422
+
sig,
423
+
};
424
+
};
425
+
426
+
Deno.test("insertLabel inserts a signed label and returns an id", async () => {
427
+
const cfg = await createConfig();
428
+
const db = createDb(cfg);
429
+
const modService = createModService(db);
430
+
431
+
const label = {
432
+
src: "did:example:alice",
433
+
uri: "at://did:example:bob/app.bsky.feed.post/123",
434
+
val: "spam",
435
+
neg: false,
436
+
cts: new Date().toISOString(),
437
+
sig: new Uint8Array([1, 2, 3]),
438
+
};
439
+
440
+
const id = modService.insertLabel(label);
441
+
assertExists(id);
442
+
443
+
// Check that the label is in the database
444
+
const row = db.prepare("SELECT * FROM labels WHERE id = ?").get(id);
445
+
assertExists(row);
446
+
assertEquals(row.src, label.src);
447
+
assertEquals(row.uri, label.uri);
448
+
assertEquals(row.val, label.val);
449
+
assertEquals(row.neg, 0);
450
+
assertEquals(row.sig, label.sig);
451
+
});
452
+
453
+
Deno.test("signLabel produces a valid signature", async () => {
454
+
const cfg = await createConfig();
455
+
const keyHex = cfg.signingKey;
456
+
const signingKey = await Secp256k1Keypair.import(keyHex);
457
+
458
+
const label: Label = {
459
+
ver: 1,
460
+
src: "did:example:alice",
461
+
uri: "at://did:example:bob/app.bsky.feed.post/123",
462
+
val: "spam",
463
+
cts: new Date().toISOString(),
464
+
};
465
+
466
+
const signed = await signLabel(label, signingKey);
467
+
468
+
assertExists(signed.sig);
469
+
assertEquals(signed.src, label.src);
470
+
assertEquals(signed.uri, label.uri);
471
+
assertEquals(signed.val, label.val);
472
+
});
473
+
474
+
Deno.test("getLabels retrieves labels with filtering and pagination", async () => {
475
+
const cfg = await createConfig();
476
+
const db = createDb(cfg);
477
+
const modService = createModService(db);
478
+
479
+
// Insert some test labels
480
+
for (let i = 1; i <= 10; i++) {
481
+
modService.insertLabel({
482
+
src: "did:example:alice",
483
+
uri: `at://did:example:bob/app.bsky.feed.post/${i}`,
484
+
val: i % 2 === 0 ? "spam" : "scam",
485
+
neg: false,
486
+
cts: new Date().toISOString(),
487
+
sig: new Uint8Array([1, 2, 3]),
488
+
});
489
+
}
490
+
491
+
// Retrieve labels with limit and cursor
492
+
const { rows, nextCursor } = modService.getLabels({
493
+
patterns: ["at://did:example:bob/app.bsky.feed.post/%"],
494
+
sources: ["did:example:alice"],
495
+
limit: 5,
496
+
cursor: 0,
497
+
});
498
+
499
+
assertEquals(rows.length, 5);
500
+
assertEquals(nextCursor, "5");
501
+
502
+
// Retrieve next page
503
+
const { rows: nextRows } = modService.getLabels({
504
+
patterns: ["at://did:example:bob/app.bsky.feed.post/%"],
505
+
sources: ["did:example:alice"],
506
+
limit: 5,
507
+
cursor: parseInt(nextCursor, 10),
508
+
});
509
+
510
+
assertEquals(nextRows.length, 5);
511
+
});
512
+
513
+
export type ModService = ReturnType<typeof createModService>;
+2
-2
services/pds/pdsadmin-command.sh
+2
-2
services/pds/pdsadmin-command.sh
···
21
21
# --header "Content-Type: application/json" \
22
22
# --user "admin:${PDS_ADMIN_PASSWORD}" \
23
23
# --data '{"useCount": 1}' \
24
-
# "https://${PDS_HOST}/xrpc/com.atproto.server.createInviteCode"
24
+
# "https://${PDS_HOSTNAME}/xrpc/com.atproto.server.createInviteCode"
25
25
26
26
# delete an account
27
27
# curl \
···
32
32
# --header "Content-Type: application/json" \
33
33
# --user "admin:${PDS_ADMIN_PASSWORD}" \
34
34
# --data "{\"did\": \"${DID}\"}" \
35
-
# "https://${PDS_HOST}/xrpc/com.atproto.admin.deleteAccount"
35
+
# "https://${PDS_HOSTNAME}/xrpc/com.atproto.admin.deleteAccount"
+41
src/components/DefaultLabelerAvatar.tsx
+41
src/components/DefaultLabelerAvatar.tsx
···
1
+
type DefaultLabelerAvatarProps = Readonly<{
2
+
size?: number;
3
+
backgroundColor?: string;
4
+
foregroundColor?: string;
5
+
class?: string;
6
+
}>;
7
+
8
+
export function DefaultLabelerAvatar({
9
+
size = 28,
10
+
backgroundColor = "rgb(139 92 246)", // Tailwind purple-500
11
+
foregroundColor = "#fff",
12
+
class: classProp,
13
+
}: DefaultLabelerAvatarProps) {
14
+
return (
15
+
<svg
16
+
width={size}
17
+
height={size}
18
+
viewBox="0 0 512 512"
19
+
class={classProp}
20
+
aria-hidden="true"
21
+
>
22
+
{/* Square background */}
23
+
<rect
24
+
x="0"
25
+
y="0"
26
+
width="512"
27
+
height="512"
28
+
rx="64"
29
+
fill={backgroundColor}
30
+
/>
31
+
32
+
{/* Shield icon (perfectly centered) */}
33
+
<g transform="translate(256, 280) scale(0.7) translate(-256, -256)">
34
+
<path
35
+
fill={foregroundColor}
36
+
d="M466.5 83.7 263.1 5.1c-5.9-2.2-12.3-2.2-18.2 0L45.5 83.7C36.6 87 30 95.7 30 105.3c0 198.6 114.6 289.7 221.2 325.7 4.5 1.5 9.3 1.5 13.8 0C367.4 395 482 303.9 482 105.3c0-9.6-6.6-18.3-15.5-21.6z"
37
+
/>
38
+
</g>
39
+
</svg>
40
+
);
41
+
}
+28
-20
src/components/GalleryPage.tsx
+28
-20
src/components/GalleryPage.tsx
···
5
5
import { AtUri } from "@atproto/syntax";
6
6
import { WithBffMeta } from "@bigmoves/bff";
7
7
import { Button } from "@bigmoves/bff/components";
8
+
import { ModerationDecsion } from "../lib/moderation.ts";
8
9
import { ActorInfo } from "./ActorInfo.tsx";
9
10
import { FavoriteButton } from "./FavoriteButton.tsx";
10
11
import { GalleryLayout } from "./GalleryLayout.tsx";
12
+
import { ModerationWrapper } from "./ModerationWrapper.tsx";
11
13
import { ShareGalleryButton } from "./ShareGalleryButton.tsx";
12
14
13
15
export function GalleryPage({
14
16
gallery,
15
17
favs = [],
16
18
currentUserDid,
19
+
modDecision,
17
20
}: Readonly<{
18
21
gallery: GalleryView;
19
22
favs: WithBffMeta<Favorite>[];
20
23
currentUserDid?: string;
24
+
modDecision?: ModerationDecsion;
21
25
}>) {
22
26
const isCreator = currentUserDid === gallery.creator.did;
23
27
const isLoggedIn = !!currentUserDid;
···
85
89
)
86
90
: null}
87
91
</div>
88
-
<GalleryLayout
89
-
layoutButtons={
90
-
<>
91
-
<GalleryLayout.ModeButton mode="justified" />
92
-
<GalleryLayout.ModeButton mode="masonry" />
93
-
</>
94
-
}
95
-
>
96
-
<GalleryLayout.Container>
97
-
{galleryItems?.length
98
-
? galleryItems.map((photo) => (
99
-
<GalleryLayout.Item
100
-
key={photo.cid}
101
-
photo={photo}
102
-
gallery={gallery}
103
-
/>
104
-
))
105
-
: null}
106
-
</GalleryLayout.Container>
107
-
</GalleryLayout>
92
+
{
93
+
<ModerationWrapper moderationDecision={modDecision} class="mb-2">
94
+
<GalleryLayout
95
+
layoutButtons={
96
+
<>
97
+
<GalleryLayout.ModeButton mode="justified" />
98
+
<GalleryLayout.ModeButton mode="masonry" />
99
+
</>
100
+
}
101
+
>
102
+
<GalleryLayout.Container>
103
+
{galleryItems?.length
104
+
? galleryItems.map((photo) => (
105
+
<GalleryLayout.Item
106
+
key={photo.cid}
107
+
photo={photo}
108
+
gallery={gallery}
109
+
/>
110
+
))
111
+
: null}
112
+
</GalleryLayout.Container>
113
+
</GalleryLayout>
114
+
</ModerationWrapper>
115
+
}
108
116
</div>
109
117
);
110
118
}
+1
-1
src/components/GalleryPreviewLink.tsx
+1
-1
src/components/GalleryPreviewLink.tsx
+17
src/components/LabelDefinitionButton.tsx
+17
src/components/LabelDefinitionButton.tsx
···
1
+
export function LabelDefinitionButton(
2
+
{ src, val }: Readonly<{ src: string; val: string }>,
3
+
) {
4
+
return (
5
+
<button
6
+
type="button"
7
+
class="text-sky-500 hover:underline cursor-pointer"
8
+
hx-get={`/dialogs/label/${src}/${val}`}
9
+
hx-trigger="click"
10
+
hx-target="#layout"
11
+
hx-swap="afterbegin"
12
+
_="on click halt"
13
+
>
14
+
Learn more
15
+
</button>
16
+
);
17
+
}
+33
src/components/LabelDefinitionDialog.tsx
+33
src/components/LabelDefinitionDialog.tsx
···
1
+
import { LabelValueDefinition } from "$lexicon/types/com/atproto/label/defs.ts";
2
+
import { Dialog } from "@bigmoves/bff/components";
3
+
import { profileLink } from "../utils.ts";
4
+
5
+
export function LabelDefinitionDialog({
6
+
labelValueDefinition,
7
+
labelByHandle,
8
+
}: Readonly<{
9
+
labelValueDefinition: LabelValueDefinition;
10
+
labelByHandle: string;
11
+
}>) {
12
+
const enLocale = labelValueDefinition.locales?.find(
13
+
(locale) => locale.lang === "en",
14
+
);
15
+
return (
16
+
<Dialog id="mod-decision-dialog" class="z-100">
17
+
<Dialog.Content class="dark:bg-zinc-950 relative gap-2">
18
+
<Dialog.X class="fill-zinc-950 dark:fill-zinc-50" />
19
+
<Dialog.Title>{enLocale?.name}</Dialog.Title>
20
+
<p>{enLocale?.description}</p>
21
+
<p>
22
+
Source:{" "}
23
+
<a
24
+
href={profileLink(labelByHandle)}
25
+
class="text-sky-500 hover:underline"
26
+
>
27
+
@{labelByHandle}
28
+
</a>
29
+
</p>
30
+
</Dialog.Content>
31
+
</Dialog>
32
+
);
33
+
}
+26
src/components/LabelerAvatar.tsx
+26
src/components/LabelerAvatar.tsx
···
1
+
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
2
+
import { Un$Typed } from "$lexicon/util.ts";
3
+
import { cn } from "@bigmoves/bff/components";
4
+
import { DefaultLabelerAvatar } from "./DefaultLabelerAvatar.tsx";
5
+
6
+
export function LabelerAvatar({
7
+
profile,
8
+
size,
9
+
class: classProp,
10
+
}: Readonly<
11
+
{ profile: Un$Typed<ProfileView>; size?: number; class?: string }
12
+
>) {
13
+
return (
14
+
profile.avatar
15
+
? (
16
+
<img
17
+
src={profile.avatar}
18
+
alt={profile.handle}
19
+
title={profile.handle}
20
+
class={cn("rounded-full object-cover", classProp)}
21
+
style={size ? { width: size, height: size } : undefined}
22
+
/>
23
+
)
24
+
: <DefaultLabelerAvatar size={size} class={classProp} />
25
+
);
26
+
}
+82
src/components/ModerationWrapper.tsx
+82
src/components/ModerationWrapper.tsx
···
1
+
import { ComponentChildren } from "preact";
2
+
import { ModerationDecsion } from "../lib/moderation.ts";
3
+
import { LabelDefinitionButton } from "./LabelDefinitionButton.tsx";
4
+
5
+
type ModerationWrapperProps = Readonly<{
6
+
class?: string;
7
+
moderationDecision?: ModerationDecsion;
8
+
children: ComponentChildren;
9
+
}>;
10
+
11
+
export function ModerationWrapper({
12
+
class: classProp,
13
+
moderationDecision,
14
+
children,
15
+
}: ModerationWrapperProps) {
16
+
const id = crypto.randomUUID();
17
+
return (
18
+
moderationDecision
19
+
? (
20
+
moderationDecision.isMe
21
+
? (
22
+
<div>
23
+
<button
24
+
type="button"
25
+
hx-get={`/dialogs/label/${moderationDecision.src}/${moderationDecision.val}`}
26
+
hx-trigger="click"
27
+
hx-target="#layout"
28
+
hx-swap="afterbegin"
29
+
_="on click halt"
30
+
class="flex items-center gap-2 bg-zinc-200 dark:bg-zinc-800 p-2 text-sm mb-2"
31
+
>
32
+
<i class="fa fa-circle-info text-zinc-500" />
33
+
A label has been placed on this gallery
34
+
</button>
35
+
{children}
36
+
</div>
37
+
)
38
+
: (
39
+
<div
40
+
id={`moderation-wrapper-${id}`}
41
+
data-state="closed"
42
+
class={classProp}
43
+
>
44
+
<div class="bg-zinc-200 dark:bg-zinc-800 p-4 w-full">
45
+
<div class="flex items-center justify-between gap-2 w-full">
46
+
<div class="flex items-center gap-2">
47
+
<i class="fa fa-circle-info text-zinc-500"></i>
48
+
<span class="text-sm">{moderationDecision?.name}</span>
49
+
</div>
50
+
<button
51
+
type="button"
52
+
class="text-sm font-semibold cursor-pointer"
53
+
_={`
54
+
on click
55
+
toggle .hidden on #mod-content-${id}
56
+
if my innerText is 'Show'
57
+
put 'Hide' into me
58
+
then put 'open' into @data-state of #moderation-wrapper-${id}
59
+
else
60
+
put 'Show' into me
61
+
then put 'closed' into @data-state of #moderation-wrapper-${id}`}
62
+
>
63
+
Show
64
+
</button>
65
+
</div>
66
+
</div>
67
+
<div class="text-sm my-2">
68
+
Labeled by @{moderationDecision?.labeledBy || "unknown"}.{" "}
69
+
<LabelDefinitionButton
70
+
src={moderationDecision.src}
71
+
val={moderationDecision.val}
72
+
/>
73
+
</div>
74
+
<div id={`mod-content-${id}`} class="hidden">
75
+
{children}
76
+
</div>
77
+
</div>
78
+
)
79
+
)
80
+
: children
81
+
);
82
+
}
+232
-140
src/components/ProfilePage.tsx
+232
-140
src/components/ProfilePage.tsx
···
1
+
import { LabelValueDefinition } from "$lexicon/types/com/atproto/label/defs.ts";
1
2
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
2
3
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
3
4
import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
4
5
import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
5
6
import { Un$Typed } from "$lexicon/util.ts";
6
7
import { AtUri } from "@atproto/syntax";
8
+
import { LabelerPolicies } from "@bigmoves/bff";
7
9
import { Button, cn } from "@bigmoves/bff/components";
10
+
import {
11
+
atprotoLabelValueDefinitions,
12
+
ModerationDecsion,
13
+
} from "../lib/moderation.ts";
8
14
import type { SocialNetwork } from "../lib/timeline.ts";
9
15
import {
10
16
bskyProfileLink,
···
16
22
import { ActorAvatar } from "./ActorAvatar.tsx";
17
23
import { AvatarButton } from "./AvatarButton.tsx";
18
24
import { FollowButton } from "./FollowButton.tsx";
25
+
import { LabelDefinitionButton } from "./LabelDefinitionButton.tsx";
26
+
import { LabelerAvatar } from "./LabelerAvatar.tsx";
19
27
20
-
export type ProfileTabs = "favs" | "galleries";
28
+
export type ProfileTabs = "favs" | "galleries" | "labels";
21
29
22
30
export function ProfilePage({
23
31
followUri,
···
29
37
selectedTab,
30
38
galleries,
31
39
galleryFavs,
40
+
galleryModDecisionsMap = new Map(),
41
+
isLabeler,
42
+
labelerDefinitions,
32
43
}: Readonly<{
33
44
followUri?: string;
34
45
followersCount?: number;
···
40
51
selectedTab?: ProfileTabs;
41
52
galleries?: GalleryView[];
42
53
galleryFavs?: GalleryView[];
54
+
galleryModDecisionsMap?: Map<string, ModerationDecsion>;
55
+
isLabeler?: boolean;
56
+
labelerDefinitions?: LabelerPolicies;
43
57
}>) {
44
58
const isCreator = loggedInUserDid === profile.did;
45
59
const displayName = profile.displayName || profile.handle;
···
47
61
<div class="px-4 mb-4" id="profile-page">
48
62
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4">
49
63
<div class="flex flex-col mb-4">
50
-
<AvatarButton profile={profile} />
64
+
{isLabeler
65
+
? <LabelerAvatar profile={profile} size={64} />
66
+
: <AvatarButton profile={profile} />}
51
67
<p class="text-2xl font-bold">{displayName}</p>
52
68
<p class="text-zinc-600 dark:text-zinc-500">@{profile.handle}</p>
53
-
<p class="space-x-1">
54
-
<a href={followersLink(profile.handle)}>
55
-
<span class="font-semibold" id="followers-count">
56
-
{followersCount ?? 0}
57
-
</span>{" "}
58
-
<span class="text-zinc-600 dark:text-zinc-500">followers</span>
59
-
</a>{" "}
60
-
<a href={followingLink(profile.handle)}>
61
-
<span class="font-semibold" id="following-count">
62
-
{followingCount ?? 0}
63
-
</span>{" "}
64
-
<span class="text-zinc-600 dark:text-zinc-500">following</span>
65
-
</a>{" "}
66
-
<span class="font-semibold">{galleries?.length ?? 0}</span>
67
-
<span class="text-zinc-600 dark:text-zinc-500">galleries</span>
68
-
</p>
69
+
{!isLabeler && (
70
+
<p class="space-x-1">
71
+
<a href={followersLink(profile.handle)}>
72
+
<span class="font-semibold" id="followers-count">
73
+
{followersCount ?? 0}
74
+
</span>{" "}
75
+
<span class="text-zinc-600 dark:text-zinc-500">followers</span>
76
+
</a>{" "}
77
+
<a href={followingLink(profile.handle)}>
78
+
<span class="font-semibold" id="following-count">
79
+
{followingCount ?? 0}
80
+
</span>{" "}
81
+
<span class="text-zinc-600 dark:text-zinc-500">following</span>
82
+
</a>{" "}
83
+
<span class="font-semibold">{galleries?.length ?? 0}</span>
84
+
<span class="text-zinc-600 dark:text-zinc-500">galleries</span>
85
+
</p>
86
+
)}
69
87
{profile.description
70
88
? <p class="mt-2 sm:max-w-[500px]">{profile.description}</p>
71
89
: null}
···
81
99
)}
82
100
</p>
83
101
</div>
84
-
{!isCreator && loggedInUserDid
102
+
{!isCreator && !isLabeler && loggedInUserDid
85
103
? (
86
104
<div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row">
87
105
<FollowButton
···
133
151
role="tablist"
134
152
style={{ WebkitOverflowScrolling: "touch" }}
135
153
>
136
-
<button
137
-
type="button"
138
-
name="tab"
139
-
value="galleries"
140
-
hx-get={profileLink(profile.handle)}
141
-
hx-target="#profile-page"
142
-
hx-swap="outerHTML"
143
-
class={cn(
144
-
"flex-1 min-w-[120px] py-2 px-4 cursor-pointer font-semibold",
145
-
selectedTab === "galleries" && "bg-zinc-100 dark:bg-zinc-800",
154
+
{isLabeler
155
+
? (
156
+
<button
157
+
type="button"
158
+
name="tab"
159
+
value="favs"
160
+
hx-get={profileLink(profile.handle)}
161
+
hx-target="#profile-page"
162
+
hx-swap="outerHTML"
163
+
class={cn(
164
+
"flex-1 min-w-[120px] py-2 px-4 cursor-pointer font-semibold",
165
+
selectedTab === "labels" && "bg-zinc-100 dark:bg-zinc-800",
166
+
)}
167
+
role="tab"
168
+
aria-selected={selectedTab === "labels"}
169
+
aria-controls="tab-content"
170
+
>
171
+
Labels
172
+
</button>
173
+
)
174
+
: (
175
+
<button
176
+
type="button"
177
+
name="tab"
178
+
value="galleries"
179
+
hx-get={profileLink(profile.handle)}
180
+
hx-target="#profile-page"
181
+
hx-swap="outerHTML"
182
+
class={cn(
183
+
"flex-1 min-w-[120px] py-2 px-4 cursor-pointer font-semibold",
184
+
selectedTab === "galleries" && "bg-zinc-100 dark:bg-zinc-800",
185
+
)}
186
+
role="tab"
187
+
aria-selected={selectedTab === "galleries"}
188
+
aria-controls="tab-content"
189
+
>
190
+
Galleries
191
+
</button>
146
192
)}
147
-
role="tab"
148
-
aria-selected={selectedTab === "galleries"}
149
-
aria-controls="tab-content"
150
-
>
151
-
Galleries
152
-
</button>
193
+
153
194
{isCreator && (
154
195
<button
155
196
type="button"
···
169
210
Favs
170
211
</button>
171
212
)}
172
-
{
173
-
/* <button
174
-
type="button"
175
-
hx-get={profileLink(profile.handle)}
176
-
hx-target="body"
177
-
hx-swap="outerHTML"
178
-
class={cn(
179
-
"flex-1 min-w-[120px] py-2 px-4 cursor-pointer font-semibold",
180
-
!selectedTab && "bg-zinc-100 dark:bg-zinc-800 font-semibold",
181
-
)}
182
-
role="tab"
183
-
aria-selected={!selectedTab}
184
-
aria-controls="tab-content"
185
-
hx-push-url="true"
186
-
>
187
-
Activity
188
-
</button> */
189
-
}
190
213
</div>
191
-
<div id="tab-content" role="tabpanel">
192
-
{selectedTab === "galleries"
193
-
? (
194
-
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
195
-
{galleries?.length
196
-
? (
197
-
galleries.map((gallery) => (
198
-
<a
199
-
href={galleryLink(
200
-
gallery.creator.handle,
201
-
new AtUri(gallery.uri).rkey,
202
-
)}
203
-
class="cursor-pointer relative aspect-square"
204
-
>
205
-
{gallery.items?.length
206
-
? (
207
-
<img
208
-
src={gallery.items?.filter(isPhotoView)?.[0]
209
-
?.fullsize}
210
-
alt={gallery.items?.filter(isPhotoView)?.[0]?.alt}
211
-
class="w-full h-full object-cover"
212
-
/>
213
-
)
214
-
: (
215
-
<div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />
216
-
)}
217
-
<div class="absolute bottom-0 left-0 bg-black/80 text-white p-2">
218
-
{(gallery.record as Gallery).title}
219
-
</div>
220
-
</a>
221
-
))
222
-
)
223
-
: <p>No galleries yet.</p>}
224
-
</div>
225
-
)
226
-
: null}
227
-
{selectedTab === "favs"
228
-
? (
229
-
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
230
-
{galleryFavs?.length
231
-
? (
232
-
galleryFavs.map((gallery) => (
233
-
<a
234
-
href={galleryLink(
235
-
gallery.creator.handle,
236
-
new AtUri(gallery.uri).rkey,
237
-
)}
238
-
class="cursor-pointer relative aspect-square"
239
-
>
240
-
{gallery.items?.length
241
-
? (
242
-
<img
243
-
src={gallery.items?.filter(isPhotoView)?.[0]
244
-
?.fullsize}
245
-
alt={gallery.items?.filter(isPhotoView)?.[0]?.alt}
246
-
class="w-full h-full object-cover"
247
-
/>
248
-
)
249
-
: (
250
-
<div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />
251
-
)}
252
-
<div class="absolute bottom-0 left-0 bg-black/80 text-white p-2 flex items-center gap-2">
253
-
<ActorAvatar profile={gallery.creator} size={20} />{" "}
254
-
{(gallery.record as Gallery).title}
255
-
</div>
256
-
</a>
257
-
))
258
-
)
259
-
: <p>No favs yet.</p>}
214
+
{selectedTab === "labels" && labelerDefinitions
215
+
? <LabelerPoliciesList defs={labelerDefinitions} />
216
+
: null}
217
+
{selectedTab === "galleries"
218
+
? (
219
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
220
+
{galleries?.length
221
+
? (
222
+
galleries.map((gallery) => (
223
+
<GalleryItem
224
+
key={gallery.uri}
225
+
gallery={gallery}
226
+
galleryModDecisionsMap={galleryModDecisionsMap}
227
+
/>
228
+
))
229
+
)
230
+
: <p>No galleries yet.</p>}
231
+
</div>
232
+
)
233
+
: null}
234
+
{selectedTab === "favs"
235
+
? (
236
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
237
+
{galleryFavs?.length
238
+
? (
239
+
galleryFavs.map((gallery) => (
240
+
<GalleryFavItem
241
+
key={gallery.uri}
242
+
gallery={gallery}
243
+
galleryModDecisionsMap={galleryModDecisionsMap}
244
+
/>
245
+
))
246
+
)
247
+
: <p>No favs yet.</p>}
248
+
</div>
249
+
)
250
+
: null}
251
+
</div>
252
+
);
253
+
}
254
+
255
+
function LabelerPoliciesList({ defs }: Readonly<{ defs: LabelerPolicies }>) {
256
+
if (!defs?.labelValues?.length) return <p>No labels yet.</p>;
257
+
return (
258
+
<ul class="space-y-4 relative divide-zinc-200 dark:divide-zinc-800 divide-y">
259
+
{defs?.labelValues?.map((val) => {
260
+
let def = defs?.labelValueDefinitions?.find((def) =>
261
+
def.identifier === val
262
+
);
263
+
// Fallback to atproto definitions if not found
264
+
def ??= atprotoLabelValueDefinitions[val];
265
+
if (!def) return null;
266
+
return <LabelValueDefinitionListItem key={def.identifier} def={def} />;
267
+
})}
268
+
</ul>
269
+
);
270
+
}
271
+
272
+
function LabelValueDefinitionListItem({
273
+
def,
274
+
}: Readonly<{ def: LabelValueDefinition }>) {
275
+
const enLocale = def.locales.find((v) => v.lang === "en");
276
+
return (
277
+
<li class="flex flex-col pb-4 gap-2">
278
+
<div class="font-semibold">{enLocale?.name}</div>
279
+
<div>{enLocale?.description}</div>
280
+
{def.adultOnly
281
+
? (
282
+
<div class="flex items-center gap-2 text-sm">
283
+
<i class="fa fa-info-circle" />{" "}
284
+
<span>Adult content is disabled.</span>
285
+
</div>
286
+
)
287
+
: null}
288
+
<div class="text-sm">
289
+
Default setting:{" "}
290
+
<span class="font-semibold">
291
+
{def.defaultSetting || "No default value set"}
292
+
</span>
293
+
</div>
294
+
</li>
295
+
);
296
+
}
297
+
298
+
function GalleryItem({
299
+
gallery,
300
+
galleryModDecisionsMap,
301
+
}: Readonly<{
302
+
gallery: GalleryView;
303
+
galleryModDecisionsMap: Map<string, ModerationDecsion>;
304
+
}>) {
305
+
const modDecision = galleryModDecisionsMap.get(gallery.uri);
306
+
return (
307
+
<a
308
+
href={galleryLink(
309
+
gallery.creator.handle,
310
+
new AtUri(gallery.uri).rkey,
311
+
)}
312
+
class="cursor-pointer relative aspect-square"
313
+
>
314
+
{modDecision && !modDecision.isMe
315
+
? (
316
+
<div class="w-full h-full bg-zinc-200 dark:bg-zinc-900 p-2 text-sm">
317
+
<i class="fa fa-circle-info text-zinc-500"></i> {modDecision.name}
318
+
<div class="text-sm">
319
+
Labeled by @{modDecision?.labeledBy || "unknown"}.{" "}
320
+
<LabelDefinitionButton
321
+
src={modDecision.src}
322
+
val={modDecision.val}
323
+
/>
260
324
</div>
261
-
)
262
-
: null}
263
-
{
264
-
/* {!selectedTab
265
-
? (
266
-
<ul class="space-y-4 relative divide-zinc-200 dark:divide-zinc-800 divide-y w-fit">
267
-
{timelineItems.length
268
-
? (
269
-
timelineItems.map((item) => (
270
-
<Item item={item} key={item.itemUri} />
271
-
))
272
-
)
273
-
: <li>No activity yet.</li>}
274
-
</ul>
275
-
)
276
-
: null} */
277
-
}
325
+
</div>
326
+
)
327
+
: gallery.items?.length
328
+
? (
329
+
<img
330
+
src={gallery.items?.filter(isPhotoView)?.[0]?.fullsize}
331
+
alt={gallery.items?.filter(isPhotoView)?.[0]?.alt}
332
+
class="w-full h-full object-cover"
333
+
/>
334
+
)
335
+
: <div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />}
336
+
<div class="absolute bottom-0 left-0 bg-black/80 text-white p-2 flex items-center gap-2">
337
+
{(gallery.record as Gallery).title}
338
+
</div>
339
+
</a>
340
+
);
341
+
}
342
+
343
+
function GalleryFavItem({
344
+
gallery,
345
+
galleryModDecisionsMap,
346
+
}: Readonly<{
347
+
gallery: GalleryView;
348
+
galleryModDecisionsMap: Map<string, ModerationDecsion>;
349
+
}>) {
350
+
return (
351
+
<a
352
+
href={galleryLink(
353
+
gallery.creator.handle,
354
+
new AtUri(gallery.uri).rkey,
355
+
)}
356
+
class="cursor-pointer relative aspect-square"
357
+
>
358
+
{gallery.items?.length
359
+
? (
360
+
<img
361
+
src={gallery.items?.filter(isPhotoView)?.[0]?.fullsize}
362
+
alt={gallery.items?.filter(isPhotoView)?.[0]?.alt}
363
+
class="w-full h-full object-cover"
364
+
/>
365
+
)
366
+
: <div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />}
367
+
<div class="absolute bottom-0 left-0 bg-black/80 text-white p-2 flex items-center gap-2">
368
+
<ActorAvatar profile={gallery.creator} size={20} />{" "}
369
+
{(gallery.record as Gallery).title}
278
370
</div>
279
-
</div>
371
+
</a>
280
372
);
281
373
}
+1
-1
src/components/Timeline.tsx
+1
-1
src/components/Timeline.tsx
···
93
93
</form>
94
94
)
95
95
: null}
96
-
<ul class="space-y-4 relative divide-zinc-200 dark:divide-zinc-800 divide-y w-fit">
96
+
<ul class="space-y-4 relative divide-zinc-200 dark:divide-zinc-800 divide-y sm:w-fit">
97
97
{items.length > 0
98
98
? items.map((item) => <Item item={item} key={item.itemUri} />)
99
99
: (
+11
-5
src/components/TimelineItem.tsx
+11
-5
src/components/TimelineItem.tsx
···
5
5
import { formatRelativeTime, galleryLink } from "../utils.ts";
6
6
import { ActorInfo } from "./ActorInfo.tsx";
7
7
import { GalleryPreviewLink } from "./GalleryPreviewLink.tsx";
8
+
import { ModerationWrapper } from "./ModerationWrapper.tsx";
8
9
9
10
export function TimelineItem({ item }: Readonly<{ item: TimelineItem }>) {
10
11
return (
11
12
<li>
12
-
<div class="w-fit flex flex-col gap-4 pb-4">
13
+
<div class="flex flex-col gap-4 pb-4 max-w-md">
13
14
<div class="flex items-center justify-between gap-2 w-full">
14
15
<ActorInfo profile={item.actor} />
15
16
<span class="shrink-0">
···
18
19
</div>
19
20
{item.gallery.items?.filter(isPhotoView).length
20
21
? (
21
-
<GalleryPreviewLink
22
-
gallery={item.gallery}
23
-
/>
22
+
<ModerationWrapper
23
+
moderationDecision={item.modDecision}
24
+
class="gap-2 sm:min-w-md"
25
+
>
26
+
<GalleryPreviewLink
27
+
gallery={item.gallery}
28
+
/>
29
+
</ModerationWrapper>
24
30
)
25
31
: null}
26
32
<p class="w-full flex items-baseline gap-1">
27
-
{item.itemType === "favorite" ? "Favorited" : "Created"}{" "}
33
+
Created{" "}
28
34
<a
29
35
href={galleryLink(
30
36
item.gallery.creator.handle,
+19
-1
src/lib/actor.ts
+19
-1
src/lib/actor.ts
···
1
1
import { Record as BskyProfile } from "$lexicon/types/app/bsky/actor/profile.ts";
2
+
import { Label } from "$lexicon/types/com/atproto/label/defs.ts";
2
3
import { Record as TangledProfile } from "$lexicon/types/sh/tangled/actor/profile.ts";
3
4
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
4
5
import { Record as GrainProfile } from "$lexicon/types/social/grain/actor/profile.ts";
···
77
78
78
79
const galleryPhotosMap = getGalleryItemsAndPhotos(ctx, galleries);
79
80
const creator = getActorProfile(did, ctx);
81
+
const labelMap = new Map<string, Label[]>();
82
+
for (const gallery of galleries) {
83
+
const labels = ctx.indexService.queryLabels({ subjects: [gallery.uri] });
84
+
labelMap.set(gallery.uri, labels);
85
+
}
80
86
81
87
if (!creator) return [];
82
88
83
89
return galleries.map((gallery) =>
84
-
galleryToView(gallery, creator, galleryPhotosMap.get(gallery.uri) ?? [])
90
+
galleryToView(
91
+
gallery,
92
+
creator,
93
+
galleryPhotosMap.get(gallery.uri) ?? [],
94
+
labelMap.get(gallery.uri) ?? [],
95
+
)
85
96
);
86
97
}
87
98
···
127
138
new Set(galleries.map((gallery) => gallery.did)),
128
139
);
129
140
141
+
const labelMap = new Map<string, Label[]>();
142
+
for (const gallery of galleries) {
143
+
const labels = ctx.indexService.queryLabels({ subjects: [gallery.uri] });
144
+
labelMap.set(gallery.uri, labels);
145
+
}
146
+
130
147
const { items: profiles } = ctx.indexService.getRecords<
131
148
WithBffMeta<GrainProfile>
132
149
>(
···
152
169
gallery,
153
170
creator,
154
171
galleryPhotosMap.get(gallery.uri) ?? [],
172
+
labelMap.get(gallery.uri) ?? [],
155
173
);
156
174
})
157
175
.filter((g) => g !== null);
+7
src/lib/gallery.ts
+7
src/lib/gallery.ts
···
1
+
import { Label } from "$lexicon/types/com/atproto/label/defs.ts";
1
2
import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts";
2
3
import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts";
3
4
import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts";
···
83
84
const galleryPhotosMap = getGalleryItemsAndPhotos(ctx, [gallery]);
84
85
const profile = getActorProfile(did, ctx);
85
86
if (!profile) return null;
87
+
const labels = ctx.indexService.queryLabels({
88
+
subjects: [gallery.uri],
89
+
});
86
90
return galleryToView(
87
91
gallery,
88
92
profile,
89
93
galleryPhotosMap.get(gallery.uri) ?? [],
94
+
labels,
90
95
);
91
96
}
92
97
···
131
136
record: WithBffMeta<Gallery>,
132
137
creator: Un$Typed<ProfileView>,
133
138
items: Photo[],
139
+
labels: Label[] = [],
134
140
): Un$Typed<GalleryView> {
135
141
return {
136
142
uri: record.uri,
···
140
146
items: items
141
147
?.map((item) => itemToView(record.did, item))
142
148
.filter(isPhotoView),
149
+
labels,
143
150
indexedAt: record.indexedAt,
144
151
};
145
152
}
+104
src/lib/moderation.ts
+104
src/lib/moderation.ts
···
1
+
import {
2
+
Label,
3
+
LabelValueDefinition,
4
+
} from "$lexicon/types/com/atproto/label/defs.ts";
5
+
import { AtUri } from "@atproto/syntax";
6
+
import { BffContext } from "@bigmoves/bff";
7
+
import { State } from "../state.ts";
8
+
9
+
export type ModerationDecsion = {
10
+
name: string;
11
+
description: string;
12
+
labeledBy: string;
13
+
blurs: string;
14
+
isMe: boolean;
15
+
src: string;
16
+
val: string;
17
+
};
18
+
19
+
export async function moderateGallery(labels: Label[], ctx: BffContext<State>) {
20
+
const did = ctx.currentUser?.did;
21
+
const labelDefinitions = await ctx.getLabelerDefinitions();
22
+
23
+
for (const label of labels ?? []) {
24
+
const labelSubject = new AtUri(label.uri).hostname;
25
+
const labelerAtpData = await ctx.didResolver.resolveAtprotoData(label.src);
26
+
// Try labelDefinitions first, then fallback to atprotoLabelValueDefinitions
27
+
let defs = labelDefinitions[label.src]?.labelValueDefinitions?.filter((
28
+
def,
29
+
) => def.identifier === label.val);
30
+
if (
31
+
(!defs || defs.length === 0) && atprotoLabelValueDefinitions[label.val]
32
+
) {
33
+
defs = [atprotoLabelValueDefinitions[label.val]];
34
+
}
35
+
if (defs && defs.length > 0) {
36
+
const enLocale = defs[0].locales?.find((locale) => locale.lang === "en");
37
+
if (enLocale) {
38
+
return {
39
+
name: enLocale.name,
40
+
description: enLocale.description,
41
+
labeledBy: labelerAtpData.handle ?? label.src,
42
+
blurs: defs[0].blurs ?? "",
43
+
isMe: labelSubject === did,
44
+
src: label.src,
45
+
val: label.val,
46
+
};
47
+
}
48
+
}
49
+
}
50
+
return undefined;
51
+
}
52
+
53
+
export async function isLabeler(did: string, ctx: BffContext<State>) {
54
+
const labelDefinitions = await ctx.getLabelerDefinitions();
55
+
return Object.keys(labelDefinitions).includes(did);
56
+
}
57
+
58
+
export const atprotoLabelValueDefinitions: Record<
59
+
string,
60
+
LabelValueDefinition
61
+
> = {
62
+
porn: {
63
+
blurs: "media",
64
+
severity: "high",
65
+
identifier: "porn",
66
+
adultOnly: true,
67
+
defaultSetting: "hide",
68
+
locales: [
69
+
{
70
+
lang: "en",
71
+
name: "Adult Content",
72
+
description: "Explicit sexual images.",
73
+
},
74
+
],
75
+
},
76
+
sexual: {
77
+
blurs: "media",
78
+
severity: "high",
79
+
identifier: "porn",
80
+
adultOnly: true,
81
+
defaultSetting: "hide",
82
+
locales: [
83
+
{
84
+
lang: "en",
85
+
name: "Sexually Suggestive",
86
+
description: "Does not include nudity.",
87
+
},
88
+
],
89
+
},
90
+
nudity: {
91
+
blurs: "media",
92
+
severity: "high",
93
+
identifier: "porn",
94
+
adultOnly: true,
95
+
defaultSetting: "hide",
96
+
locales: [
97
+
{
98
+
lang: "en",
99
+
name: "Non-sexual Nudity",
100
+
description: "E.g. artistic nudes.",
101
+
},
102
+
],
103
+
},
104
+
};
+20
-9
src/lib/timeline.ts
+20
-9
src/lib/timeline.ts
···
9
9
import { BffContext, QueryOptions, WithBffMeta } from "@bigmoves/bff";
10
10
import { getActorProfile } from "./actor.ts";
11
11
import { galleryToView, getGalleryItemsAndPhotos } from "./gallery.ts";
12
+
import { moderateGallery, ModerationDecsion } from "./moderation.ts";
12
13
13
14
export type TimelineItemType = "gallery";
14
15
···
20
21
itemUri: string;
21
22
actor: Un$Typed<ProfileView>;
22
23
gallery: GalleryView;
24
+
modDecision?: ModerationDecsion;
23
25
};
24
26
25
27
type TimelineOptions = {
···
27
29
followingDids?: Set<string>;
28
30
};
29
31
30
-
function processGalleries(
32
+
async function processGalleries(
31
33
ctx: BffContext,
32
34
options?: TimelineOptions,
33
-
): TimelineItem[] {
35
+
): Promise<TimelineItem[]> {
34
36
const items: TimelineItem[] = [];
35
37
36
38
let whereClause: QueryOptions["where"] = options?.actorDid
···
70
72
new AtUri(gallery.uri).rkey
71
73
}`;
72
74
const galleryPhotos = galleryPhotosMap.get(galleryUri) || [];
75
+
const labels = ctx.indexService.queryLabels({
76
+
subjects: [gallery.uri],
77
+
});
78
+
const galleryView = galleryToView(gallery, profile, galleryPhotos, labels);
79
+
80
+
let modDecision: ModerationDecsion | undefined = undefined;
81
+
if (galleryView.labels?.length) {
82
+
modDecision = await moderateGallery(labels, ctx);
83
+
}
73
84
74
-
const galleryView = galleryToView(gallery, profile, galleryPhotos);
75
85
items.push({
76
86
itemType: "gallery",
77
87
createdAt: gallery.createdAt,
78
88
itemUri: galleryView.uri,
79
89
actor: galleryView.creator,
80
90
gallery: galleryView,
91
+
modDecision,
81
92
});
82
93
}
83
94
84
95
return items;
85
96
}
86
97
87
-
function getTimelineItems(
98
+
async function getTimelineItems(
88
99
ctx: BffContext,
89
100
options?: TimelineOptions,
90
-
): TimelineItem[] {
91
-
const galleryItems = processGalleries(ctx, options);
101
+
): Promise<TimelineItem[]> {
102
+
const galleryItems = await processGalleries(ctx, options);
92
103
return galleryItems.sort(
93
104
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
94
105
);
···
114
125
return new Set(follows.map((f) => f.subject).filter(Boolean));
115
126
}
116
127
117
-
export function getTimeline(
128
+
export async function getTimeline(
118
129
ctx: BffContext,
119
130
type: "timeline" | "following",
120
131
graph: SocialNetwork,
121
-
): TimelineItem[] {
132
+
): Promise<TimelineItem[]> {
122
133
let followingDids: Set<string> | undefined = undefined;
123
134
if (type === "following") {
124
135
followingDids = getFollowingDids(graph, ctx);
125
136
}
126
-
const galleryItems = processGalleries(ctx, { followingDids });
137
+
const galleryItems = await processGalleries(ctx, { followingDids });
127
138
return galleryItems.sort(
128
139
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
129
140
);
+5
src/main.tsx
+5
src/main.tsx
···
5
5
import { PDS_HOST_URL } from "./env.ts";
6
6
import { onError } from "./lib/errors.ts";
7
7
import * as actionHandlers from "./routes/actions.tsx";
8
+
import { handler as communityGuidelinesHandler } from "./routes/community_guidelines.tsx";
8
9
import * as dialogHandlers from "./routes/dialogs.tsx";
9
10
import { handler as exploreHandler } from "./routes/explore.tsx";
10
11
import { handler as followersHandler } from "./routes/followers.tsx";
···
25
26
26
27
bff({
27
28
appName: "Grain Social",
29
+
appLabelers: ["did:plc:nd45zozo34cr4pvxqu4rtr7e"],
30
+
appLabelerCollection: "social.grain.labeler.service",
28
31
collections: [
29
32
"social.grain.gallery",
30
33
"social.grain.actor.profile",
···
70
73
route("/support/privacy", legalHandlers.privacyHandler),
71
74
route("/support/terms", legalHandlers.termsHandler),
72
75
route("/support/copyright", legalHandlers.copyrightHandler),
76
+
route("/support/community-guidelines", communityGuidelinesHandler),
73
77
route("/dialogs/create-account", dialogHandlers.createAccount),
74
78
route("/dialogs/gallery/new", dialogHandlers.createGallery),
75
79
route("/dialogs/gallery/:rkey", dialogHandlers.editGallery),
76
80
route("/dialogs/gallery/:rkey/sort", dialogHandlers.sortGallery),
81
+
route("/dialogs/label/:src/:val", dialogHandlers.labelValueDefinition),
77
82
route("/dialogs/profile", dialogHandlers.editProfile),
78
83
route("/dialogs/avatar/:handle", dialogHandlers.avatar),
79
84
route("/dialogs/image", dialogHandlers.image),
+123
src/routes/community_guidelines.tsx
+123
src/routes/community_guidelines.tsx
···
1
+
import { BffContext, RouteHandler } from "@bigmoves/bff";
2
+
import { ComponentChildren } from "preact";
3
+
import { Breadcrumb } from "../components/Breadcrumb.tsx";
4
+
import { State } from "../state.ts";
5
+
6
+
export const handler: RouteHandler = (
7
+
_req,
8
+
_params,
9
+
ctx: BffContext<State>,
10
+
) => {
11
+
ctx.state.meta = [
12
+
{ title: "Community Guidelines — Grain" },
13
+
];
14
+
return ctx.render(
15
+
<div className="px-4 py-4">
16
+
<Breadcrumb
17
+
items={[{ label: "support", href: "/support" }, {
18
+
label: "community guidelines",
19
+
}]}
20
+
/>
21
+
<h1 className="text-3xl font-bold mb-6 text-zinc-900 dark:text-white">
22
+
Community Guidelines
23
+
</h1>
24
+
<Section title="About Grain Social">
25
+
<p>
26
+
Grain Social is a photo-sharing service built on the AT Protocol.
27
+
These guidelines apply specifically to Grain Social. While the
28
+
protocol is decentralized and supports many independent services, our
29
+
focus is on fostering a respectful, creative, and safe experience
30
+
within our app.
31
+
</p>
32
+
</Section>
33
+
34
+
<Section title="Our Principles">
35
+
<ul className="list-disc pl-5 space-y-1">
36
+
<li>
37
+
<strong>User choice</strong>: We are committed to empowering users
38
+
with control over where their data is stored, how their content is
39
+
moderated, and which algorithms power their feeds (hopefully more
40
+
options soon!).
41
+
</li>
42
+
<li>
43
+
<strong>Welcoming space</strong>: We aim to build a friendly,
44
+
inclusive environment where people enjoy sharing and discovering
45
+
photos.
46
+
</li>
47
+
<li>
48
+
<strong>Evolving standards</strong>: Our policies will adapt over
49
+
time based on your feedback and the needs of the community.
50
+
</li>
51
+
</ul>
52
+
</Section>
53
+
54
+
<Section title="What’s Not Allowed">
55
+
<p>
56
+
Don't use Grain Social to break the law, harm others, or disrupt the
57
+
network. Specifically, do not:
58
+
</p>
59
+
<ul className="list-disc pl-5 space-y-1">
60
+
<li>Promote hate groups or terrorism</li>
61
+
<li>
62
+
Share child sexual abuse material or any sexual content involving
63
+
minors
64
+
</li>
65
+
<li>Engage in trafficking, exploitation, or predatory behavior</li>
66
+
<li>Trade illegal goods or substances</li>
67
+
<li>Share private personal info without consent</li>
68
+
<li>Hack, phish, scam, or impersonate others</li>
69
+
<li>Spam, abuse automation, or manipulate engagement</li>
70
+
<li>Violate copyrights or trademarks</li>
71
+
<li>Spread false or misleading election info</li>
72
+
<li>
73
+
Evade moderation actions (e.g., ban evasion) by creating new
74
+
accounts
75
+
</li>
76
+
</ul>
77
+
</Section>
78
+
79
+
<Section title="Respect Others">
80
+
<p>We expect respectful conduct. This includes avoiding:</p>
81
+
<ul className="list-disc pl-5 space-y-1">
82
+
<li>Harassment, bullying, or targeted abuse</li>
83
+
<li>Hate speech or extremist content</li>
84
+
<li>Threats of violence or glorification of harm</li>
85
+
<li>Promotion of self-harm or suicide</li>
86
+
<li>Graphic violence or non-consensual sexual content</li>
87
+
<li>Misleading impersonation of individuals or organizations</li>
88
+
</ul>
89
+
</Section>
90
+
91
+
<Section title="Reporting Violations">
92
+
<p>
93
+
Help us keep the community safe. You can report photos, galleries, or
94
+
accounts directly through the app (soon!) or by contacting us at{" "}
95
+
<a
96
+
href="mailto:support@grain.social"
97
+
className="text-sky-500 underline hover:underline"
98
+
>
99
+
support@grain.social
100
+
</a>
101
+
. Our moderation team will review and take action where needed.
102
+
Reports may consider off-platform context when relevant.
103
+
</p>
104
+
</Section>
105
+
</div>,
106
+
);
107
+
};
108
+
109
+
type SectionProps = {
110
+
title: string;
111
+
children: ComponentChildren;
112
+
};
113
+
114
+
const Section = ({ title, children }: SectionProps) => (
115
+
<section className="mb-8">
116
+
<h2 className="text-xl font-bold mb-2 text-zinc-800 dark:text-zinc-100">
117
+
{title}
118
+
</h2>
119
+
<div className="space-y-2 text-zinc-700 dark:text-zinc-300">
120
+
{children}
121
+
</div>
122
+
</section>
123
+
);
+30
src/routes/dialogs.tsx
+30
src/routes/dialogs.tsx
···
9
9
import { CreateAccountDialog } from "../components/CreateAccountDialog.tsx";
10
10
import { GalleryCreateEditDialog } from "../components/GalleryCreateEditDialog.tsx";
11
11
import { GallerySortDialog } from "../components/GallerySortDialog.tsx";
12
+
import { LabelDefinitionDialog } from "../components/LabelDefinitionDialog.tsx";
12
13
import { PhotoAltDialog } from "../components/PhotoAltDialog.tsx";
13
14
import { PhotoDialog } from "../components/PhotoDialog.tsx";
14
15
import { PhotoSelectDialog } from "../components/PhotoSelectDialog.tsx";
15
16
import { ProfileDialog } from "../components/ProfileDialog.tsx";
16
17
import { getActorPhotos, getActorProfile } from "../lib/actor.ts";
17
18
import { getGallery, getGalleryItemsAndPhotos } from "../lib/gallery.ts";
19
+
import { atprotoLabelValueDefinitions } from "../lib/moderation.ts";
18
20
import { photoToView } from "../lib/photo.ts";
19
21
import type { State } from "../state.ts";
20
22
···
162
164
) => {
163
165
return ctx.html(<CreateAccountDialog />);
164
166
};
167
+
168
+
export const labelValueDefinition: RouteHandler = async (
169
+
_req,
170
+
params,
171
+
ctx: BffContext<State>,
172
+
) => {
173
+
const src = params.src;
174
+
const val = params.val;
175
+
const labelerDeinitionsMap = await ctx.getLabelerDefinitions();
176
+
if (!labelerDeinitionsMap) return ctx.next();
177
+
const labelValueDefinitions = labelerDeinitionsMap[src]
178
+
?.labelValueDefinitions;
179
+
if (!labelValueDefinitions) return ctx.next();
180
+
181
+
let valDef = labelValueDefinitions.find((def) => def.identifier === val);
182
+
if (!valDef && typeof val === "string") {
183
+
valDef = atprotoLabelValueDefinitions[val];
184
+
}
185
+
186
+
if (!valDef) return ctx.next();
187
+
const labelerAtpData = await ctx.didResolver.resolveAtprotoData(src);
188
+
return ctx.html(
189
+
<LabelDefinitionDialog
190
+
labelByHandle={labelerAtpData?.handle}
191
+
labelValueDefinition={valDef}
192
+
/>,
193
+
);
194
+
};
+5
-1
src/routes/explore.tsx
+5
-1
src/routes/explore.tsx
···
5
5
import { Input } from "@bigmoves/bff/components";
6
6
import { ComponentChildren } from "preact";
7
7
import { ActorAvatar } from "../components/ActorAvatar.tsx";
8
+
import { LabelerAvatar } from "../components/LabelerAvatar.tsx";
8
9
import { profileToView } from "../lib/actor.ts";
9
10
import { getPageMeta } from "../meta.ts";
10
11
import type { State } from "../state.ts";
···
85
86
{profileViews.map((profile) => (
86
87
<li key={profile.did}>
87
88
<a class="flex items-center" href={`/profile/${profile.handle}`}>
88
-
<ActorAvatar profile={profile} size={32} class="mr-2" />
89
+
{/* @TODO remove hard-coded handler */}
90
+
{profile.handle === "moderation.grain.social"
91
+
? <LabelerAvatar profile={profile} size={32} class="mr-2" />
92
+
: <ActorAvatar profile={profile} size={32} class="mr-2" />}
89
93
<div class="flex flex-col">
90
94
<div class="font-semibold">
91
95
{profile.displayName || profile.handle}
+13
-2
src/routes/gallery.tsx
+13
-2
src/routes/gallery.tsx
···
3
3
import { BffContext, RouteHandler, WithBffMeta } from "@bigmoves/bff";
4
4
import { GalleryPage } from "../components/GalleryPage.tsx";
5
5
import { getGallery, getGalleryFavs } from "../lib/gallery.ts";
6
+
import { moderateGallery, ModerationDecsion } from "../lib/moderation.ts";
6
7
import { getGalleryMeta, getPageMeta } from "../meta.ts";
7
8
import type { State } from "../state.ts";
8
9
import { galleryLink } from "../utils.ts";
9
10
10
-
export const handler: RouteHandler = (
11
+
export const handler: RouteHandler = async (
11
12
_req,
12
13
params,
13
14
ctx: BffContext<State>,
···
30
31
31
32
ctx.state.scripts = ["photo_dialog.js", "masonry.js", "sortable.js"];
32
33
34
+
let modDecision: ModerationDecsion | undefined = undefined;
35
+
if (gallery.labels?.length) {
36
+
modDecision = await moderateGallery(gallery.labels ?? [], ctx);
37
+
}
38
+
33
39
return ctx.render(
34
-
<GalleryPage favs={favs} gallery={gallery} currentUserDid={did} />,
40
+
<GalleryPage
41
+
favs={favs}
42
+
gallery={gallery}
43
+
currentUserDid={did}
44
+
modDecision={modDecision}
45
+
/>,
35
46
);
36
47
};
+34
-3
src/routes/profile.tsx
+34
-3
src/routes/profile.tsx
···
1
-
import { BffContext, RouteHandler } from "@bigmoves/bff";
1
+
import { BffContext, LabelerPolicies, RouteHandler } from "@bigmoves/bff";
2
2
import { ProfilePage, ProfileTabs } from "../components/ProfilePage.tsx";
3
3
import {
4
4
getActorGalleries,
···
7
7
getActorProfiles,
8
8
} from "../lib/actor.ts";
9
9
import { getFollow, getFollowers, getFollowing } from "../lib/follow.ts";
10
+
import {
11
+
isLabeler as isLabelerFn,
12
+
moderateGallery,
13
+
ModerationDecsion,
14
+
} from "../lib/moderation.ts";
10
15
import { type SocialNetwork } from "../lib/timeline.ts";
11
16
import { getPageMeta } from "../meta.ts";
12
17
import type { State } from "../state.ts";
13
18
import { profileLink } from "../utils.ts";
14
19
15
-
export const handler: RouteHandler = (
20
+
export const handler: RouteHandler = async (
16
21
req,
17
22
params,
18
23
ctx: BffContext<State>,
···
31
36
const followers = getFollowers(actor.did, ctx);
32
37
const following = getFollowing(actor.did, ctx);
33
38
39
+
let labelerDefinitions: LabelerPolicies | undefined = undefined;
40
+
const isLabeler = await isLabelerFn(actor.did, ctx);
41
+
if (isLabeler) {
42
+
const labelerDefs = await ctx.getLabelerDefinitions();
43
+
labelerDefinitions = labelerDefs[actor.did] ?? [];
44
+
}
45
+
46
+
const galleryModDecisionsMap = new Map<string, ModerationDecsion>();
47
+
for (const gallery of galleries) {
48
+
if (!gallery.labels || gallery.labels.length === 0) {
49
+
continue;
50
+
}
51
+
const modDecision = await moderateGallery(
52
+
gallery.labels ?? [],
53
+
ctx,
54
+
);
55
+
if (!modDecision) {
56
+
continue;
57
+
}
58
+
galleryModDecisionsMap.set(gallery.uri, modDecision);
59
+
}
60
+
34
61
if (!profile) return ctx.next();
35
62
36
63
let followUri: string | undefined;
···
69
96
selectedTab="favs"
70
97
galleries={galleries}
71
98
galleryFavs={galleryFavs}
99
+
galleryModDecisionsMap={galleryModDecisionsMap}
72
100
/>,
73
101
);
74
102
}
···
81
109
followUri={followUri}
82
110
loggedInUserDid={ctx.currentUser?.did}
83
111
profile={profile}
84
-
selectedTab="galleries"
112
+
selectedTab={isLabeler ? "labels" : "galleries"}
85
113
galleries={galleries}
114
+
galleryModDecisionsMap={galleryModDecisionsMap}
115
+
isLabeler={isLabeler}
116
+
labelerDefinitions={labelerDefinitions}
86
117
/>,
87
118
);
88
119
};
+2
-2
src/routes/timeline.tsx
+2
-2
src/routes/timeline.tsx
···
6
6
import { getPageMeta } from "../meta.ts";
7
7
import type { State } from "../state.ts";
8
8
9
-
export const handler: RouteHandler = (
9
+
export const handler: RouteHandler = async (
10
10
req,
11
11
_params,
12
12
ctx: BffContext<State>,
···
44
44
graph = actorProfiles[0];
45
45
}
46
46
47
-
const items = getTimeline(
47
+
const items = await getTimeline(
48
48
ctx,
49
49
tab === "following" ? "following" : "timeline",
50
50
graph,
+24
-1
static/styles.css
+24
-1
static/styles.css
···
1
-
/*! tailwindcss v4.1.7 | MIT License | https://tailwindcss.com */
1
+
/*! tailwindcss v4.1.8 | MIT License | https://tailwindcss.com */
2
2
@layer properties;
3
3
@layer theme, base, components, utilities;
4
4
@layer theme {
···
483
483
.resize {
484
484
resize: both;
485
485
}
486
+
.list-disc {
487
+
list-style-type: disc;
488
+
}
486
489
.grid-cols-1 {
487
490
grid-template-columns: repeat(1, minmax(0, 1fr));
488
491
}
···
527
530
}
528
531
.gap-4 {
529
532
gap: calc(var(--spacing) * 4);
533
+
}
534
+
.space-y-1 {
535
+
:where(& > :not(:last-child)) {
536
+
--tw-space-y-reverse: 0;
537
+
margin-block-start: calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));
538
+
margin-block-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)));
539
+
}
530
540
}
531
541
.space-y-2 {
532
542
:where(& > :not(:last-child)) {
···
695
705
.pl-2 {
696
706
padding-left: calc(var(--spacing) * 2);
697
707
}
708
+
.pl-5 {
709
+
padding-left: calc(var(--spacing) * 5);
710
+
}
698
711
.text-center {
699
712
text-align: center;
700
713
}
···
932
945
min-width: 120px;
933
946
}
934
947
}
948
+
.sm\:min-w-md {
949
+
@media (width >= 40rem) {
950
+
min-width: var(--container-md);
951
+
}
952
+
}
935
953
.sm\:grid-cols-3 {
936
954
@media (width >= 40rem) {
937
955
grid-template-columns: repeat(3, minmax(0, 1fr));
···
1052
1070
.dark\:text-zinc-100 {
1053
1071
@media (prefers-color-scheme: dark) {
1054
1072
color: var(--color-zinc-100);
1073
+
}
1074
+
}
1075
+
.dark\:text-zinc-200 {
1076
+
@media (prefers-color-scheme: dark) {
1077
+
color: var(--color-zinc-200);
1055
1078
}
1056
1079
}
1057
1080
.dark\:text-zinc-300 {